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

MCP はどう動くのか?プロトコルをステップごとに解説(2026)

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

MCP はどう動くのか?2026-07-28 版プロトコルを順に解説:discover、tools/list、tools/call、elicitation、通知、トランスポート、認証まで実際の JSON 付きで。

MCP の仕組みはこうです。AI アプリケーション(ホスト)が接続するサーバーごとにクライアントを 1 つ動かし、そのクライアントがサーバーと JSON-RPC 2.0 メッセージをやり取りします。サーバーのツールを一覧し、モデルが 1 つを選び、クライアントが tools/call を送って結果をモデルに返します。現行の仕様リビジョン 2026-07-28 から MCP はステートレスになりました。ハンドシェイクはなく、すべてのリクエストが自分のプロトコルバージョンとクライアント機能を _meta に持つので、どのサーバーインスタンスでもどのリクエストにも応答できます。

このページでは、1 回のツール呼び出しをワイヤ上でメッセージ単位に追いかけます。まず全体像を知りたい方は MCP とは? を、サーバー側については MCP サーバーとは? をご覧ください。

登場人物:ホスト、クライアント、サーバー

  • ホスト:ユーザーが直接触るアプリ。Claude Code、Cursor、VS Code、ChatGPT など。会話、モデル、同意プロンプトを管理します。
  • クライアント:ホストが 1 つのサーバーのために作るコネクタ。アーキテクチャ仕様 によれば、各クライアントが話すサーバーはちょうど 1 つです。サーバーを 5 つ接続したホストはクライアントを 5 つ動かします。(詳しくは MCP クライアントガイド へ。)
  • サーバー:ツール、リソース、プロンプトを公開するプログラム。Filesystem サーバー のようなローカルプロセスのことも、ホスト型の Sentry MCP サーバー のようなリモートサービスのこともあります。

仕様には重要な設計ルールがあります。サーバーは会話全体を見られず、他のサーバーを「のぞき見る」こともできません。各サーバーが何を受け取るかはホストが決めます。

ステップ 0:トランスポートの確立

MCP メッセージが流れる前に、クライアントには通信路が必要です。仕様は 2 つの標準トランスポート を定めています。

stdioStreamable HTTP
サーバーの実行場所クライアントが起動するサブプロセスどこでも可。https://example.com/mcp のような 1 つの URL の背後
フレーミングstdin/stdout 上の改行区切り JSON-RPC1 メッセージにつき 1 回の HTTP POST。応答は JSON か、そのリクエスト専用の SSE ストリーム
認証情報環境変数からOAuth 2.1 Bearer トークン(ステップ 7 参照)
リクエストのキャンセルnotifications/cancelled を送るそのリクエストの応答ストリームを閉じる

旧来の HTTP+SSE トランスポート(2024-11-05)は正式に非推奨になりました。移行方法は SSE と Streamable HTTP で解説しています。

ステップ 1(任意):server/discover

クライアントは最初に、サーバーが何をサポートしているかを尋ねられます。サーバーは server/discover の実装が必須ですが、クライアントが呼ぶかどうかは任意です。次のやり取りは公式の アーキテクチャガイド からの引用です。

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "server/discover",
  "params": {
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {
        "name": "example-client",
        "version": "1.0.0"
      },
      "io.modelcontextprotocol/clientCapabilities": { "elicitation": {} }
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": { "tools": { "listChanged": true }, "resources": {} },
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "example-server",
        "version": "1.0.0"
      }
    },
    "ttlMs": 3600000,
    "cacheScope": "public"
  }
}

この _meta ブロックこそがステートレス設計の核心です。すべてのリクエストに次が含まれます。

  • io.modelcontextprotocol/protocolVersion(必須):このリクエストが使うリビジョン。
  • io.modelcontextprotocol/clientCapabilities(必須):このリクエストでクライアントができること(例:elicitation)。
  • io.modelcontextprotocol/clientInfo(推奨):クライアントの名前とバージョン。

サーバーが要求されたバージョンを話せない場合は、supported リスト付きの UnsupportedProtocolVersionError(コード -32022)を返し、クライアントはそのリストにあるバージョンで再試行します。必須フィールドが欠けたリクエストには -32602(Invalid params)が返ります。

ステップ 2:ツールの一覧取得

次にクライアントはサーバーのツールカタログを要求します。

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": { "_meta": { "...": "same three fields as above" } }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "complete",
    "tools": [
      {
        "name": "weather_current",
        "title": "Weather Information",
        "description": "Get current weather information for any location worldwide",
        "inputSchema": {
          "type": "object",
          "properties": {
            "location": {
              "type": "string",
              "description": "City name, address, or coordinates"
            },
            "units": {
              "type": "string",
              "enum": ["metric", "imperial", "kelvin"],
              "default": "metric"
            }
          },
          "required": ["location"]
        }
      }
    ],
    "ttlMs": 300000,
    "cacheScope": "public"
  }
}

2026-07-28 で新しくなった点が 3 つあります。

  • 一覧結果には ttlMs と cacheScope が必須になりました。この例では、クライアントは一覧を 5 分間キャッシュできます。
  • 一覧は接続ごとに変えてはいけません。呼び出し元の認可によって変えることは許されます(例:OAuth スコープでツールを絞り込む)。
  • サーバーはツールを決まった順序で返すべきです。そうするとモデルのプロンプトキャッシュが効きやすくなります。

大きな一覧は cursor でページングされます。多数のサーバーを束ねるホストはツールを遅延読み込みすることが多く、その手法は MCP ツールディスカバリー で解説しています。

ステップ 3:モデルがツールを選ぶ

このステップは MCP の一部ではありません。ホストは各ツールの name、description、inputSchema を自社モデルの function calling 形式に変換し、ユーザーのメッセージと一緒にモデルへ送ります。モデルがツール呼び出し(weather_current、引数 location: "San Francisco")を返したら、ホストは何が実行されるのかをユーザーに見せるべきです。仕様は、ツール呼び出しを拒否できる人間が常にループ内にいるべきだと定めています。

ステップ 4:tools/call

クライアントが呼び出しを送ります。Streamable HTTP では、ゲートウェイが JSON を解析せずにルーティングできるよう、トランスポートが主要なフィールドをヘッダーにも写します。Mcp-Method はすべてのリクエストで必須、Mcp-Name は tools/call、resources/read、prompts/get で必須です。以下は Streamable HTTP 仕様 の例です。

POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": { "location": "Seattle, WA" },
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

MCP-Protocol-Version ヘッダーと本文の _meta が一致しない場合、サーバーは 400 と HeaderMismatch エラー(-32020)で拒否します。成功時の応答は次のとおりです。

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "Current weather in Seattle, WA: 54°F, light rain..."
      }
    ]
  }
}

いまはすべての結果に resultType が付きます。"complete" はこれが最終回答であることを意味します。アプリケーションレベルで失敗したツールも結果を返し、isError: true が付くだけなので、モデルはエラーを読んで別の方法を試せます。JSON-RPC エラーとして返るのは、存在しないツールのようなプロトコルレベルの失敗だけです。ホストは content(ツールが outputSchema を宣言していれば structuredContent も)を会話に加え、モデルが回答を書きます。

ステップ 5:サーバーが追加の入力を必要とするとき(MRTR)

ツールがユーザーなしでは完了できないことがあります。購入の確認やアカウントの選択などです。2026-07-28 より前は、サーバーが自らクライアントへ JSON-RPC リクエストを送っていました。いまはサーバーがリクエストを発行することはなく、代わりに Multi Round-Trip Requests(MRTR) を使います。

  1. クライアントが tools/call(id 1)を送る。
  2. サーバーが resultType: "input_required" で応答する。応答には inputRequests(例:elicitation/create のフォーム)と、中身の見えない requestState が入る。
  3. クライアントがユーザーにフォームを見せる。
  4. クライアントが新しいリクエスト(id 2)として元の呼び出しを再試行する。params.inputResponses(同じキーに入れたユーザーの回答)を渡し、params.requestState をそのまま返す。
  5. サーバーは再試行の内容から状態を復元し、resultType: "complete" を返す。
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "input_required",
    "inputRequests": {
      "github_login": {
        "method": "elicitation/create",
        "params": {
          "mode": "form",
          "message": "Please provide your GitHub username",
          "requestedSchema": {
            "type": "object",
            "properties": { "name": { "type": "string" } },
            "required": ["name"]
          }
        }
      }
    },
    "requestState": "AEAD-protected blob"
  }
}

2 回の呼び出しの間にサーバーは何も保持しないので、ロードバランサーの背後のどのインスタンスが再試行を受けても問題ありません。仕様はサーバーに対し、requestState を攻撃者が操作しうる入力として扱い、その完全性を保護する(HMAC や AEAD)よう求めています。Elicitation には 2 つのモードがあります。通常のデータには form、認証情報のような機密情報には URL を使います。機密情報は決してクライアントを経由させてはいけません。

残り 2 つのクライアント機能である sampling と roots も MRTR で運べますが、どちらも(プロトコルのロギングとともに)2026-07-28 で非推奨 になりました。新しいサーバーは LLM API を直接呼び、パスはツールの引数で受け取るべきです。

ステップ 6:通知

MCP の通知は 2 種類あり、通る経路が違います。

  • **リクエスト単位の通知。**リクエストの _meta に progressToken を入れると、サーバーは最終結果の前に、そのリクエスト自身の応答ストリームで notifications/progress(progress、total、message)を流せます。
  • **変更通知。**ツール、プロンプト、リソースの変更を知るには、クライアントが長時間持続する subscriptions/listen リクエストを開き、受け取る種類を選びます。これは resources/subscribe と旧来の HTTP GET ストリームの両方を置き換えました。
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "subscriptions/listen",
  "params": {
    "_meta": { "...": "standard fields" },
    "notifications": {
      "toolsListChanged": true,
      "resourceSubscriptions": ["file:///project/config.json"]
    }
  }
}

サーバーはまず、送ることに同意した内容を示す notifications/subscriptions/acknowledged を送ります。その後 notifications/tools/list_changed や notifications/resources/updated が届き、どれにも io.modelcontextprotocol/subscriptionId のタグが付いています。list_changed を受け取ったクライアントは tools/list を呼び直します。

リクエストをキャンセルするには、stdio のクライアントはリクエスト ID を指定した notifications/cancelled を送り、Streamable HTTP のクライアントはそのリクエストのストリームを閉じます。実装はすべてのリクエストにタイムアウトを設けるべきです。

ステップ 7:認可

認可は JSON-RPC メッセージの外側、トランスポート層にあります。認可仕様 の定めは次のとおりです。

  • stdio サーバーは OAuth フローを使うべきではなく、環境から認証情報を読みます(通常はクライアント設定の env ブロック)。
  • HTTP サーバーは OAuth 2.1 のリソースサーバーとして振る舞います。未認証のリクエストには 401 が返り、その WWW-Authenticate ヘッダーがサーバーの保護リソースメタデータを指します。クライアントはそこから認可サーバーを見つけ、登録し(現在は Client ID Metadata Documents が推奨で、Dynamic Client Registration は非推奨)、PKCE を実行して、以降のすべてのリクエストで Authorization: Bearer を送ります。

動く例を含む全体の流れは MCP 認証ガイド にあります。

initialize はどうなったのか?

ネット上の解説の多くは、今も動いている多くのサーバーと同じく、2025-11-25 以前のレガシーフローを説明しています。そのフローでは、クライアントがまず initialize(protocolVersion、capabilities、clientInfo を含む)を送り、サーバーが自分の機能を返し、クライアントが notifications/initialized を送ります。以降、接続はセッションになります(HTTP ではサーバーが Mcp-Session-Id ヘッダーを発行し、後続のリクエストはそれを返す必要がありました)。2026-07-28 は、ハンドシェイク、セッション ID、ping、ストリームの再開機能を削除しました。

2 つの世代は バージョニング仕様 のフォールバック規則で相互運用します。stdio では、両世代に対応したクライアントが server/discover で探りを入れ、新世代の応答が返らなければ initialize にフォールバックします。HTTP では新世代のリクエストを送り、新世代のエラー本文を含まない 400 が返ればフォールバックします。

SDK がこれを処理してくれますが、デフォルト値には注意してください。TypeScript SDK v2 の new Client(...) は、versionNegotiation: { mode: 'auto' } を渡さない限り、今もレガシーのハンドシェイクを行います。このオプションを渡すと server/discover で探り、必要なら自動でフォールバックします。サーバー側の serveStdio ヘルパーは両世代に応答します(プロトコルバージョンガイド)。

実際に見てみる

JSON を読むのと、動きをライブで見るのとでは大違いです。MCP Inspector を任意のサーバーにつなげば(例:npx @modelcontextprotocol/inspector と GitHub MCP サーバー)、tools/list と tools/call のやり取りをすべて観察できます。自分でサーバーを作るなら MCP サーバーの作り方 から始めましょう。

次のステップ

これで往復の全体像がわかりました。discover、list、call、input-required の再試行、通知、認可です。実際に動かしてみるには、MCP サーバーディレクトリ を眺めて、サーバーを 1 つクライアントに接続してみてください。

よくある質問

MCP の仕組みを簡単に言うと?

AI アプリはサーバーごとに MCP クライアントを 1 つ動かします。クライアントがサーバーのツールを尋ね、モデルが 1 つを選び、クライアントが tools/call リクエストを送って結果をモデルに返します。メッセージはすべて JSON-RPC 2.0 です。

MCP にはまだ initialize ハンドシェイクがありますか?

現行の 2026-07-28 リビジョンにはありません。各リクエストが _meta にプロトコルバージョンとクライアント機能を持ち、クライアントは最初に server/discover を呼ぶこともできます。2025-11-25 以前のサーバーは今も initialize を使うため、SDK は必要に応じてフォールバックします。

MCP はどのトランスポートを使いますか?

標準トランスポートは 2 つです。ローカルサーバーは stdio を使い、サブプロセスの stdin と stdout で改行区切りの JSON-RPC をやり取りします。リモートサーバーは Streamable HTTP を使い、1 メッセージにつき 1 回の POST を送って、JSON かそのリクエスト専用の SSE ストリームで応答を受けます。旧来の HTTP+SSE トランスポートは非推奨です。

MCP サーバーはどうやってユーザーに入力を求めますか?

Multi Round-Trip Requests パターンで届けられる elicitation を使います。サーバーは tools/call に対し、必要な情報を記した input_required の結果を返します。クライアントは回答を集め、inputResponses と変更していない requestState を付けて元のリクエストを再試行します。