ツールカタログ

ToolCatalog は、すべてのエージェントツールの単一の真実の源です。各ツールは名前、可用性ルール、ビルダーと共に一度だけ宣言されます。呼び出し元(AgentServiceChatToolConfigurationMessagingAgentConcernChannelMentionWorkflow)は、ドリフトしがちな並列の 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 つの責務:

  1. ToolCatalog — 宣言的レジストリ。ツール 1 つにつき 1 行の register(...)。何が存在するかを決める。
  2. ToolContext — 呼び出しごとの値オブジェクト。今この瞬間に何が利用可能かを決める(スコープ内のユーザー/スペース/認証情報)。
  3. 呼び出し元のグループFULL_AGENT_GROUPS / CHAT_TOOL_GROUPS / MESSAGING_TOOL_GROUPS など。この呼び出し元がカタログのどのスライスを欲しいかを決める。

各呼び出し(SDK ツールオブジェクトのための build_toolsallowed_tools ホワイトリストのための tool_names)は現在のコンテキストに対して再評価されるため、false 可用性のツールは呼び出し元が気づかないうちに単に消えます。

いつ使うか

状況カタログを使う?
1つ以上のエージェントに公開される新しいインプロセス Themis ツールはい
既存ツールに新しい呼び出し元(ワークフロー / ジョブ)が必要はい — グループに追加して、グループを include
再利用なしで唯一のワークフローが使う一回限りのツールいいえ — AgentServiceextra_themis_tools: で渡す
外部 MCP チャネル(Linear、Sentry、Metabase、Google Drive / Calendar / Gmail、Google Analytics、Google Maps、Flight Search)ChannelRegistry に存在。許可ツールのみカタログにミラーされる
共有ツールに対する呼び出し元固有の副作用(ジョブのエンキュー、インスタンス変数への push):triggers ファクトリーを使う(下記参照)

ツールの登録

app/services/tool_catalog.rbregister_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 MCPmcp__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_onlyWeb/API チャットのみ(ウィジェット、ファイル作成、スキル、自動化、個人タスク、GDrive)
:triggersカタログ宣言、呼び出し元供給のファクトリー
:skill_builtinSDK 検出の “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)、Symbolctx に対して呼び出し)、または Procctx を引数に呼び出し)。

呼び出し元ごとのツールの 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_toolstool_names同じ ToolContext を再利用してください。credentialschannel_configured?can_query_themis?google_drive_available?skills_enabled?automations_enabled? はすべてコンテキスト単位でメモ化されます — コンテキストを共有することで各 predicate が2回実行されることを避けられます。

標準例

  • app/services/agent_service.rb:37FULL_AGENT_GROUPS(フルエージェントパス)
  • app/jobs/concerns/chat_tool_configuration.rb:15CHAT_TOOL_GROUPS(Web/API チャット)
  • app/services/concerns/messaging_agent_concern.rb:13MESSAGING_TOOL_GROUPS(Telegram/Teams)
  • app/services/workflows/channel_mention_workflow.rb:302:triggers でテミス MCP サーバーを構築

ファクトリー(:triggers グループ)

trigger_code_generationtrigger_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.conversationctx.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! を使うと隔離されたレジストリが得られます。teardownload_defaults! 経由でデフォルトを復元します。
  • ToolContext predicate の変更test/services/tool_context_test.rb にケースを追加。expects(:method).once でメモ化を検証。
  • 呼び出し元統合 → 呼び出し元の allowed_tools(または同等のもの)を実行し、期待される名前をアサート。パターンは test/jobs/chat_job_test.rb を参照。

カタログは同じ名前を誤って2回 register した場合に ArgumentError を発生させます — 重複ツールバグはプロダクションで暗黙のうちに発生するのではなく、起動時に表面化します。

ツールの非推奨化

  1. tool_catalog.rb から register(...) ブロックを削除。
  2. ツール名をハードコードしている full_names 参照を削除。
  3. app/prompts/app/services/、テスト全体で grep -r mcp__themis__<name> を実行 — 古い指示やアサーションをクリーンアップ。
  4. 基底のビルダーに他の呼び出し元がない場合、削除。

落とし穴

  • 空の Queueblank? — キューの存在をチェックする際は .present? ではなく !ctx.message_queue.nil? でゲート。
  • full_names: []allowed_tools 出力を抑制しますが、ビルダーは依然として実行されます — スキル/自動化のような SDK 検出のバンドルに使用。
  • ビルダーブロックは登録時ではなく呼び出しごとに実行されます。 ツールは毎回の build_tools 呼び出しで再構築されます。ビルダー内でツールインスタンスをメモ化しないでください。
  • ブール predicate には ||= ではなく defined? メモ化が必要false ||= ... は呼び出しごとに再実行されます。ToolContext は既にこれを行っているので、predicate を追加する際は同じパターンに従ってください。