ツールカタログ
ToolCatalog は、すべてのエージェントツールの単一の真実の源です。各ツールは名前、可用性ルール、ビルダーと共に一度だけ宣言されます。呼び出し元(AgentService、ChatToolConfiguration、MessagingAgentConcern、ChannelMentionWorkflow)は、ドリフトしがちな並列の allowed_tools 配列を維持する代わりに、必要なツールグループを列挙してオプトインします。
全体像
┌────────────────────────────────────────────────┐
│ app/services/tool_catalog.rb │
│ ───────────────────────────────────────────── │
│ ONE register(...) call per tool │
│ │
│ register(:save_memory, │
│ groups: :core, available: ...) │
│ register(:show_widget, │
│ groups: :chat_only, ...) │
│ register(:trigger_pr_review, │
│ groups: :triggers, ...) │
│ ... │
└────────────────────────────────────────────────┘
▲
build_tools(ctx, include: GROUPS)
tool_names(ctx, include: GROUPS)
│
┌───────────────┬────────────┼────────────┬──────────────┐
│ │ │ │ │
┌──────┴──────┐ ┌──────┴─────┐ ┌────┴──────┐ ┌───┴──────────────┐
│ AgentService│ │ ChatJob │ │ Messaging │ │ ChannelMention │
│ full agent │ │ web/API │ │ Telegram/ │ │ Workflow │
│ FULL_AGENT_ │ │ CHAT_TOOL_ │ │ Teams │ │ FULL_AGENT_ │
│ GROUPS │ │ GROUPS │ │ MESSAGING_│ │ GROUPS │
│ │ │ │ │ TOOL_ │ │ + :triggers │
│ │ │ │ │ GROUPS │ │ │
└─────────────┘ └────────────┘ └───────────┘ └──────────────────┘
│
▼
┌─────────────────────────┐
│ ToolContext │ per-invocation
│ ───────────────────── │
│ space, user, conv, │ memoized predicates:
│ message, queue, │ credentials
│ factories │ channel_configured?
│ │ skills_enabled?
│ │ ...
└─────────────────────────┘
│
▼
Catalog keeps entries where
(entry.groups ∩ include).any? AND entry.available?(ctx)
│
┌─────────────┴─────────────┐
▼ ▼
builder blocks run full_names resolve
→ SDK tool instances → "mcp__*" name strings
│ │
▼ ▼
create_sdk_mcp_server options.allowed_tools
(name: "themis")
3 つのピース、3 つの責務:
ToolCatalog— 宣言的レジストリ。ツール 1 つにつき 1 行のregister(...)。何が存在するかを決める。ToolContext— 呼び出しごとの値オブジェクト。今この瞬間に何が利用可能かを決める(スコープ内のユーザー/スペース/認証情報)。- 呼び出し元のグループ —
FULL_AGENT_GROUPS/CHAT_TOOL_GROUPS/MESSAGING_TOOL_GROUPSなど。この呼び出し元がカタログのどのスライスを欲しいかを決める。
各呼び出し(SDK ツールオブジェクトのための build_tools、allowed_tools ホワイトリストのための tool_names)は現在のコンテキストに対して再評価されるため、false 可用性のツールは呼び出し元が気づかないうちに単に消えます。
いつ使うか
| 状況 | カタログを使う? |
|---|---|
| 1つ以上のエージェントに公開される新しいインプロセス Themis ツール | はい |
| 既存ツールに新しい呼び出し元(ワークフロー / ジョブ)が必要 | はい — グループに追加して、グループを include |
| 再利用なしで唯一のワークフローが使う一回限りのツール | いいえ — AgentService の extra_themis_tools: で渡す |
| 外部 MCP チャネル(Linear、Sentry、Metabase、Google Drive / Calendar / Gmail、Google Analytics、Google Maps、Flight Search) | ChannelRegistry に存在。許可ツール名のみカタログにミラーされる |
| 共有ツールに対する呼び出し元固有の副作用(ジョブのエンキュー、インスタンス変数への push) | :triggers ファクトリーを使う(下記参照) |
ツールの登録
app/services/tool_catalog.rb の register_core_tools! または register_chat_only_tools! にエントリを1つ追加します:
register(:my_tool, groups: :core, available: :sentry_configured?) do |ctx|
MyToolBuilder.build_tool(space: ctx.space)
end
名前と full_names
シンボルは自動的に mcp__themis__my_tool になります。以下のルールに当てはまらない場合のみ full_names: を上書きします:
| ツールの種類 | 命名規則 | 例 |
|---|---|---|
| インプロセス Themis MCP | mcp__themis__<name>(デフォルト) | mcp__themis__get_pr_info |
| 外部 MCP サーバー | mcp__<channel>__<tool> | mcp__sentry__list_issues |
| SDK ビルトイン | 裸の名前 | "Skill" |
| 1つのビルダーが N 個のツールを生成 | 名前を明示的にリスト | mcp__themis__get_pr_info, mcp__themis__get_pr_diff, … |
| 名前がランタイム状態に依存 | tool_names 時に評価される Proc を渡す | :google_drive_bundle |
| ツールは登録されるが SDK 経由で発見される | full_names: [](出力を抑制) | :skills_bundle, :automations_bundle |
グループ
意味のある最小のセットを選びます:
| グループ | 到達範囲 |
|---|---|
:core | フルエージェント + チャット + メッセージング |
:chat_only | Web/API チャットのみ(ウィジェット、ファイル作成、スキル、自動化、個人タスク、GDrive) |
:triggers | カタログ宣言、呼び出し元供給のファクトリー |
:skill_builtin | SDK 検出の “Skill” ツール |
:linear, :sentry_full, :sentry_messaging, :metabase_full, :metabase_messaging | 外部 MCP 許可リストのミラー |
呼び出し元はエントリではなくグループで構成します — 除外リストは存在しません。「:core から1つを引く」が必要なら、答えは呼び出し側で減算するのではなくグループを分割することです。
可用性ルール
インラインの Proc よりも ToolContext の predicate シンボルを優先します:
register(:query_themis_data, groups: :core, available: :can_query_themis?) { ... }
条件が本当に一回限りの場合のみ Proc を使います。2つのエントリが同じチェックを共有するなら、代わりに ToolContext にメソッドを追加します。
available: は受け入れます: :always / true / nil(常に on)、false(常に off)、Symbol(ctx に対して呼び出し)、または Proc(ctx を引数に呼び出し)。
呼び出し元ごとのツールの JSON スキーマ拡張
同じカタログツールが呼び出し元固有のスキーマフィールドを必要とする場合(例:trigger_code_generation はメッセージングコンテキストでのみ project 引数が必要)、カタログではなくビルダー経由で渡します:
# ChannelMentionWorkflow — `project` を必須引数として追加
CodeGenerationToolBuilder.build_tool(
extra_properties: { project: { type: "string", description: "..." } },
extra_required: %w[project]
) { |args| handle_code_generation_tool(args) }
:triggers ファクトリー(次のセクション)でこれをワイヤーアップして、カタログエントリを宣言的に保ちます。
呼び出し元のワイヤリング
3つのステップ:グループ定数を宣言、ToolContext を構築、カタログに2回問い合わせ。
MY_GROUPS = %i[core triggers chat_only].freeze
ctx = ToolContext.new(
space: space, user: user, conversation: conv, message: msg,
message_queue: queue,
factories: { trigger_pr_review: ->(_ctx) { build_my_handler } }
)
tools = ToolCatalog.build_tools(ctx, include: MY_GROUPS)
mcp = ClaudeAgentSDK.create_sdk_mcp_server(name: "themis", tools: tools)
allowed = ToolCatalog.tool_names(ctx, include: MY_GROUPS)
build_tools と tool_names で同じ ToolContext を再利用してください。credentials、channel_configured?、can_query_themis?、google_drive_available?、skills_enabled?、automations_enabled? はすべてコンテキスト単位でメモ化されます — コンテキストを共有することで各 predicate が2回実行されることを避けられます。
標準例
app/services/agent_service.rb:37—FULL_AGENT_GROUPS(フルエージェントパス)app/jobs/concerns/chat_tool_configuration.rb:15—CHAT_TOOL_GROUPS(Web/API チャット)app/services/concerns/messaging_agent_concern.rb:13—MESSAGING_TOOL_GROUPS(Telegram/Teams)app/services/workflows/channel_mention_workflow.rb:302—:triggersでテミス MCP サーバーを構築
ファクトリー(:triggers グループ)
trigger_code_generation と trigger_pr_review はカタログ宣言ですが、ハンドラーは呼び出し元ごとに異なります(チャットはジョブをエンキュー、メンションはインスタンス変数にリクエストをキャプチャ)。カタログエントリはファクトリーの存在をゲートにします — トリガーが不要な呼び出し元は単にファクトリーを省略すれば、エントリは自動的にスキップされます:
# tool_catalog.rb 内 — 一度だけ宣言
register(:trigger_pr_review,
groups: :triggers,
available: ->(ctx) { ctx.factory(:trigger_pr_review).present? },
full_names: "mcp__themis__trigger_pr_review") do |ctx|
ctx.factory(:trigger_pr_review).call(ctx)
end
# 呼び出し元 — ファクトリーを供給
factories: { trigger_pr_review: ->(_ctx) { PRReviewTriggerToolBuilder.build_tool { |args| ... } } }
ファクトリーは副作用が呼び出し元の状態に依存する場合に使います。異なる設定だけが必要なら、代わりにビルダーブロック(ctx.conversation、ctx.message)経由で渡します。
外部 MCP とセキュリティフックのミラー
外部 MCP ツール名(:linear、:sentry_full、:metabase_full)は tool_names 時に ChannelRegistry.tools_for(...) から取得されます。制限バリアント(:sentry_messaging、:metabase_messaging)はチャット/メッセージングの表面積をフルエージェントパスより意図的に小さくしているため、ToolCatalog のハードコード定数になっています。
⚠️
allowed_toolsリストは外部 MCP サーバーに対して SDK によって強制されません — MCP サーバーはすべてのツールを登録し、SDK はホワイトリストに関係なく呼び出しを転送します。破壊的な Metabase ツールはPreToolUseフックを介してAgentSecurityHooks::METABASE_RESTRICTED_TOOLSによってブロックされます。新しい危険な外部ツールを追加する場合、本当のガードはカタログではなくフックです。
テスト
- カタログ DSL の変更 →
test/services/tool_catalog_test.rbにケースを追加。テスト先頭でToolCatalog.reset!を使うと隔離されたレジストリが得られます。teardownがload_defaults!経由でデフォルトを復元します。 ToolContextpredicate の変更 →test/services/tool_context_test.rbにケースを追加。expects(:method).onceでメモ化を検証。- 呼び出し元統合 → 呼び出し元の
allowed_tools(または同等のもの)を実行し、期待される名前をアサート。パターンはtest/jobs/chat_job_test.rbを参照。
カタログは同じ名前を誤って2回 register した場合に ArgumentError を発生させます — 重複ツールバグはプロダクションで暗黙のうちに発生するのではなく、起動時に表面化します。
ツールの非推奨化
tool_catalog.rbからregister(...)ブロックを削除。- ツール名をハードコードしている
full_names参照を削除。 app/prompts/、app/services/、テスト全体でgrep -r mcp__themis__<name>を実行 — 古い指示やアサーションをクリーンアップ。- 基底のビルダーに他の呼び出し元がない場合、削除。
落とし穴
- 空の
Queueはblank?— キューの存在をチェックする際は.present?ではなく!ctx.message_queue.nil?でゲート。 full_names: []はallowed_tools出力を抑制しますが、ビルダーは依然として実行されます — スキル/自動化のような SDK 検出のバンドルに使用。- ビルダーブロックは登録時ではなく呼び出しごとに実行されます。 ツールは毎回の
build_tools呼び出しで再構築されます。ビルダー内でツールインスタンスをメモ化しないでください。 - ブール predicate には
||=ではなくdefined?メモ化が必要 —false ||= ...は呼び出しごとに再実行されます。ToolContextは既にこれを行っているので、predicate を追加する際は同じパターンに従ってください。