MCP stdioトランスポートの仕様を実装レベルで確認する
MCPのstdioトランスポートは改行区切りJSON-RPCで通信します。stderrの扱い、SIGTERMからのシャットダウン手順、_metaでのメタデータ運搬までを仕様原文で確認します。
MCPのstdioトランスポートは、クライアントがMCPサーバーを子プロセスとして起動し、その標準入出力でJSON-RPCメッセージをやり取りする方式です。標準外の要件がある場合はこのstdioのフレーミングを流用してカスタムトランスポートを設計する選択肢もあります。ネットワークを介さないぶん構成が単純で、手元のファイルや社内限定のツールをつなぐローカルサーバーの多くがこの方式を使います。仕様原文では、メッセージの区切り方からプロセスの終了手順まで、実装者が迷いやすい部分がかなり具体的に定められています。
stdioの通信ルール — 改行区切りJSON-RPCと3つの制約
サーバーはstdinからJSON-RPCメッセージを読み、stdoutに書き込みます。メッセージは1行に1つのリクエスト・通知・レスポンスで、行の途中に改行を含んではいけません。
| 主体 | してよいこと | してはいけないこと |
|---|---|---|
| サーバー | してよいことstderrへのUTF-8ログ出力(情報・デバッグ・エラーいずれも自由) | してはいけないことstdoutに有効なMCPメッセージ以外を書く |
| クライアント | してよいことstderr出力の取得・転送・無視のいずれか | してはいけないことstdinに有効なMCPメッセージ以外を書く、stderrをエラーの合図とみなす |
stderrをエラーの合図とみなしてはいけない、という制約は見落としやすいところです。デバッグログを大量に出すサーバーと組み合わせるツールを書くときは、stderrへの出力があってもプロセスが正常に動いていると判断できる設計にしておく必要があります。stdout側の制約も同じくらい見落としやすく、デバッグ目的のconsole.logがうっかりstdoutに向くと、混じった文字列がJSON-RPCのパーサーを壊して以降のメッセージがすべて読めなくなります。ログは必ずstderrに向けます。
サーバーはstdoutにJSON-RPCのリクエストも書けません。サーバー側から問い合わせたい内容は、MRTRのInputRequiredResultに載せて返します。クライアントもstdinにレスポンスを書いてはいけません。
Claude Codeのクライアント側も、この違反への挙動を何度か直しています。changelogによると、v2.1.105では不正な出力でセッションが固まる代わりに"Connection closed"で切断するようになりました。v2.1.110では、その変更で非JSONの行が1行紛れただけで切断される回帰が直されています。v2.1.132では、stdoutにプロトコル外のデータを書き続けるサーバーでメモリが際限なく増える問題も修正されました。
とはいえ、これはClaude Code側の救済にすぎません。仕様上は違反のままで、他のクライアントが同じ寛容さを持つとは限りません。
この配線ルール自体は標準入出力に限定されません。1行1メッセージの改行区切りJSON-RPCという書式は、UnixドメインソケットやTCP接続でもそのまま使えると仕様に書かれています。独自のトランスポートを作る場合も、このフレーミングとメッセージ規則を再利用することが推奨されています。
リクエストメタデータはヘッダーでなくメタフィールドに乗る
Streamable HTTPではプロトコルバージョンやクライアント情報をHTTPヘッダーに複製しますが、stdioにはそもそもヘッダー層がありません。すべてのメタデータはJSON-RPCボディの_metaフィールドにインラインで格納されます。
_metaキー | 必須か | 内容 |
|---|---|---|
io.modelcontextprotocol/protocolVersion | 必須か必須 | 内容このリクエストのプロトコルバージョン(例: "2026-07-28") |
io.modelcontextprotocol/clientCapabilities | 必須か必須 | 内容このリクエストに関係するクライアント機能 |
io.modelcontextprotocol/clientInfo | 必須か任意(送信が推奨) | 内容クライアント名とバージョン |
io.modelcontextprotocol/logLevel | 必須か任意 | 内容このリクエストでサーバーが出すべき最小ログレベル |
2026-07-28改訂でMCPはステートレスなプロトコルになりました。以前は接続開始時にinitialize/initializedのハンドシェイクを1度だけ交わしていましたが、今はリクエストのたびに_metaでプロトコルバージョンと機能を運びます。サーバーは「同じstdioプロセス上の以前のリクエストを覚えている」ことを前提にしてはならず、必須フィールドが欠けたリクエストは-32602(Invalid params)エラーで拒否する義務があります。
ステートレス設計がstdioのプロセス生存期間に求めること
2026-07-28版のMCPは、リクエストの処理に必要な情報をすべてそのリクエスト自身から得て、以前のリクエストから状態を推測しないというステートレスな原則を掲げています。この原則はstdioにとって、単なる建前ではなく実装の前提を変える話です。
仕様は、stdioプロセスのような開いた接続は会話やセッションではないと注記しています。クライアントは同じstdioプロセス上で無関係な複数のリクエストを混在させてよく、サーバー側もプロセスや接続の同一性を会話の連続性の代わりに使ってはいけません。逆にクライアント側にも制約があり、1つのタスクや会話の生存期間をstdioプロセスの生存期間と同一視すべきではないとされています。つまり「このタスクが終わったからプロセスを落とす」という設計は、仕様が想定する使い方から外れます。
サーバーは複数のタスク・スレッド・会話にまたがるリクエストを同時に処理できる状態を保つべきだとも定められています。子プロセス1つに対してリクエストを1つずつ順番に処理すればよい、という単純化はもうできません。
シャットダウンの手順 — SIGTERMからSIGKILLへ
クライアントがサーバーを終了させる標準手順は、次の3段階です。
クライアントが行う終了手順
- 1
入力ストリームを閉じる
子プロセス(サーバー)の
stdinを閉じます。これが最初の合図です。 - 2
終了を待つ
サーバーが自分で終了するまで、妥当な時間だけ待ちます。
- 3
強制終了する
それでも終了しなければ、OSに合った方法で強制終了します。
POSIX環境での強制終了は、通常SIGTERMからSIGKILLへとエスカレーションします。Windowsには相当するシグナルがないため、TerminateProcessやJob Objectsが使われます。サーバー側は、標準入力が閉じられるかEOFを検知したら速やかに終了すべきだと定められています。これがポータブルな唯一のシャットダウン合図であり、これに従うサーバーであれば強制終了に頼る必要が減ります。サーバー自身が先に終了したい場合は、出力ストリームを閉じてプロセスを終了させる形も認められています。
Claude Code側では、v2.1.283で「起動中にセッションが終わると、stdioサーバーが動いたまま残る」問題が直されています。仕様が唯一のポータブルな合図とするstdinのEOFで終わる作りにしておけば、クライアントの終了手順とも噛み合います。
サーバープロセスが予期せず終了した場合、クライアントは再起動すべきだとされています。プロトコルはステートレスなので、進行中だったリクエストはそのまま失われますが、クライアントは新しいプロセスに対してリクエストを送り直すだけで済みます。進行中のsubscriptions/listenストリームも、再起動後に改めて張り直す必要があります。
キャンセルの扱いも単純です。stdioは単一の双方向チャネルしか持たないため、リクエストごとに閉じられる専用ストリームがありません。進行中のリクエストを止めたいときはnotifications/cancelled通知をそのリクエストIDと一緒に送ります。
subscriptions/listenで届く通知は、_metaのio.modelcontextprotocol/subscriptionIdでどの購読のものかを対応づけます。クライアントにはこの照合が必須です。
後方互換のプロービング — server/discoverでレガシー判定
新旧両方のMCPバージョンに対応したいクライアントは、他のリクエストを送る前にserver/discoverで相手を探ることが推奨されています。結果は3通りに分かれます。
server/discoverへの返答と次の一手
DiscoverResultが返る
相手は最新仕様に対応しています。
supportedVersionsから共通のバージョンを選んで続行します。既知の最新仕様エラーが返る
UnsupportedProtocolVersionErrorなどが返れば、相手は最新仕様対応でも、要求したバージョンには非対応です。広告されたsupportedの中から選び直し、initializeには戻りません。それ以外のエラー、または無応答
相手をレガシーサーバーとみなし、
initializeハンドシェイクにフォールバックします。
ここで仕様が強調しているのは、「特定のエラーコード1つにフォールバックの判定を紐付けてはならない」という点です。レガシーサーバーは未知の事前initializeリクエストに対して、実装依存のエラー(よく見るのは-32601や-32602)を返すこともあれば、何も返さないこともあります。
最新仕様だけに対応するクライアントでも、プロービングは推奨されています。initializeより前に届いたことを検証しないレガシーサーバーが、tools/callのような新旧で意味が分かれるメソッドを旧仕様の意味で処理しかねないためです。
Claude Codeがこのプロービングを行うのは、MCPクライアントのv2ランタイムです。フィーチャーフラグを取得するセッションでは、v2.1.232以降がv2を使います。対象サーバーは種類によって扱いが違います。
- HTTPサーバーには、プロービングで2026-07-28への対応を尋ねます。
- claude.aiコネクタには、フィーチャーフラグを取得するセッションで尋ねます。v2.1.295以降は、取得しないインストールでも既定で尋ねます。
- stdioサーバーには、v2.1.292以降、すべてのインストールで既定で尋ねます。
フィーチャーフラグを取得しないセッションでも、v2.1.274以降はBedrockやFoundryなどの利用時とテレメトリ無効時にv2が既定になります。ランタイムは環境変数MCP_SDK_GENERATIONにv1かv2を指定して選ぶこともできます。
環境変数MCP_PROTOCOL_NEGOTIATION(v2.1.221以降)で、この挙動を固定できます。autoならHTTP・コネクタ・stdioのすべてに尋ね、legacyなら全サーバーを旧来のハンドシェイクに留めます。
MCP_PROTOCOL_NEGOTIATION=auto claude自作のstdioサーバーを2026-07-28対応にした場合、Claude Codeが新方式で話しかけるかどうかは、バージョンとセッションの条件で変わります。確実に試したいならautoを付け、旧来の経路で試したいならlegacyを付けます。
プロービング対象のサーバーでは、最初に届くリクエストがinitializeとは限りません。v2.1.238では、initializeより前にserver/discoverを受けたstdioサーバーが、遅延起動のはずのバックエンドを毎回立ち上げてしまう問題が直されました。起動時に重い処理を置く場合は、server/discoverが最初に来る経路も想定しておく必要があります。
stdioは2026-07-28のステートレス化を骨格だけ受け取る動き
Streamable HTTPの改訂は、ロードバランサーやゲートウェイを挟んだ複数バックエンド運用を前提にヘッダー層やセッション廃止を進めました。stdioにはそもそも仲介者が存在しません。子プロセス1つとクライアント1つが直接つながるだけの構成に、ヘッダー層もありません。
そのためstdioが受け取ったのは、_metaによるリクエスト単位のメタデータと、ステートレスという原則、MRTR(複数往復リクエスト)のパターンといった骨格の部分です。ヘッダーによるルーティング最適化のように、複数クライアント・複数サーバーの本番運用を前提にした部分は、1対1の接続方式には最初から刺さりません。
Claude Codeがstdioサーバーに渡す環境変数
stdioサーバーはクライアントが起動する子プロセスなので、起動時の環境変数が使える入力経路になります。Claude Codeのchangelogには、次の環境変数が子プロセスに渡されると書かれています。
| 環境変数 | 渡されるようになったバージョン |
|---|---|
CLAUDE_PROJECT_DIR | 渡されるようになったバージョンv2.1.139 |
CLAUDE_CODE_SESSION_ID、CLAUDECODE=1 | 渡されるようになったバージョンv2.1.154 |
v2.1.163では、--resumeで再開したときも、stdioサーバーがフックやBashと同じCLAUDE_CODE_SESSION_IDを受け取るようになりました。なおこのセッションIDはClaude Code側の識別子で、MCPの仕様が言う会話の連続性とは別物です。サーバーがこれを会話の状態管理に使うと、ステートレスの原則から外れます。
サーバーの登録はclaude mcp addで行います。v2.1.289の--helpの例では、環境変数は-eで渡し、サーバー自身のコマンドは--の後ろに置きます。
claude mcp add my-server -e API_KEY=xxx -- npx my-mcp-server動作確認にはMCP Inspectorが向いている
stdioサーバーはネットワークを介さないぶん、ブラウザの開発者ツールのようにリクエストを覗く手段が手元にありません。改行区切りのJSON-RPCが正しく吐かれているか、_metaの必須フィールドが揃っているかを目視で追いたいときは、公式ツールのMCP Inspectorが役に立ちます。サーバーの標準入出力を仲介して、送受信されたメッセージをそのまま表示してくれるため、_metaの値やエラーレスポンスの中身を1件ずつ確認しながらデバッグできます。導入手順はMCP Inspectorの使い方にまとめています。
まとめ
stdioサーバーを書くときの境界線は2つです。stdoutにはプロトコルメッセージだけを書き、ログはstderrに出します。終了はstdinのEOFを合図にし、プロセスの生存期間に会話の状態を結びつけません。
自作サーバーの動作確認は、autoとlegacyの両方で通しておくと、新旧どちらのクライアントが来ても接続できる状態を保てます。最初にstdoutの出力だけをMCP Inspectorで眺めると、混入した文字列を早い段階で見つけられます。
MCP全体の3層構造や利用面別の違いはMCPとはに、stdioとStreamable HTTPの使い分けはAgent SDKの文脈ですがAgent SDK MCP接続ガイドにまとめています。接続できないときの切り分け手順はMCPサーバーに接続できないときの切り分け手順を参照してください。