MCP.so
ログイン
ブログに戻る

MCP の動的ツール登録と検出を徹底解説

2026年10月15日MCP.so TeamMCP.so Team

MCP の動的ツール登録と検出:クライアントが tools/list、ページネーション、list_changed 通知でツールを見つける仕組みと、TypeScript と Python SDK で実行時にツールを追加・削除する方法を解説します。

MCP クライアントは、接続中の各サーバーに tools/list を呼び出し、nextCursor をたどってすべてのページを取得することでツールを検出します。実行時にサーバーのツールセットが変わると、tools.listChanged を宣言しているサーバーは notifications/tools/list_changed を送り、クライアントは再び tools/list を呼び出します。動的ツール登録とは、このループのサーバー側にすぎません。ツールを追加・削除・有効化・更新し、通知を送るだけです。

仕組みはこれだけです。つまずきやすいのは、現行仕様リビジョン(2026-07-28)の新しいルール、v2 の TypeScript・Python SDK で変わった API、そしてクライアントごとに異なる通知の扱いです。本ガイドではこの 3 つをすべて扱います。

ツール検出の仕組み

検出は 4 つのステップで行われ、すべて MCP 仕様の Tools セクションで定義されています:

  1. 機能の宣言。 ツールを公開するサーバーは tools 機能を宣言します。listChanged: true を設定すると、一覧が変わったときにクライアントへ通知することを約束します。
  2. 一覧の取得。 クライアントが tools/list を送ります。各ツールは name、任意の title、description、inputSchema(JSON Schema)、任意の outputSchema、annotations、icons とともに返されます。
  3. ページネーション。 レスポンスに nextCursor があれば、クライアントはそのカーソルを付けて再度 tools/list を送り、カーソルが返らなくなるまで続けます。
  4. 鮮度の維持。 クライアントは notifications/tools/list_changed を受け取ったとき、またはキャッシュした結果が古くなったときに再取得します。

現行リビジョンの tools/list レスポンスは次のとおりです:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "tools": [
      {
        "name": "get_weather",
        "description": "Get current weather information for a location",
        "inputSchema": {
          "type": "object",
          "properties": { "location": { "type": "string" } },
          "required": ["location"]
        }
      }
    ],
    "nextCursor": "next-page-cursor",
    "ttlMs": 300000,
    "cacheScope": "public"
  }
}

ページネーションのルール

ページネーションの仕様は短いですが厳密です:

  • カーソルは 不透明 です。クライアントは解析も変更もしてはいけません。
  • ページサイズはサーバーが決めます。クライアントは固定サイズを前提にしてはいけません。
  • nextCursor が ない ことが終端を意味します。空文字列は有効なカーソルであり、終端ではありません。
  • 無効なカーソルにはエラー -32602(Invalid params)を返すべきです。

最小限の手動ループ:

let cursor: string | undefined;
const all = [];
do {
  const page = await client.listTools(
    cursor === undefined ? undefined : { cursor }
  );
  all.push(...page.tools);
  cursor = page.nextCursor;
} while (cursor !== undefined);

実際にはほとんど書く必要はありません。v2 の TypeScript クライアントでは、カーソルなしの listTools() がすべてのページを自動で集約します(デフォルトでは listMaxPages により最大 64 ページ)。

キャッシュのヒント(2026-07-28 で追加)

現行リビジョンではキャッシュが追加されました。サーバーは tools/list の結果に ttlMs と cacheScope を含めなければなりません:

  • ttlMs——クライアントが一覧を新鮮とみなしてよい時間。鮮度のヒントであり、ポーリング間隔ではありません。
  • cacheScope: "public"——すべてのユーザーで同一。共有ゲートウェイがキャッシュしてもかまいません。
  • cacheScope: "private"——一覧が要求者によって変わる(たとえば OAuth スコープで絞り込む)場合で、異なる認可コンテキスト間で共有してはいけません。

TTL と listChanged は併用されます。TTL が切れていなくても、通知を受け取ればキャッシュした一覧は即座に無効になります。通知をプッシュできないサーバーでも、TTL を短くすればクライアントをそれなりに最新の状態に保てます。

変更通知:旧来のセッションと新しいストリーム

list_changed 通知がどのようにクライアントに届くかは、ネゴシエートされたプロトコルリビジョンによって異なります:

2025-11-25 以前2026-07-28(現行)
配信方法初期化後、サーバーがセッション上でいつでも送信クライアントが toolsListChanged: true で開いた subscriptions/listen ストリームでのみ送信
サーバーの確認応答—notifications/subscriptions/acknowledged で送信する内容を通知
ツール一覧を接続ごとに変えてよいか?実際にはよく使われていた(セッション状態)不可——接続ごとに、または同じ接続上の他のリクエストの副作用として変えてはいけない
呼び出し元ごとに変えてよいか?可可。リクエストに含まれる認可(付与されたスコープなど)に応じて変えられる

「動的ツール」にとって最も重要なのは 3 行目です。2026-07-28 では MCP にプロトコルレベルのセッションがないため、「login を呼ぶと、この接続に新しいツールが現れる」という従来のパターンはもう認められません。ツールはグローバルに変わる(プラグインの読み込み、フィーチャーフラグの切り替え、上流 API のエンドポイント追加)か、呼び出し元の認証情報によって異なるかのどちらかです。呼び出しをまたいで保持するワークフローの状態については、あるツールから明示的なハンドルを返し、次のツールに渡すことを仕様は推奨しています。

仕様はまた、ツールを 決定的な順序 で返すことをサーバーに求めています。これによりクライアントのキャッシュが有効に保たれ、LLM のプロンプトキャッシュのヒット率も上がります。

実行時にツールを登録・登録解除する

TypeScript SDK(v2)

v2 の TypeScript SDK は @modelcontextprotocol/server として配布されています(@modelcontextprotocol/sdk の後継)。registerTool は enable()、disable()、update()、remove() を持つハンドルを返します。SDK の通知ガイドによると、ハンドルを通じた変更はすべて自動で list_changed を送信 し、ツールを登録した時点で McpServer は tools.listChanged: true を宣言します。

import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';

serveStdio(() => {
  const server = new McpServer({ name: 'reports', version: '1.0.0' });

  const exportCsv = server.registerTool(
    'export_csv',
    {
      description: 'Export a report as CSV',
      inputSchema: z.object({ reportId: z.string() }),
    },
    async ({ reportId }) => ({
      content: [{ type: 'text', text: `exported ${reportId}` }],
    })
  );

  // 機能が有効になるまで tools/list に表示しない
  exportCsv.disable();

  // 独自のフィーチャーフラグのフック(SDK の一部ではない)
  onFeatureFlag('exports', (on) =>
    on ? exportCsv.enable() : exportCsv.disable()
  );

  return server;
});

無効化されたツールは tools/list から消え、呼び出すと Invalid params エラーで失敗します。update({ description, paramsSchema, callback, ... }) でツールをその場で変更でき、remove() で削除できます。SDK から見えない理由で変更が起きた場合は、自分で server.sendToolListChanged() を呼んでください。

createMcpHandler で構築したステートレスな HTTP デプロイでは、McpServer インスタンスはリクエストごとに作られるため、ハンドラー経由で発行します:handler.notify.toolsChanged() は、ツールの変更を要求したすべての subscriptions/listen ストリームに届きます。

Python SDK(v2)

mcp 2.x では FastMCP が MCPServer に改名されました(from mcp.server.mcpserver import MCPServer)。add_tool() と remove_tool() がありますが、TypeScript のハンドルとは異なり、自動では通知しません——イベントは自分で発行します。ツール内では await ctx.notify_tools_changed() を使います。リクエストの外(バックグラウンドジョブ、ファイル監視)では、独自のサブスクリプションバスを渡してそこに発行します:

from mcp.server.mcpserver import MCPServer
from mcp.server.subscriptions import InMemorySubscriptionBus, ToolsListChanged

bus = InMemorySubscriptionBus()
mcp = MCPServer("reports", subscriptions=bus)


def export_csv(report_id: str) -> str:
    """Export a report as CSV."""
    return f"exported {report_id}"


async def set_exports_enabled(enabled: bool) -> None:
    if enabled:
        mcp.add_tool(export_csv)
    else:
        mcp.remove_tool("export_csv")
    await bus.publish(ToolsListChanged())


if __name__ == "__main__":
    mcp.run()

ctx.notify_tools_changed() とバスは subscriptions/listen ストリームに届きます。これが 2026-07-28 のクライアントの待ち受け方法です。旧リビジョンのクライアントには、セッションレベルの await ctx.session.send_tool_list_changed() で従来の通知を送ります。複数レプリカのデプロイでは、Redis や NATS などの上に SubscriptionBus プロトコルを実装すれば、すべてのレプリカのストリームにイベントが届きます。

スタンドアロンの FastMCP フレームワークには独自の API(mcp.disable(names=...)、mcp.enable(tags=..., only=True)、タグベースの可視性制御)があり、アクティブなリクエスト内で呼び出すと list_changed 通知が自動で送られるとドキュメントに記載されています。インストールしたバージョンのドキュメントを確認してください。

クライアント側の実装

v2 の TypeScript クライアントでは、ハンドラーを一度登録するだけで SDK が再取得してくれます(デフォルトで 300 ミリ秒のデバウンス付き):

import { Client } from '@modelcontextprotocol/client';

const client = new Client(
  { name: 'my-host', version: '1.0.0' },
  {
    listChanged: {
      tools: {
        onChanged: (error, tools) => {
          if (error) return console.error('Refresh failed:', error);
          console.log('Tools refreshed:', tools?.length);
        },
      },
    },
  }
);

このハンドラーは、サーバーが tools.listChanged を宣言している場合にのみ有効になります。複数のサーバーからツールを集約する場合は、ツール名にサーバー識別子のプレフィックスを付けてください。仕様は、2 つのサーバーがどちらも search ツールを公開しうること、そして serverInfo.name が一意である保証はないことを注意喚起しています。

クライアント対応の注意点

仕様はサーバーが通知を送る べき としていますが、ホストに同じ反応を強制するものではありません。動的ツールを前提に設計する前に、対象ホストの実際の挙動を確認しましょう:

  • Claude Code のドキュメントによると、対話セッションでは list_changed を受け取るとツール・プロンプト・リソースを再取得し、-p(ヘッドレス)モードと Agent SDK ではツール一覧のみを更新します。2026-07-28 ランタイムでは通知ストリームを開いたまま保持し、何度も閉じるストリームの再接続頻度を制限するため、サーバーレスのホストでは /mcp から再接続するまで更新が遅れることがあります。
  • 他のホスト はバージョンやプロトコルリビジョンによって異なります。接続時にしかツールを取得しないものもあります。実行時の変更は最適化と捉え、初期の一覧だけでもサーバーが使える状態にしておきましょう。
  • 本当の制約はコンテキストの予算です。 ツール定義を遅延読み込みするホスト(Claude Code のツール検索など)では大きなツールセットのコストが下がるため、ツールを動的に隠す必要自体がなくなることも多いです。

実行時登録が適さない場合は、起動時に選ぶ静的な ツールセット(GitHub MCP Server は --toolsets フラグを使用)、大きなサーバーを用途別の小さなサーバーに分割する方法、あるいは複数のサーバーを 1 つのエンドポイントにまとめるゲートウェイ(Docker MCP Gateway ガイドを参照)を検討してください。

レジストリ:見つけるのはツールではなくサーバー

「検出(discovery)」という言葉は サーバー を探す意味でも使われます。それは別のレイヤーです:

  • 公式の MCP Registry(プレビュー段階)は、検証済みの名前空間の下に server.json メタデータ——名前、パッケージまたはリモート URL、インストール手順——を保存します。主に下流のアグリゲーターやマーケットプレイスが利用するためのもので、ホストアプリケーションが直接使うことは想定されていません。
  • MCP.so のようなディレクトリはその上にキュレーション、カテゴリ、検索を提供し、tools/list が行われる前にサーバーを見つけられるようにします。
  • プロトコル内では、server/discover(2026-07-28 で追加)が、他のリクエストに先立ってサーバーのサポートバージョン、機能、識別情報を返します。

つまり全体の流れは、レジストリやディレクトリがサーバーを見つけ、server/discover が互換性を確認し、tools/list がツールを見つけ、list_changed が一覧を最新に保つ、というものです。

リリース前にテストする

MCP Inspector で動的な挙動を検証しましょう。接続してフィーチャーフラグを切り替え、Protocol タブで notifications/tools/list_changed と続く tools/list を確認します。Server Settings で Fetch Lists One Page at a Time をオンにすれば、ページネーションを 1 ページずつ確認できます。また、変化するツール一覧はそれ自体がセキュリティ上の攻撃面でもあります——ユーザーが承認した後にツールの説明が変わる可能性があるからです——MCP セキュリティのベストプラクティスもあわせてお読みください。

次のステップ

よくある質問

MCP クライアントはどうやってツールを検出しますか?

クライアントは接続中の各サーバーに tools/list を呼び出し、nextCursor をたどってすべてのページを取得します。サーバーが notifications/tools/list_changed を送ったとき、またはキャッシュした一覧の ttlMs が切れたときに再取得します。

MCP サーバーは実行時にツールを追加・削除できますか?

できます。tools.listChanged を宣言していれば、ツールセットを変更して notifications/tools/list_changed を送ります。TypeScript SDK では registerTool のハンドルの enable、disable、update、remove が自動で通知を送り、Python SDK では自分で発行します。

MCP サーバーはユーザーごとに異なるツールを見せられますか?

2026-07-28 の仕様では、リクエストに含まれる認可(付与された OAuth スコープなど)に応じてツール一覧を変えることはできますが、接続ごとや他のリクエストの副作用として変えてはいけません。そうした一覧には cacheScope 「private」を付けます。

すべての MCP クライアントが list_changed 通知に対応していますか?

いいえ。対応状況はホスト、バージョン、プロトコルリビジョンによって異なり、接続時にしかツールを取得しないクライアントもあります。Claude Code は list_changed で再取得するとドキュメントに記載していますが、対象クライアントごとにテストし、初期のツール一覧だけでもサーバーが使えるようにしておきましょう。