MCPアーキテクチャ — Host/Client/Serverの三層構造を実装者向けに読む
MCPのHost・Client・Serverという参加者の役割分担と、データ層・トランスポート層の2層構造を仕様書ベースで解説します。LSPに着想を得た構成と2026-07-28のステートレス化がもたらす影響も扱います。
MCPアーキテクチャの3つの参加者
MCP(Model Context Protocol)はクライアント・サーバー型のアーキテクチャを採用しています。参加者は3種類で、AIアプリケーション自身が「Host」、その内部で接続を受け持つ部品が「Client」、文脈を提供するプログラムが「Server」です。
3つの参加者の役割
MCP Host
Claude CodeやClaude Desktopのような、1つ以上のMCPクライアントを管理・調整するAIアプリケーションです。
MCP Client
1つのMCPサーバーへの接続を維持し、Hostのためにコンテキストを取得するコンポーネントです。
MCP Server
MCPクライアントにコンテキストを提供するプログラムです。実行場所は問いません。
HostはMCPサーバー1つにつき1つのMCPクライアントを生成します。Visual Studio CodeがSentry MCPサーバーとローカルのファイルシステムサーバーの両方に接続する場合を考えます。VS Codeのランタイムは、独立したMCPクライアントオブジェクトを2つ生成します。1つのクライアントは1つのサーバーとの専用接続だけを担当します。
stdioトランスポートを使うローカルサーバー(ファイルシステムサーバーなど)も、Streamable HTTPを使うリモートサーバー(Sentry MCPサーバーなど)も、どちらも「サーバー」です。ローカルサーバーは典型的に単一のクライアントに応答し、リモートサーバーは多数のクライアントに同時に応答します。
参加者の呼び分けでつまずきやすい点
3層構造を初めて読む実装者が混同しやすいのが、「Client」という語の範囲です。日常会話の「MCPクライアントを実装する」は、多くの場合「MCPサーバーに接続するアプリケーション全体」を指します。仕様上のClientは1サーバーにつき1つ生成される内部コンポーネントで、アプリケーション全体を指す語は「Host」です。HostがN個のサーバーに接続していれば、Host内部にはClientインスタンスがN個あります。
「サーバー」も、実行場所(ローカル/リモート)とトランスポート(stdio/Streamable HTTP)を取り違えやすい語です。典型例ではローカルがstdio、リモートがStreamable HTTPですが、これは「典型的に」という対応にとどまります。Streamable HTTPの仕様はローカル実行のとき localhost(127.0.0.1)だけにバインドするよう求めており、ローカルで動くHTTPサーバーも想定内です。SDKのドキュメントで迷ったら、「ローカル/リモート」は実行場所の軸、「stdio/Streamable HTTP」は通信手段の軸、と分けて読むと整理できます。
Claude Codeのコマンドは、どの層の設定か
Claude CodeはHostにあたります。claude mcp add のヘルプ(v2.1.289の出力から主なオプションを抜粋)を層構造に当てはめると、オプションごとの担当が分かれます。
claude mcp add --helpOptions:
--callback-port <port> Fixed port for OAuth callback (for servers
requiring pre-registered redirect URIs)
--client-id <clientId> OAuth client ID for HTTP/SSE servers
-e, --env <env...> Set environment variables (e.g. -e KEY=value)
-H, --header <header...> Set headers for HTTP/SSE servers (e.g. -H
"X-Api-Key: abc123" -H "X-Custom: value")
-s, --scope <scope> Configuration scope (local, user, or project)
(default: "local")
-t, --transport <transport> Transport type (stdio, sse, http). Defaults to
stdio if not specified.--transport が選ぶのはトランスポート層の手段です。ここでの http が仕様のStreamable HTTPにあたり、stdio は何も指定しなかったときの既定になります。sse は、仕様が2026-07-28で標準としている2つのトランスポート(stdioとStreamable HTTP)に含まれない旧方式の指定です。-H は任意のヘッダーを付けられるため、ベアラートークンやAPIキーによるHTTP認証の入口になります。--client-id はHTTP/SSEサーバー向けのOAuthクライアントIDで、仕様がトークン取得に推奨するOAuthの設定にあたります。どちらも、仕様がトランスポート層の責務に含めた「認可」の側の設定です。-e はstdioで起動する子プロセスの環境変数を渡します。
--scope だけは、MCP仕様ではなくClaude Code自身の設定保存先を決めるオプションです。サーバー定義をどこに持つかはHostの実装事項で、プロトコルの外にあります。Hostの設定と層構造を分けて考えると、「接続できない」という症状がHost側の設定か、トランスポートか、データ層のバージョン不一致かを切り分けやすくなります。
MCPアーキテクチャの2層構造 — データ層とトランスポート層
MCPは概念的に2つの層で構成されます。データ層が内側、トランスポート層が外側です。
| 層 | 役割 |
|---|---|
| データ層 | 役割JSON-RPCベースのクライアント・サーバー間通信プロトコル。バージョン・機能の発見、Tool/Resource/Prompt/通知などのコア要素を定義 |
| トランスポート層 | 役割通信の確立・メッセージのフレーミング・認可を含む、データ交換のための通信手段を定義 |
データ層はJSON-RPC 2.0上に構築され、次の4つで構成されます。
- Discovery:
server/discoverで、サーバーが対応するバージョン・機能・識別情報を問い合わせる - Server features: Tool・Resource・Promptによる中核機能の提供
- Client features: サーバーがユーザーに入力を求めるelicitation
- Utility features: 通知と、長い処理の進捗トラッキング
トランスポート層は、データ層の意味を変えずに運ぶ「束縛(binding)」として定義されています。どのトランスポートでもメッセージの意味は同じで、違うのは区切り方、メタデータの運び方、キャンセルの伝え方です。標準は2つです。
stdioとStreamable HTTPの違い
stdio
クライアントがサーバーを子プロセスとして起動し、標準入出力で通信します。ネットワークオーバーヘッドがありません。メッセージは改行区切りの1行1メッセージで、メッセージ内に改行を含めることはできません。ログは stderr に書いてよく、クライアントは stderr の出力をエラーの印と決めつけてはいけないとされています。リクエストの取り消しは notifications/cancelled を送ります。
Streamable HTTP
メッセージごとにHTTP POSTを1つのエンドポイントへ送り、応答はJSONオブジェクトか、そのリクエスト専用のSSEストリームで返ります。ベアラートークン・APIキー・カスタムヘッダーによる標準的なHTTP認証が使え、仕様はOAuthでのトークン取得を推奨しています。リクエストの取り消しは、応答ストリームを閉じて伝えます。
トランスポートが通信の差を吸収するため、データ層は同じJSON-RPC 2.0メッセージ形式をどちらでも共通に扱えます。
Streamable HTTPでは、リクエスト1件がどう流れるか
HTTPの場合、データ層のメッセージは「本文が正」のまま、一部が通信経路上のヘッダーにも写されます。ロードバランサやゲートウェイが本文を解析せずに振り分けや検査をできるようにするためです。
tools/callが届くまで
- 1
クライアントが本文の `_meta` を組み立てる
プロトコルバージョンとクライアント機能を毎回入れます。クライアントの識別情報(clientInfo)も、設定で無効にしない限り入れるのが基本です。
- 2
同じ値をHTTPヘッダーに写す
すべてのPOSTに
MCP-Protocol-Version: 2026-07-28を付けます。さらにMcp-Method: tools/callと、ツール名を入れたMcp-Nameを付けます。Mcp-Nameが対象になるのはtools/call・resources/read・prompts/getです。 - 3
サーバーが検証する
ヘッダーのバージョンが本文の
_metaと食い違えば、サーバーは400 Bad RequestとHeaderMismatchのJSON-RPCエラーで拒否します。Originヘッダーがあって不正なら403 Forbiddenです。 - 4
JSONかSSEで返す
応答はJSONオブジェクト、またはSSEストリームで返ります。ストリーム上には進捗通知のようにそのリクエストに関係する通知だけが流れ、最終的な応答でストリームが終わります。
Origin の検証が必須なのはDNSリバインディング攻撃を防ぐためで、ローカルで動かすサーバーに特に効きます。プロキシ越しにSSEを流すなら、サーバーが X-Accel-Buffering: no を付けると、nginxなどが応答を溜め込んで通知の到着が遅れる事態を避けられます。
中核となる3つのプリミティブ — Tools / Resources / Prompts
データ層でクライアント・サーバー間が共有できる情報の種類を「プリミティブ」と呼びます。サーバーが公開できるプリミティブは3つです。
- Tools: AIアプリケーションが呼び出せる実行可能な関数(ファイル操作・API呼び出し・DBクエリなど)
- Resources: コンテキスト情報を提供するデータソース(ファイル内容・DBレコード・APIレスポンスなど)
- Prompts: モデルとのやり取りを構造化する再利用可能なテンプレート
クライアントは tools/list のような */list メソッドでプリミティブを動的に発見し、tools/call で実行します。データベースの例で言えば、サーバーはクエリ用のTool、スキーマ情報を返すResource、Toolの使い方を示すfew-shot例を含むPromptをまとめて公開できます。各プリミティブのリクエスト・レスポンス形式はTools仕様・Resources仕様・Prompts仕様でそれぞれ個別に定義されています。
server/discover と tools/list の応答には、キャッシュ用の ttlMs(鮮度の目安をミリ秒で示す)と cacheScope(誰が再利用してよいか)が付きます。仕様書の例では、discoverが3,600,000ms(1時間)、ツール一覧が300,000ms(5分)です。ステートレスなプロトコルが問い合わせを毎回やり直さずに済むための仕組みです。
クライアント側が公開できるプリミティブはElicitation(サーバーによるユーザーへの追加入力や確認の要求)です。ElicitationはMulti Round-Trip Requests(MRTR)パターンで配送されます。かつてクライアント側プリミティブだったSamplingとLoggingは、プロトコルバージョン 2026-07-28 で非推奨になりました。Samplingの新規実装はLLMプロバイダーAPIに直接統合する形に、Loggingはstdioなら stderr、そのほかはOpenTelemetryへのログ出力に置き換えることが推奨されています。
なぜ2026-07-28でステートレス化されたのか
2026-07-28 仕様の最大の変更点は、MCPをステートレスなプロトコルにしたことです。すべてのリクエストは、処理に必要な情報を自身の _meta フィールドに含みます。サーバーは以前のリクエストから何かを推測する必要がありません。
クライアントはプロトコルバージョン(io.modelcontextprotocol/protocolVersion)とクライアント機能(io.modelcontextprotocol/clientCapabilities)を毎リクエストに含めます。識別情報(io.modelcontextprotocol/clientInfo)も、設定で外さない限り含めます。サーバーは server/discover を必ず実装し、対応するバージョン・機能・識別情報を返します。一方、クライアントがdiscoverを呼ぶかどうかは任意で、他のリクエストより前に送ってもよい、という位置づけです。
2025年の仕様では、接続ごとのセッションを initialize ハンドシェイクで確立し、サーバーからクライアントへJSON-RPCリクエストを送ることもできました。2026-07-28ではその前提が変わっています。サーバーがJSON-RPCリクエストを開始することは禁止され、サンプリング・elicitation・ルート一覧のようなサーバーからの問いかけは、InputRequiredResult という応答の中に入力要求として埋め込まれます。クライアントはそれに答えて、同じ操作をもう一度送ります。stdioでは、サーバーが stdout に書けるのは応答と通知だけです。
各リクエストが独立して完結するため、リクエストを受けるサーバーはどの接続の続きかを覚えておく必要がありません。ヘッダーの写しによる振り分けや検査の経路を支えるのが、この自己完結性です。互換性のため、旧版の相手には仕様がフォールバックの手順を定めており、仕様書には実装者向けの互換性マトリクスも載っています。
LSPからMCPが受け継いだ着想
MCPはLanguage Server Protocol(LSP)から着想を得ています。LSPは、プログラミング言語のサポートを開発ツールのエコシステム全体に広げる方法を標準化したプロトコルです。MCPも同じ発想で、AIアプリケーションに文脈とツールを統合する方法を標準化します。
1つのMCPサーバー実装を、MCPに対応したAIアプリケーションから共通に使えるのが狙いです。
LSPと分かれるのは、状態の扱いです。MCPは 2026-07-28 で接続スコープのセッションを仕様から外し、すべてのリクエストを自己完結させました。前節のステートレス化は、この標準化を、接続ごとの状態を持たない方向へ進めた変更です。
通知はなぜオプトイン購読方式になったか
2026-07-28 では、サーバーからクライアントへのリアルタイム通知は subscriptions/listen という長時間ストリームに統合されました。クライアントは受け取りたい通知の種類を指定してsubscriptions/listenを呼び、サーバーの応答はそのままSSEストリームとして開いたままになります。サーバーは最初のメッセージで、受け付けた通知の種類を notifications/subscriptions/acknowledged で返します。以前の resources/subscribe とHTTP GETによる通知の受け口は、この仕組みに置き換わりました。対象の変化が起きると、notifications/tools/list_changed や notifications/resources/updated といった通知がこのストリームに流れます。
通知は、応答を期待しないJSON-RPC 2.0の通知セマンティクスに従い、リクエストIDに相当する id を持ちません。stdioには応答ごとのストリームがないため、listenに紐づく通知は _meta の io.modelcontextprotocol/subscriptionId で突き合わせます。HTTPならストリームそのものが突き合わせの手がかりです。
進捗通知(notifications/progress)とログメッセージ(notifications/message)は別の経路です。この2つはlistenストリームには流れず、元のリクエストの応答ストリーム上にだけ現れます。購読ストリームは「サーバー側の状態変化」を伝える専用の経路で、進行中のリクエストの経過を知るには、そのリクエストの応答側を見ます。
まとめ
2025年の仕様のサーバーに繋ぐ場合は、前節で触れたフォールバックの手順と互換性マトリクスを先に見ると、接続できない原因を絞りやすくなります。プロトコル全体の改訂履歴はMCP仕様のバージョン履歴、Claudeでの実際の使い方はMCPとは何かにあります。