MCPクライアントは、結局どこにいるのか
連載「ReactでMCPクライアントを作る」第1回 — ホスト・クライアント・サーバーと、legacy/modernの地図
「MCPクライアントを作る」と言うとき、そのクライアントは一体どこにいるのでしょうか。ブラウザで動くアプリそのものを指すのか、それともアプリの中に隠れた一部品を指すのか——この問いに答えないまま実装を始めると、後々ヘッダーやセッションの話で必ず迷子になります。
この記事は、全6回の連載「ReactでMCPクライアントを作る」の第1回です。連載では最終的にブラウザで動く小さなアプリを組み立てていきますが、初回はコードより先に地図を描きます。MCP(Model Context Protocol)という言葉を初めて聞く方にも、「AIとツールをつなぐ仕組みらしい」くらいの認識の方にも届くよう、登場人物とこの技術が置かれている転換期を押さえます。
目次
導入 — 表を返すだけのアプリ、その内側
やることはシンプルです。ユーザーがテキストボックスに「先月の売上が多かった順に商品を10件教えて」のような自然言語の問い合わせを入力すると、アプリがそれをSQLに変換し、リレーショナルデータベースに問い合わせて、結果を表にして返します。バックエンドも最低限しか持たない、最小構成のブラウザアプリです。
一見するとよくある「AIにSQLを書かせるアプリ」ですが、内側を分解すると3つの役割が出てきます。
- ユーザーの問い合わせを受け取り、画面に表を描くアプリ本体
- 自然言語からSQLを組み立てるLLMのAPI
- SQLを実行し、テーブルの構造や結果をアプリに返すデータベースへの窓口
この3つ目、「データベースへの窓口」の作り方を標準化しようとしているのがMCPです。そして「MCPクライアント」という言葉が指すのは、アプリ全体ではなく、この窓口とやり取りするための一部品にすぎません。
では、どこからどこまでがホストで、どこからがクライアントで、どこからがサーバーなのでしょうか。ここを取り違えたまま実装すると、後の回で出てくるヘッダーの話やセッションの話がすべて迷子になります。
MCPとは何か
MCP(Model Context Protocol)は、LLMアプリケーションが外部のデータやツールと接続するための、標準化されたプロトコルです。公式仕様は「LLMにコンテキストを共有する」「AIシステムにツールや機能を公開する」「組み合わせ可能な統合とワークフローを構築する」の3点を目的として挙げています。
通信の土台にはJSON-RPC 2.0を使います。JSON-RPCは「メソッド名とパラメータを持つリクエストを送り、結果かエラーを含むレスポンスを返す」という、リモート呼び出しのための軽量な規約です(詳細な構造は本記事の主眼ではないため、正確な定義は末尾の一次情報を参照してください)。MCPはこのJSON-RPCのメッセージに、独自の意味づけ(どんなメソッドがあり、誰が何を呼べるか)を積み重ねる形で設計されています。
似た設計を先に採用している例として、公式仕様はLanguage Server Protocol(LSP)を挙げています。LSPが「プログラミング言語ごとの支援機能」を標準化したのと同じように、MCPは「AIアプリケーションに追加のコンテキストとツールを組み込む方法」を標準化する、という立て付けです。
三者構成 — ホスト・クライアント・サーバー
MCPの仕様は、通信に関わる主体を3種類に分けています。
| 役割 | 仕様上の定義 | 冒頭のアプリでいうと |
|---|---|---|
| ホスト(Host) | 接続を開始するLLMアプリケーション | 問い合わせを受け付けるアプリ本体 |
| クライアント(Client) | ホストの内部にいる、サーバーとの接続を担当するコネクタ | データベースの窓口と話すための接続部品 |
| サーバー(Server) | コンテキストや機能を提供するサービス | テーブル定義や実行結果を返す、データベース側のサービス |
「クライアント」はアプリ全体の呼び名ではなく、ホストの内部でサーバーと1対1の接続を受け持つ部品を指す言葉です。連載タイトルの「MCPクライアントを作る」も、正確に言えば「ホストの中に、サーバーと話すためのクライアントを組み込む」ということです。
サーバーが差し出せる機能は、仕様で3種類に決められています。
- Resources: ユーザーやAIモデルが使う、コンテキストやデータ
- Prompts: ユーザー向けの、テンプレート化されたメッセージやワークフロー
- Tools: AIモデルが実行できる関数
逆にクライアント側がサーバーに提供できる機能として、仕様は Elicitation(サーバーがユーザーへ追加情報を求めるための仕組み)を挙げています。
冒頭のアプリで考えてみましょう。データベースの窓口になるMCPサーバーは、テーブル定義を Resource として、SQLを実行する機能を Tool として持つのが自然な形です。ホストはまず Resource でスキーマを受け取り、それをLLMのAPIに渡して「このスキーマに沿ったSQLを書いて」と頼みます。返ってきたSQLを今度は Tool の呼び出しとしてサーバーへ送り、その結果を表にする。この一往復半が、アプリの中で起きていることのすべてです。
LLMのAPIを呼ぶ部分は、MCPの外側にあります。MCPが標準化しているのは「ホストがサーバーからコンテキストや機能を得る手順」であって、「ホストがモデルをどう呼ぶか」まではMCPの範囲に含まれません。サーバー側の Resource / Tool の具体的な設計(inputSchema や outputSchema の書き方など)は第3回で、ホスト側の実装は第4回で解説します。
もう一つ、仕様の「メッセージパターン」のページには興味深い制約があります。「サーバーはJSON-RPCリクエストを発行してはならない(Servers MUST NOT initiate JSON-RPC requests)」という規定です。サーバーがモデルの出力(sampling)やユーザーへの追加確認(elicitation)、ファイルシステムの起点(roots)を必要とする場合は、サーバーからクライアントへ新規リクエストを飛ばすのではなく、進行中のリクエストへの応答として InputRequiredResult を返し、クライアントが inputResponses を添えて同じリクエストを再送する、という往復で処理します。
仕様はこれを Multi Round-Trip Requests(MRTR) と呼んでおり、先ほど挙げた Elicitation は、この MRTR の枠組みの中で実現される機能です。ただし、MRTRの具体的な往復は、この連載では実測できていません(この段落は仕様文の説明であり、実装での動作確認はしていません)。
この図は仕様が定める往復のパターンを図解したものであり、実装で動作を確かめたものではありません。
セッションを張る時代と、名乗るだけの時代
MCPの仕様には、2026-07-28のリビジョンで大きな断層が入りました。仕様の「Versioning and Compatibility」というページは、この断層の前後を legacy(2025-11-25以前)と modern(2026-07-28以降)という言葉で呼び分けています。
legacy era — ハンドシェイクでセッションを張る
2025-11-25以前のバージョンでは、クライアントは最初に initialize というメソッドを呼び、サーバーと対応バージョンや機能をすり合わせます。このやり取りに成功すると、以降のリクエストはそのセッションに紐づいた状態として扱われます。つまり、一度ハンドシェイクを済ませれば、以後のリクエストはそのセッションの続きとして処理される、という考え方です。
modern era — リクエストごとに名乗る
2026-07-28以降は、事前のハンドシェイクという概念そのものがありません。仕様は次のように書いています。
There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently.
日本語にすると、「ネゴシエーションのハンドシェイクは存在しない。すべてのリクエストが自分のプロトコルバージョンを運び、サーバーはリクエストごとに独立して受理か拒否かを判断する」ということです。では、そのバージョンはどこに書くのか。各リクエストのボディにある _meta フィールドに入れて送ります。サーバーが知らないバージョンだったときは、UnsupportedProtocolVersionError(エラーコード -32022)に、自分が対応しているバージョンの一覧を添えて返す決まりです。
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}
仕様はもう一つ、server/discover というメソッドをサーバーに必ず用意させています。クライアントは先にこれを呼んで対応バージョンを確かめてもいいですし、いきなり本命のメソッドを投げて、UnsupportedProtocolVersionError が返ってきてから合わせ直しても構いません。どちらでもよい、という作りです。
セッションという単位が無くなったことで、トランスポート側の設計も変わりました。標準のトランスポートは stdio(クライアントが起動したサブプロセスの標準入出力を使う方式)と、Streamable HTTP(1つのエンドポイントへのHTTP POSTでやり取りする方式)の2つです。
Streamable HTTPの仕様には次の一文があります。
Removal of the GET stream endpoint. / Removal of protocol-level sessions.
つまり、常時接続の状態を待ち受けるGETストリームと、プロトコルレベルのセッションという概念そのものが、2026-07-28で仕様から取り除かれました。Streamable HTTPの詳しい構造(必須ヘッダー、_metaの二重化、HeaderMismatchエラーなど)は第2回でまとめて説明します。
転換期に何が起きるか
legacyとmodernが混在する期間には、クライアントとサーバーの組み合わせによって結果が変わります。仕様には両者の組み合わせ表(Compatibility Matrix)が載っており、要点だけ抜き出すと次のようになります。
| クライアント | サーバー | 結果 |
|---|---|---|
| modern | modern | 動く。バージョン不一致は UnsupportedProtocolVersionError で検出し、対応バージョンへ再試行する |
| modern | legacy | 失敗する。legacy側は modern の申告形式を理解できない |
| legacy | modern | 失敗する。modern側は initialize を必須の入口として扱っていない |
| 両対応(dual-era) | どちらか一方 | 動く。相手の世代を検出してから合わせる |
この表からわかるとおり、「新しい仕様を読んで最新の書き方で実装すれば動く」とは限りません。相手(今回でいえばサーバー実装のライブラリ)がどちらの時代に対応しているかによって、同じクライアントコードでも結果が変わります。
ただし、これはあくまで仕様上の整理です。実際に手元の環境で試すと、何が起きるのでしょうか。
実測 — 今日SDKで書くとどう動くか
ここからは、仕様文だけでなく、実際に手を動かして確認した内容です。環境は Node.js v22.12.0、公式TypeScript SDK(@modelcontextprotocol/sdk)のバージョン1.30.0、実行日は2026-08-06です。
サーバーは echo ツールを1つだけ持つ最小構成のMCPサーバーを自前で用意しました。特定の製品やサービスには依存していません。
仕様のリビジョンとSDKの実装にはズレがある
まず、SDKが実際にどのプロトコルバージョンに対応しているかを確認しました。
LATEST_PROTOCOL_VERSION = 2025-11-25
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = 2025-03-26
SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]
仕様の現行リビジョンは2026-07-28ですが、公式TypeScript SDKの最新版(1.30.0)が実装しているのは2025-11-25までです。つまりSDKは、まだ legacy era の枠組みでしか動きません。配布物の中身を検索しても、2026-07-28 という文字列や server/discover というメソッド名は見つかりませんでした。
試しに 2026-07-28 を名乗ってリクエストを送ると、次のように拒否されます。
--- response status ---
400 Bad Request
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"},"id":null}
ここで一つ、仕様文と実装のズレに注意が必要です。仕様は未対応バージョンへの応答として UnsupportedProtocolVersionError(コード -32022)と、対応バージョン一覧を持つ data.supported を要求していますが、手元で確認した実装は -32000 という別のコードと、対応バージョンを埋め込んだ散文のメッセージを返しました。実装が仕様の新しいエラー語彙に追いつくまでは、こういう食い違いは起きるものだと思って読んでおいたほうがよさそうです。
「仕様の現在地」と「今日SDKで書ける形」は、分けて考える必要があります。この連載のハンズオン部分(第2回以降)は、実際に動かせることを優先し、今日のSDKが対応する legacy era の書き方を土台にします。そのうえで、modern era で何が変わるのかを都度補足していきます。
Acceptヘッダーが無いと、ボディを読む前に落ちる
Streamable HTTPの仕様は、Accept ヘッダーに application/json と text/event-stream の両方を並べるよう、クライアントに求めています。これを守らずに Content-Type だけを付けた素のPOSTを送ると、次のように 406 が返ってきます。
--- response status ---
406 Not Acceptable
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Not Acceptable: Client must accept both application/json and text/event-stream"},"id":null}
これは重要なポイントで、「素のPOSTがMCPサーバーで動かない」ことの本当の理由は、ハンドシェイクの有無ではなく Accept ヘッダーです。このチェックはボディの中身を読む前に行われるため、JSON-RPCのリクエストとして正しい形にしても、Accept が欠けていれば同じ406で弾かれます。Streamable HTTPの必須ヘッダーやレスポンスの読み方は、第2回で取り上げます。
ハンドシェイクの要否は、サーバーのセッションモード次第
もう一つ、実際に試すまで筆者も誤解していた点があります。「initialize を踏まずにメソッドを呼ぶと必ず失敗する」わけではありません。サーバーの実装をステートレス(セッションIDを発行しない構成)にすると、Accept さえ満たしていれば、ハンドシェイクなしで tools/list や tools/call が200で通りました。
--- response status ---
200 OK
--- response body ---
event: message
data: {"result":{"content":[{"type":"text","text":"hello"}]},"jsonrpc":"2.0","id":6}
一方、同じSDKでもサーバーをステートフル(セッションIDを発行する構成)にすると、initialize を踏まないリクエストは次のように拒否されます。
--- response status ---
400 Bad Request
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Server not initialized"},"id":null}
以降のリクエストにも、ハンドシェイクで発行された Mcp-Session-Id を毎回付ける必要があります。つまり、ハンドシェイクの要否は、プロトコルの規定というより、サーバーがどのセッションモードで実装されているか次第です。ステートレスな実装がすでに現実に存在しているという事実は、2026-07-28でプロトコルレベルのセッションそのものが仕様から削除された流れの、小さな伏線になっています。
もう一度、アプリの全体像へ
ここまでを踏まえて、最初のアプリをもう一度眺めてみましょう。アプリ本体がホストで、その内側にMCPクライアントという小さな接続部品がいます。この部品が、データベース側のMCPサーバーと話す係です。サーバーが差し出すのは、テーブル定義を返す Resource と、SQLを実行する Tool の二つでした。
LLMのAPIを呼んでSQLを組み立てる部分は、MCPの外側でホストが直接行います。そしてこの会話が、legacy eraのハンドシェイクの上で成り立つのか、modern eraの毎リクエスト申告の上で成り立つのかは、接続するサーバー実装が今どちらの時代に立っているかによって決まります。今日時点の公式SDKで組み立てる場合、実質的には legacy era の作法(initialize を踏むかどうかはサーバーのセッションモード次第、Accept は両方必須)に従うことになります。
この連載でこの先扱うこと
第1回は地図を描くところまでにとどめ、各論は次の回に送ります。
| 回 | テーマ |
|---|---|
| 第2回 | Streamable HTTPの実体 — 必須ヘッダー、_meta の二重化、HeaderMismatch、SSE、Origin検証 |
| 第3回 | サーバー側の設計 — ツールとリソース、inputSchema / outputSchema、x-mcp-header |
| 第4回 | React側の実装 — ブラウザから直接叩けるのか、BFFを挟むべきか、SSEの読み方、キャンセル |
| 第5回 | 待てない処理 — notifications/progress、subscriptions/listen、Tasks拡張、そしてキャンセルが協調的であること |
| 第6回 | 完成と振り返り — 認可の設計、legacyとmodernのデュアルエラ互換、生成SQLをそのまま実行してよいか |
第5回で扱うTasks拡張は、公式仕様が挙げているオプトインの拡張機能の一つで、ポーリングや処理途中の入力、状態を保持したハンドルを使って、長時間かかる処理を非同期に扱うための仕組みです(この段落は仕様文の紹介であり、動作の実測はしていません)。
まとめ
MCPは、ホスト・クライアント・サーバーという三者をJSON-RPC 2.0でつなぐプロトコルです。「クライアント」はアプリ全体の呼び名ではなく、ホストの内部でサーバーと1対1の接続を受け持つ部品を指します。2026-07-28のリビジョンで、セッションをハンドシェイクで張る legacy era から、リクエストごとに _meta で名乗る modern era への転換が明文化されましたが、公式TypeScript SDKの最新版はまだ legacy era の枠組みでしか動きません。
実装で確かめると、素のPOSTが落ちる本当の理由は Accept ヘッダーの欠如であり、initialize の要否はサーバーのセッションモード次第でした。仕様の言葉と、今日書けるコードの言葉を混同しないことが、この技術を実際に動かす第一歩になります。次回は、ここで触れた Streamable HTTP のヘッダーとSSEの中身を、実際のやり取りに沿って掘り下げます。
この連載の記事
第1回から順に読むと、仕様の地図を描くところから、ブラウザで動くコードまでが一本の線でつながります。どの回からでも単体で読めるようには書いていますが、前後を行き来したくなったらここから飛んでください。
- 第1回: MCPクライアントは、結局どこにいるのか(この記事) — ホスト・クライアント・サーバーの三者と、legacy/modernの断層
- 第2回: MCPサーバーに、どう話しかければ聞いてもらえるのか — Streamable HTTPの必須ヘッダー、SSE、Origin検証
- 第3回: MCPサーバーの窓口は、何を差し出し、何を隠すべきか — スキーマ、annotations、失敗の通り道
- 第4回: Reactは、MCPサーバーを直接叩けるのか — CORS、BFF、SSEの逐次読み、協調的キャンセル
- 第5回: 重い処理を、MCPはどう見せて、どう止めるのか — 進捗通知、購読、Tasks拡張
- 第6回: その扉を開けるのは、誰の仕事か — 認可の設計、legacy/modernの互換判定、生成SQLの危うさ
参考にした一次情報
- MCP Specification(latest)(三者構成、JSON-RPC 2.0、Resources/Prompts/Tools、Elicitation、Tasks拡張の概要)
- Versioning and Compatibility(2026-07-28)(legacy / modern の定義、ハンドシェイク廃止、
UnsupportedProtocolVersionError、server/discover、互換性マトリクス) - Message Patterns(2026-07-28)(サーバーはJSON-RPCリクエストを発行してはならないという規定、Multi Round-Trip Requestsの説明)
- Transports Overview(2026-07-28)(標準トランスポートがstdioとStreamable HTTPの2つであること)
- Streamable HTTP(2026-07-28)(GETストリームとプロトコルレベルセッションの廃止、
Acceptヘッダー要件) - Tasks Extension Overview(第5回で扱うTasks拡張の概要)
- JSON-RPC 2.0 Specification(MCPの通信土台となるメッセージ形式)








