MCP はどう動くのか?プロトコルをステップごとに解説(2026)
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 つの標準トランスポート を定めています。
| stdio | Streamable HTTP | |
|---|---|---|
| サーバーの実行場所 | クライアントが起動するサブプロセス | どこでも可。https://example.com/mcp のような 1 つの URL の背後 |
| フレーミング | stdin/stdout 上の改行区切り JSON-RPC | 1 メッセージにつき 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) を使います。
- クライアントが
tools/call(id 1)を送る。 - サーバーが
resultType: "input_required"で応答する。応答にはinputRequests(例:elicitation/createのフォーム)と、中身の見えないrequestStateが入る。 - クライアントがユーザーにフォームを見せる。
- クライアントが新しいリクエスト(id 2)として元の呼び出しを再試行する。
params.inputResponses(同じキーに入れたユーザーの回答)を渡し、params.requestStateをそのまま返す。 - サーバーは再試行の内容から状態を復元し、
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 を付けて元のリクエストを再試行します。