メインコンテンツまでスキップ

MCPクライアント・ホストの実装

MCP クライアントおよびホストアプリケーションを実装する際の責務と設計上の注意点を解説します。サーバー側の実装は MCPサーバーの実装、認可の詳細は MCPの認可、ツールをモデルへ渡す戦略は コンテキスト肥大問題と対策、セキュリティの観点は MCPセキュリティベストプラクティス と AIエージェント特有の脅威 で扱います。ここでは、クライアントとホストが、2026-07-28 のステートレスなプロトコルの上で、何をどう実装するかに絞ります。

1. このドキュメントで扱うこと​

  • クライアントとホストの責務分担
  • リクエストの組み立て
  • server/discover の使いどころ
  • 認可フローの実装
  • ツールをモデルへ提示する方法
  • ユーザー同意の UI
  • キャッシュと更新
  • 購読
  • 障害対応
  • SSRF と URL 検証
  • 複数サーバーの管理

クライアントとホストの責務分担​

公式の用語では、ホストは利用者が操作するアプリケーション (Claude.ai や IDE など) で、全体の体験と、複数のクライアントの調整を担います。クライアントは、ホストが生成する、特定の MCP サーバーとの通信を担うプロトコル層のコンポーネントで、1 つのクライアントは 1 つのサーバーと 1 本の接続を扱います。

この分担が重要なのは、安全上の判断を、どちらが持つかが決まるからです。ユーザーへの同意、モデルへのツールの提示、サーバー間のデータの流れの制御は、利用者とモデルの両方を見ているホストの仕事です。クライアントは、プロトコルの正しさ (リクエストの組み立て、結果の解釈、キャッシュ、再試行) を担います。SDK が提供するのは主にクライアントで、同意の UI やモデルへの提示の方針は、ホストの実装者が決めます。

クライアントがサーバーに提供する機能には、次のものがあります。

機能内容2026-07-28 での扱い
Elicitationサーバーが、利用者に追加の入力を求める。form モード (スキーマに沿った構造化データ) と URL モード (画面外の操作で、データがクライアントを通らない) がある有効。MRTR のパターンで届く
Samplingサーバーが、クライアントを通じて LLM の補完を要求する非推奨。削除予定。新規の実装は LLM プロバイダーの API を直接使う
Rootsクライアントが、サーバーが対象にすべきディレクトリを伝える非推奨。削除予定。ツールの引数、リソースの URI、サーバーの設定で渡す

リクエストの組み立て​

2026-07-28 には、initialize のハンドシェイクがありません。クライアントは、すべてのリクエストを、単体で成立するように組み立てます。

_meta の必須フィールド: すべてのリクエストの _meta に、次のフィールドを含めます。

キー必須内容
io.modelcontextprotocol/protocolVersionはいこのリクエストのプロトコルバージョン (例: "2026-07-28")
io.modelcontextprotocol/clientCapabilitiesはいこのリクエストに関係するクライアントの機能。拡張もここで宣言する
io.modelcontextprotocol/clientInfoいいえ (推奨)クライアントの名前とバージョン。自己申告で、検証されない
io.modelcontextprotocol/logLevelいいえサーバーが出力する最小のログレベル
progressTokenいいえ進捗通知を受け取るためのトークン
traceparent などいいえOpenTelemetry のトレースコンテキスト

必須のフィールドが欠けると、サーバーは -32602 (HTTP では 400 Bad Request) で拒否します。宣言していない機能が必要なら、サーバーは MissingRequiredClientCapability (-32021) を返します。clientInfo と、サーバーが返す serverInfo は、表示、ログ、デバッグのための自己申告の値で、挙動の変更やセキュリティの判断に使ってはなりません。

Streamable HTTP のヘッダー: HTTP では、JSON の本体に加えて、ゲートウェイや WAF が本体を解析せずに処理できるよう、一部の値をヘッダーに写します。

  • Accept に、application/json と text/event-stream の両方を入れる。サーバーは、リクエストごとにどちらでも返せ、クライアントは両方を扱えなければならない
  • MCP-Protocol-Version を入れる。値は、本体の _meta の protocolVersion と一致させる
  • Mcp-Method に、メソッド名を入れる
  • tools/call、resources/read、prompts/get では、Mcp-Name に、params.name または params.uri を入れる
  • ツールの引数に x-mcp-header が付いていれば、Mcp-Param-{name} として写す。クライアントは、これに対応しなければならない

ヘッダーの値は、可視 ASCII の範囲に収まらない場合 (非 ASCII 文字、制御文字、前後の空白) と、=?base64?...?= の形に見える文字列は、=?base64?{UTF-8 を Base64 にした値}?= の形式で符号化します。サーバーは、ヘッダーと本体の値が一致するかを検証し、不一致なら 400 と HeaderMismatch (-32020) で拒否します。必要な Mcp-Param-* が欠けていたり、本体と違っていたりして HeaderMismatch を受けたら、クライアントは tools/list を呼んで inputSchema の変更を確認し、正しいヘッダーで再試行します。x-mcp-header が仕様の制約を満たさないツールの定義 (空の名前、number 型の引数、配列や $ref を経由する場所への付与など) は、そのツールを tools/list の結果から除外し、警告をログに出します。1 つの壊れた定義で、他のツールまで使えなくならないようにするためです。stdio など、HTTP 以外では、x-mcp-header を無視できます。

リクエスト ID: ID は、文字列か整数で、null は使えません。応答を受け取っていない別のリクエストと同じ ID を使ってはなりません。MRTR の再試行や、障害の後の再送では、新しい ID を使います。

server/discover の使いどころ​

server/discover は、サーバーの対応バージョン (supportedVersions)、機能 (capabilities)、識別情報 (serverInfo)、instructions を返すリクエストで、サーバーは必ず実装し、クライアントが呼ぶのは任意です。結果には ttlMs と cacheScope が付き、キャッシュできます。次の 2 つの場面で使います。

  • 事前のバージョン選択と、サーバー情報の表示: 他のリクエストを送る前に、対応バージョンを知りたいとき。サーバーの一覧画面のために、tools/list、prompts/list、resources/list を別々に呼ばず、1 回で機能を把握できる。ただし、必須ではない。クライアントは、任意のリクエストをそのまま送り、サーバーが非対応のバージョンなら返す UnsupportedProtocolVersion (-32022) の data.supported から、共通のバージョンを選んで再試行してもよい
  • stdio での後方互換プローブ: stdio には、HTTP のステータスコードのような、フォールバックの手掛かりがない。旧世代 (initialize を使う 2025-11-25 以前) のサーバーとも通信するクライアントは、最初に server/discover を送る。DiscoverResult か、認識できる新世代のエラーが返れば新世代のサーバーで、それ以外のエラーや、タイムアウトなら、旧世代のサーバーとして initialize にフォールバックする

HTTP では、プローブの代わりに、新世代のリクエストを先に送ります。400 Bad Request を受けたら、本体を確認します。認識できる新世代の JSON-RPC エラー (バージョン不一致、必須機能の欠落、ヘッダーの不一致) なら、新世代のサーバーなので、フォールバックせずに修正して再試行します。本体が空か、認識できないなら、initialize にフォールバックします。サーバーの世代は、リクエストごとではなくサーバー (stdio ではプロセス、HTTP ではオリジン) の性質なので、結果は、サーバーの存続期間 (またはオリジン) にわたってキャッシュし、前提が崩れたときだけ再プローブします。なお、旧世代にしか対応しないクライアントは、新世代のサーバーには接続できません。

認可フローの実装​

HTTP のサーバーに接続するクライアントは、認可の仕様に従います。詳細と実装例は MCPの認可 (企業向けの拡張は エンタープライズ認可) で扱うので、ここでは、クライアントが実装すべき項目だけを挙げます。

  • 未認証のリクエストが 401 と WWW-Authenticate (resource_metadata) で拒否されたら、Protected Resource Metadata を取得し、認可サーバーのメタデータを探索する。WWW-Authenticate と Well-Known の両方に対応する
  • クライアントの登録は、Client ID Metadata Documents (CIMD) を優先する
  • 認可コードフローは PKCE を必ず使い、resource パラメータで、トークンを使う MCP サーバーを指定する
  • 認可レスポンスの iss を検証し、Mix-up 攻撃を防ぐ
  • 403 の insufficient_scope を受けたら、スコープを広げて再認可する (ステップアップ)
  • トークンは、発行元の認可サーバーごとに保管し、他のサーバーへ転送しない (トークンパススルーの禁止)

stdio のサーバーには、この仕様を適用せず、認証情報は環境から取得します。

ツールをモデルへ提示する方法​

tools/list で取得したツールの定義を、モデルのコンテキストに入れるかどうかは、ホストが決めます。素朴な実装は、接続したすべてのサーバーの全ツールを、会話の最初にモデルへ渡します。ツールが数個ならこれで十分ですが、数十のサーバーと数百のツールになると、定義だけでコンテキストの大半を使います。

公式の Client Best Practices は、段階的探索を推奨しています。ホストは tools/list を通常どおり呼びつつ、定義をモデルへ入れるのを遅らせ、軽量な search_tools のメタツールをモデルに渡します。モデルが探索し、必要になった定義だけを読み込みます。切り替えの目安は、ツール定義がコンテキストウィンドウに占める割合で、1〜5% 程度を閾値にします。探索の戦略は、キーワード (BM25 や正規表現)、埋め込み、サブエージェント、およびその組み合わせから選びます。モデル提供元が組み込みのツール検索を提供している場合は、それを使う方が簡単です。段階的探索、サーバーの動的接続、プログラム的ツール呼び出し (Code Mode) の詳細は、コンテキスト肥大問題と対策 を参照してください。

実装の際に押さえる点は次のとおりです。

  • 定義はホスト側でキャッシュ (メモ化) する。モデルのコンテキストにあるかどうかとは別に、再注入のたびに tools/list を呼ばなくて済む
  • 検索のカタログは、notifications/tools/list_changed を受けたら再索引する
  • ツールを、元のサーバー単位でまとめて提示する
  • ツール名は、1 つのサーバーの中でだけ一意。複数のサーバーのツールを集約するクライアントでは、search のような名前が衝突し得る。サーバーの識別子を接頭辞にして区別する。serverInfo の name は、サーバー間で一意とは限らず、識別に使ってはならない
  • ツール配列を途中で変えると、プロンプトキャッシュが切れる。新しく見つかった定義はキャッシュの切れ目の後ろに足すか、すべての呼び出しを call_tool({name, args}) のようなメタツールにまとめて配列を固定する

ユーザー同意の UI​

ツールは、モデルが自動で発見して呼ぶ (model-controlled) ように設計されていますが、仕様は、信頼性と安全性のため、ツールの呼び出しを拒否できる人間が常に介在すべきだとしています (SHOULD)。ホストは、次を実装します。

  • どのツールがモデルに公開されているかを、UI で明確にする
  • ツールが呼ばれるときに、明確な視覚表示を出す
  • 操作の確認プロンプトを出す。確認は、サーバーを呼ぶ前に、実際に送信される引数の全体を見せて行う。引数の一部しか見せない確認は、データ流出を見逃す
  • ツールのアノテーション (readOnlyHint など) は、信頼できるサーバーから得たもの以外は信用しない (MUST)。確認を省く根拠にしない
  • 確認の数を絞る。読み取り専用で外部へ送信しない操作は、ツール単位の包括的な承認にし、破壊的、課金、外部への送信は、毎回確認する。確認が多すぎると、利用者が読まずに承認するようになる (承認疲れ)

詳しい設計は AIエージェント特有の脅威 の Human-in-the-loop の節を参照してください。

ローカルサーバーの追加: ワンクリックでローカルのサーバーを導入できるクライアントは、コマンドを実行する前に、同意の仕組みを実装しなければなりません (SEP-1024)。実行するコマンドを引数も含めて切り詰めずに表示し、利用者のシステムでコードを実行する、危険になり得る操作だと明示し、明示的な承認を求め、セットアップを取り消せるようにします。詳しくは ローカルMCPサーバーの安全な運用 を参照してください。

Elicitation への応答: サーバーが利用者の入力を必要とすると、tools/call などへの応答が InputRequiredResult (resultType: "input_required") になり、inputRequests に 1 つ以上の elicitation/create が入ります。クライアントは、利用者から入力を集め、元のリクエストを、集めた inputResponses と、サーバーが返した requestState (そのまま) を付けて、新しい ID で再送します (MRTR)。

  • 要求を表示するときは、どのサーバーが、なぜ、何のために尋ねているかを示す
  • 利用者は、入力する、拒否する (理由は任意)、操作全体をキャンセルする、のいずれかを選べる。クライアントは、サーバーに返す前に、スキーマに沿って応答を検証する
  • URL モードでは、完全な URL を見せ、明示的な同意を得てから開く。URL を自動で取得してはならず、クライアントは、同意したかどうかだけを知る
  • form モードで、パスワード、API キー、アクセストークン、支払い情報のような機密を求める要求は、不審なものとして警告する。そうした入力は、URL モードで画面外に出す
  • inputRequests や requestState を伴う再試行の結果は、キャッシュしてはならない

キャッシュと更新​

server/discover、tools/list、prompts/list、resources/list、resources/templates/list、resources/read の結果 (resultType: "complete") には、キャッシュのヒント ttlMs と cacheScope が付きます。クライアントは、次のように扱います。

  • 鮮度: ttlMs は、結果を新しいと見なしてよいミリ秒数のヒントで、HTTP の Cache-Control: max-age に近い。受信した時刻から ttlMs 以内なら新しく、0 なら、直ちに古いものとして扱う。ttlMs がなければ 0、負の値も 0 と見なす。保証ではなく、TTL の間にデータが変わることもある
  • TTL をポーリングの間隔にしない: 必要になったときに、鮮度を確認し、古ければ再取得する。バックグラウンドでポーリングするなら、ジッターとバックオフを付ける
  • list_changed による無効化: 関連する通知を受けたら、TTL の期限前でも、キャッシュを直ちに古いものとして扱い、再取得する。通知は最善努力で欠落し得るため、TTL による再取得を併用する
  • cacheScope: public は、利用者に固有のデータを含まず、共有のゲートウェイやプロキシを含めて誰でも保存、再利用してよい。private は、同じ認可コンテキスト (同じアクセストークン) の間でだけ再利用でき、認可コンテキストをまたいで共有してはならない。public でも、認可の判断を cacheScope だけに頼ってはならない
  • キャッシュのキー: リクエストのメソッドと、結果に影響するパラメーター (resources/read の uri、ページネーションの cursor) の組。メソッドかパラメーターが違う結果を返してはならない
  • ページネーション: 各ページが、独立してキャッシュされ、それぞれの ttlMs を持つ。ページ間の一貫性は保証されないため、一貫したスナップショットが必要なら、cursor なしの先頭から取得し直す。cursor が無効になったら、キャッシュしたページをすべて捨てて、先頭から取得し直す
  • 障害時: 再取得に失敗したとき (ネットワークの障害、サーバーの停止) は、古いキャッシュを使い続けてもよい (MAY)

購読​

サーバーからの変更通知は、subscriptions/listen で受け取ります。これは、長く開いたままの SSE ストリームを応答とするリクエストで、以前の resources/subscribe と、HTTP の GET のエンドポイントを置き換えました。

  • リクエストの notifications フィルターで、受け取る種類を選ぶ。toolsListChanged、promptsListChanged、resourcesListChanged (真偽値) と、resourceSubscriptions (通知を受けたいリソースの URI の配列)。省略した種類は、購読しない。サーバーは、要求されていない種類を送ってはならない
  • サーバーは、最初のメッセージとして notifications/subscriptions/acknowledged を返し、実際に受け入れた部分集合を示す。クライアントは、要求と照合し、サーバーが対応しない種類を許容する
  • 以降の通知には、_meta の io.modelcontextprotocol/subscriptionId が付く。値は、subscriptions/listen のリクエストの JSON-RPC の ID。1 つのクライアントが複数の購読を同時に持てる。stdio では、すべてのメッセージが 1 本のチャネルを共有するため、このフィールドで、通知を購読と対応づける
  • 購読は、クライアントがストリームを閉じる (HTTP)、または notifications/cancelled を送る (stdio) と終わる。サーバーが自分から終了する場合は、成功の応答を返してから閉じる。この応答なしに閉じられたら、予期しない切断で、再接続の契機になる。stdio で接続を張り直したら、購読を再送する。サーバーは、再接続をまたいで購読の状態を持たない
  • Last-Event-ID による再開は、サポートされない。切断の間に変更があったかもしれないので、再接続のたびに、関連する一覧を再取得する

購読は、接続を 1 本占有するので、必要な種類だけを選びます。通知がなくても、TTL による再取得で、鮮度は保てます。キャッシュと購読は補完関係にあります。

障害対応​

タイムアウトとキャンセル: すべての送信リクエストにタイムアウトを設け、応答が来なければキャンセルします。HTTP では、そのリクエストの応答ストリームを閉じることがキャンセルで、stdio では、notifications/cancelled を送ります。進捗通知を受けたらタイムアウトの時計をリセットしてもよいですが、進捗とは別に、最大のタイムアウトは必ず適用します。キャンセルの後に届いた応答は無視します。

再試行: ストリームが切れても再開はできないので、クライアントは、同じ内容を新しいリクエスト ID で、最初から送り直します。サーバーから見ると別のリクエストなので、副作用のあるツールの自動再試行は、二重の実行を招き得ます。サーバーが冪等に設計されている保証はないため (設計は MCPサーバーの実装 を参照)、読み取りだけの呼び出しは自動で再試行し、副作用のある呼び出しは、利用者に確認するか、再試行しない方針にします。再試行には、回数の上限、指数バックオフとジッターを設けます。

エラーの扱い:

エラー対応
ツール実行エラー (isError: true)成功のレスポンスに入っている。モデルに返し、自己修正を促す
-32602必須の _meta の欠落や、不正なパラメーター。リクエストの組み立てを修正する
-32021 MissingRequiredClientCapability必要な機能を宣言して、再試行する
-32022 UnsupportedProtocolVersiondata.supported から、共通のバージョンを選んで再試行する。なければ、利用者にエラーを示す
-32020 HeaderMismatchtools/list で定義を再取得し、正しいヘッダーで再試行する

プロトコルエラーは、モデルに見せてもよい (MAY) ですが、自己修正にはつながりにくいものです。ツール実行エラーは、モデルに返すべきです (SHOULD)。

SSRF と URL 検証​

クライアントは、MCP サーバーが返す URL を取得します。WWW-Authenticate の resource_metadata、Protected Resource Metadata の authorization_servers、認可サーバーのメタデータの token_endpoint などです。悪意あるサーバーは、これらに内部の宛先を仕込めます (内部の IP、クラウドのメタデータ、localhost のサービス、DNS rebinding、リダイレクトの連鎖)。サーバー上にデプロイされるクライアントは、次を実装します。

  • OAuth 関連の URL に HTTPS を要求する。http:// は開発時のループバックのみ許可する
  • プライベート IP、ループバック、リンクローカルを遮断する。IP の検証は自作の文字列処理ではなく、標準のライブラリで行う
  • リダイレクトは自動で追従せず、各ホップに同じ検証を行う
  • 解決した IP を固定して接続する (DNS の TOCTOU への対策)。egress プロキシで、内部宛てを遮断する

詳細と実装例は MCPセキュリティベストプラクティス の SSRF の節にあります。そのほかにも、プロトコルには、クライアントが外部のリソースを取得する場面があります。

  • $ref: JSON Schema の $ref が、ネットワークの URI を指していても、自動で参照を解決してはならない (MUST NOT)。既定で無効にし、有効にするなら、ホストの許可リストか、ループバック、リンクローカル、プライベートネットワークの拒否、タイムアウトとサイズ制限、ログを備える。外部の $ref の解決に失敗したスキーマは、緩く扱わずに拒否する
  • アイコン: icons の URI は、HTTPS か data: のみを許可し、javascript:、file:、ftp: などは拒否する。資格情報なしで取得し、別のオリジンへのリダイレクトを拒否し、サーバーと同じオリジンを確認する。サイズやフレーム数に上限を設ける。SVG は、実行可能な内容を含み得るため、サニタイズするか、許可しない。MIME タイプは、先頭のバイト列で確かめる

複数サーバーの管理​

ホストは、複数のサーバーを、設定の一覧から静的に接続するのではなく、必要に応じて動的に接続、切断できます。レジストリにサーバーと概要を持たせておき、モデルが必要と判断したときに接続し、不要になれば切断してコンテキストを解放します。

汎用のエージェントのように、利用者の意図が事前に分からない場合に有効です。最小限の常時接続のサーバーから始め、必要に応じて足します。Agent Skills と組み合わせると、スキルのファイルに必要な MCP サーバーを宣言し、スキルが呼ばれたときだけ接続できます。切断は、プロンプトキャッシュを無効にするため、毎ターンではなく、会話の区切りで行います。

サーバーごとの信頼境界は、ホストが保ちます。

  • 設定した、すべてのサーバーは、同じモデルのコンテキストを共有する。悪意あるサーバーは、他のサーバーのツールの使われ方に影響を与え得るので、サーバーを足すたびに、全体の攻撃面が広がる
  • あるサーバーのツール結果は、別のサーバーにとって信頼できない入力。サーバー A の出力を、サーバー B のツールの引数に渡す前に、利用者が確認できるようにする。出力の切り詰めだけでは、データ流出を防げない
  • 認可トークンは、発行元の認可サーバーとリソースごとに管理し、混ぜない
  • Code Mode で、サンドボックスから複数のサーバーを呼ぶ場合も、ブローカーとしてのホストが、サンドボックスの呼び出しごとに同じ確認の方針を適用する。スクリプトを承認しても、実行時の呼び出しすべてを承認したことにならない。サンドボックスにはネットワークの権限を与えず、資格情報もホストが持つ

2. ホストとクライアントの責務​

責務担当補足
ユーザーへの同意取得ホストツール実行、データ共有、ローカルサーバーの起動
LLM へのツール提示ホスト全件ロードか、検索ベースの段階的ロードか
プロトコル通信クライアントリクエストの組み立て、結果の解釈、再試行
認可トークンの保管クライアント / ホスト発行元の認可サーバーごとに資格情報を管理する
サーバー間のデータ分離ホストあるサーバーの出力を別のサーバーへ渡す際の確認

3. 開発者がつまずきやすいポイント​

2025 年版のハンドシェイクを前提にする​

initialize を先に送り、接続状態に依存する実装は、2026-07-28 のサーバーでは動きません。すべてのリクエストに _meta を付け、HTTP のヘッダーも付けます。旧世代のサーバーにも接続するなら、世代を検出してキャッシュします。

_meta や HTTP のヘッダーが欠ける​

protocolVersion、clientCapabilities が欠けると -32602、ヘッダーが本体と食い違うと HeaderMismatch で 400 になります。ヘッダーは、本体から機械的に生成し、Mcp-Name と Mcp-Param-* の符号化を忘れないようにします。

Accept に片方しか書かない​

サーバーは、リクエストごとに JSON か SSE を選びます。application/json と text/event-stream の両方を書き、両方を処理できるようにします。

TTL をポーリングの間隔として使う​

ttlMs は鮮度のヒントです。バックグラウンドで定期的に再取得すると、サーバーに無駄な負荷を掛けます。必要になったときに確認します。

list_changed を受けても、キャッシュを古いと見なさない​

通知は、TTL の期限前でも、キャッシュを無効にします。逆に、通知だけに頼ると、欠落したときに古いままになるので、TTL も併用します。

cacheScope: private の結果を、認可コンテキストをまたいで使い回す​

別のアクセストークンで取得した結果を、共有のキャッシュから返すと、他の利用者のデータが見えます。キャッシュのキーに、認可コンテキストを含めます。

副作用のあるツールを、自動で再試行する​

ストリームが切れたとき、サーバーはすでに処理を終えているかもしれません。再試行は新しいリクエストなので、二重の実行になり得ます。読み取り専用の呼び出しに限って自動で再試行します。

承認の画面で引数の一部しか見せない​

悪意あるツールは、見えない引数に、データを載せられます。実際に送信される引数の全体を見せます。

アノテーションを根拠に、確認を省く​

readOnlyHint は、信頼できるサーバーからのものでなければ、信用できません。確認の要否は、ホストが信頼するサーバーの一覧と、利用者の設定で決めます。

OAuth の URL を、検証せずに取得する​

resource_metadata や token_endpoint は、悪意あるサーバーが返せる URL です。HTTPS の強制、プライベート IP の遮断、リダイレクトの検証、DNS の固定を行います。

全サーバーの全ツールを、最初からモデルに渡す​

定義がコンテキストの大半を占め、ツールの選択精度も下がります。閾値を決め、超えたら段階的探索に切り替えます。

ツール名の衝突を考慮しない​

別のサーバーの search が衝突し、意図しないサーバーを呼びます。サーバーの識別子を接頭辞にします。

4. コード例​

1 回のツール呼び出し (Streamable HTTP)​

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: execute_sql
Mcp-Param-Region: us-west1
Authorization: Bearer <アクセストークン>

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "execute_sql",
"arguments": { "region": "us-west1", "query": "SELECT 1" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleHost", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {},
"progressToken": "p-1"
}
}
}

この例は、サーバーが execute_sql の region に x-mcp-header: "Region" を付けている場合です。Authorization は、MCPの認可 のとおり、リクエストごとに送ります。

Elicitation を含む再試行 (MRTR)​

サーバーが入力を求める応答です。

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"confirm": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "注文を確定します。合計は 3,000 円です。",
"requestedSchema": {
"type": "object",
"properties": { "confirmed": { "type": "boolean" } },
"required": ["confirmed"]
}
}
}
},
"requestState": "eyJzdGVwIjoxfQ..."
}
}

クライアントは、利用者の入力を集め、元のリクエストを新しい ID で再送します。requestState は、サーバーが返した値をそのまま返します。

{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "place_order",
"arguments": { "basket_id": "bsk_a1b2c3", "idempotency_key": "k-001" },
"inputResponses": {
"confirm": { "action": "accept", "content": { "confirmed": true } }
},
"requestState": "eyJzdGVwIjoxfQ...",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
}
}
}

clientCapabilities に、Elicitation を書く形式は、SDK のドキュメントで確認してください。ここでは、宣言が必要であることを示す例です。

購読の開始​

{
"jsonrpc": "2.0",
"id": 7,
"method": "subscriptions/listen",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
},
"notifications": {
"toolsListChanged": true,
"resourceSubscriptions": ["file:///project/config.json"]
}
}
}

応答のストリームの最初に notifications/subscriptions/acknowledged が届き、以降の通知の _meta に io.modelcontextprotocol/subscriptionId: 7 が付きます。

一覧のキャッシュ (TypeScript、SDK に依存しない部分)​

ttlMs、cacheScope、list_changed による無効化を扱う、ホスト側のキャッシュの例です。fetchPage は、SDK や HTTP で tools/list を呼ぶ関数を渡します。

interface ListResult<T> {
items: T[];
nextCursor?: string;
ttlMs?: number;
cacheScope?: "public" | "private";
}

interface Entry<T> {
value: ListResult<T>;
receivedAt: number;
authContext: string; // private のときに、キーへ含める (例: アクセストークンの識別子)
}

class ListCache<T> {
private entries = new Map<string, Entry<T>>();

constructor(
private fetchPage: (cursor?: string) => Promise<ListResult<T>>,
) {}

private key(method: string, cursor: string | undefined, auth: string, scope: string) {
// public は認可コンテキストを共有できる。private は共有しない。
const a = scope === "private" ? auth : "*";
return `${method}|${cursor ?? ""}|${a}`;
}

async get(method: string, auth: string, cursor?: string): Promise<ListResult<T>> {
for (const scope of ["public", "private"] as const) {
const hit = this.entries.get(this.key(method, cursor, auth, scope));
if (hit && Date.now() < hit.receivedAt + Math.max(0, hit.value.ttlMs ?? 0)) {
return hit.value; // 新しい
}
}
try {
const value = await this.fetchPage(cursor);
const scope = value.cacheScope ?? "private"; // 不明なら、狭い方に倒す
this.entries.set(this.key(method, cursor, auth, scope), {
value,
receivedAt: Date.now(),
authContext: auth,
});
return value;
} catch (e) {
// 再取得に失敗したときは、古いキャッシュを使ってもよい。
for (const scope of ["public", "private"] as const) {
const stale = this.entries.get(this.key(method, cursor, auth, scope));
if (stale) return stale.value;
}
throw e;
}
}

// notifications/tools/list_changed を受けたら呼ぶ。TTL の期限前でも古いものとして扱う。
invalidate(method: string) {
for (const k of [...this.entries.keys()]) {
if (k.startsWith(`${method}|`)) this.entries.delete(k);
}
}
}

公式 SDK でサーバーへ接続する (TypeScript)​

公式のクイックスタートのクライアントの骨格です (stdio で接続し、ツールを取得して呼び出します)。

import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";

const mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
await mcp.connect(
new StdioClientTransport({ command: process.execPath, args: ["build/index.js"] }),
);

const { tools } = await mcp.listTools();
console.log(tools.map((t) => t.name));

// ホストは、実行の前に、引数の全体を利用者へ見せて承認を得る。
const result = await mcp.callTool({
name: "search_orders",
arguments: { status: "pending" },
});
console.log(result.content);

await mcp.close();

5. まとめ​

  • ホストは、利用者への同意、モデルへのツールの提示、サーバー間のデータの流れを担い、クライアントは、プロトコルの正しさを担う
  • すべてのリクエストに、_meta (バージョンと機能) と、HTTP では Mcp-Method、Mcp-Name、Mcp-Param-* などのヘッダーを付け、本体と一致させる
  • server/discover は任意。事前のバージョン選択と、stdio の後方互換プローブに使い、サーバーの世代の判定は、キャッシュする
  • 認可は、401 の処理、メタデータ探索、PKCE、resource、iss の検証、ステップアップを実装する。詳細は別のドキュメントを参照する
  • ツールが多いときは、閾値を決めて、段階的探索に切り替える。ツール名は接頭辞で衝突を避ける
  • 同意の UI は、引数の全体を見せ、アノテーションを信用せず、ローカルサーバーの起動コマンドも見せる。Elicitation は MRTR で、新しい ID で再試行する
  • ttlMs と cacheScope に従い、list_changed で無効化する。TTL をポーリングの間隔にせず、private を認可コンテキストをまたいで共有しない
  • 購読は subscriptions/listen で、必要な種類だけを選ぶ。再接続したら、再取得する
  • 障害は、タイムアウトでキャンセルし、新しい ID で再試行する。副作用のある呼び出しは、自動で再試行しない
  • OAuth の URL、$ref、アイコンの取得は、SSRF への対策として検証する
  • サーバーごとに信頼境界を持ち、あるサーバーの出力を、別のサーバーへ渡す前に確認する

関連ドキュメント​

参考資料​