MCP stdioトランスポートの仕様を実装レベルで確認する
MCPのstdioトランスポートは改行区切りJSON-RPCで通信します。stderrの扱い、SIGTERMからのシャットダウン手順、_metaでのメタデータ運搬までを仕様原文で確認します。
MCPのstdioトランスポートは、クライアントがMCPサーバーを子プロセスとして起動し、その標準入出力でJSON-RPCメッセージをやり取りする方式です。ネットワークを介さないぶん構成が単純で、手元のファイルや社内限定のツールをつなぐローカルサーバーの多くがこの方式を使います。仕様原文では、メッセージの区切り方からプロセスの終了手順まで、実装者が迷いやすい部分がかなり具体的に定められています。
stdioの通信ルール — 改行区切りJSON-RPCと3つの制約
サーバーはstdinからJSON-RPCメッセージを読み、stdoutに書き込みます。メッセージは1行に1つのリクエスト・通知・レスポンスで、行の途中に改行を含んではいけません。
| 主体 | してよいこと | してはいけないこと |
|---|---|---|
| サーバー | してよいことstderrへのUTF-8ログ出力(情報・デバッグ・エラーいずれも自由) | してはいけないことstdoutに有効なMCPメッセージ以外を書く |
| クライアント | してよいことstderr出力の取得・転送・無視のいずれか | してはいけないことstdinに有効なMCPメッセージ以外を書く、stderrをエラーの合図とみなす |
stderrをエラーの合図とみなしてはいけない、という制約は見落としやすいところです。デバッグログを大量に出すサーバーと組み合わせるツールを書くときは、stderrへの出力があってもプロセスが正常に動いていると判断できる設計にしておく必要があります。同様に見落としやすいのがstdout側の制約で、サーバーは有効なMCPメッセージ以外をstdoutに一切書いてはいけません。デバッグ目的でconsole.logのような出力関数をうっかりstdoutに向けてしまうと、そこに混じった文字列がJSON-RPCのパーサーを壊し、以降のメッセージがすべて読めなくなります。ログは必ずstderrに、プロトコルメッセージはstdoutにという分離を徹底する必要があります。
この配線ルール自体は標準入出力に限定されません。仕様は「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段階です。まず子プロセスへの入力ストリームを閉じ、サーバーの終了を待ち、それでも一定時間内に終了しなければ強制終了します。
POSIX環境での強制終了は、通常SIGTERMからSIGKILLへとエスカレーションします。Windowsには相当するシグナルがないため、TerminateProcessやJob Objectsが使われます。サーバー側は、標準入力が閉じられるかEOFを検知したら速やかに終了すべきだと定められています。これがポータブルな唯一のシャットダウン合図であり、これに従うサーバーであれば強制終了に頼る必要が減ります。サーバー自身が先に終了したい場合は、出力ストリームを閉じてプロセスを終了させる形も認められています。
サーバープロセスが予期せず終了した場合、クライアントは再起動すべきだとされています。プロトコルはステートレスなので、進行中だったリクエストはそのまま失われますが、クライアントは新しいプロセスに対してリクエストを送り直すだけで済みます。進行中のsubscriptions/listenストリームも、再起動後に改めて張り直す必要があります。
キャンセルの扱いも単純です。stdioは単一の双方向チャネルしか持たないため、リクエストごとに閉じられる専用ストリームがありません。進行中のリクエストを止めたいときはnotifications/cancelled通知をそのリクエストIDと一緒に送ります。
後方互換のプロービング — server/discoverでレガシー判定
新旧両方のMCPバージョンに対応したいクライアントは、他のリクエストを送る前にserver/discoverで相手を探ることが推奨されています。結果は3通りです。DiscoverResultが返れば相手は最新仕様に対応しており、共通のバージョンを選んで続行します。UnsupportedProtocolVersionErrorのような既知のエラーが返れば、相手は最新仕様対応だが要求したバージョンには非対応と分かり、相手が広告する対応バージョンの中から選び直します。それ以外のエラーや無応答の場合は、相手をレガシーサーバーとみなしinitializeハンドシェイクにフォールバックします。
ここで仕様が強調しているのは、「特定のエラーコード1つにフォールバックの判定を紐付けてはならない」という点です。レガシーサーバーは未知の事前initializeリクエストに対して、実装依存のエラー(よく見るのは-32601や-32602)を返すこともあれば、何も返さないこともあります。
Claude Codeでの挙動はこの仕様の推奨と少しずれます。v2ランタイム(Claude Code v2.1.232以降が既定)は、HTTPサーバーとclaude.aiコネクタサーバーには標準で2026-07-28対応可否を尋ねますが、stdioサーバーに対しては環境変数MCP_PROTOCOL_NEGOTIATIONをautoに設定しない限りプロービングを行わず、常に旧来のハンドシェイクで接続します(この環境変数はClaude Code v2.1.221以降が必要です)。
MCP_PROTOCOL_NEGOTIATION=auto claude自作のstdioサーバーを2026-07-28対応にしても、この環境変数を設定しない限りClaude Codeは古い方式のまま話しかけ続けるということです。
stdioは2026-07-28のステートレス化を骨格だけ受け取る動き
Streamable HTTPの改訂は、ロードバランサーやゲートウェイを挟んだ複数バックエンド運用を前提にヘッダー層やセッション廃止を進めましたが、stdioにはそもそも仲介者が存在しません。子プロセス1つとクライアント1つが直接つながるだけの構成に、ロードバランサー向けの設計を持ち込む理由がないのです。
Claude Codeがstdioサーバーへのプロービングを既定でオフにしているのも、この非対称性の表れだと見ることができます。ステートレス化やMRTRパターンといった骨格部分は_meta経由でstdioにも及んでいますが、ヘッダーによるルーティング最適化のような「複数クライアント・複数サーバーの本番運用」を前提にした部分は、stdioという1対1の接続方式には最初から刺さりにくい設計です。
動作確認にはMCP Inspectorが向いている
stdioサーバーはネットワークを介さないぶん、ブラウザの開発者ツールのようにリクエストを覗く手段が手元にありません。改行区切りのJSON-RPCが正しく吐かれているか、_metaの必須フィールドが揃っているかを目視で追いたいときは、公式ツールのMCP Inspectorが役に立ちます。サーバーの標準入出力を仲介して、送受信されたメッセージをそのまま表示してくれるため、_metaの値やエラーレスポンスの中身を1件ずつ確認しながらデバッグできます。導入手順はMCP Inspectorの使い方にまとめています。
まとめ
stdioトランスポートは、改行区切りのJSON-RPCを標準入出力でやり取りし、メタデータはヘッダーでなく_metaフィールドに乗せる方式です。終了はstdinのクローズから始めてSIGTERM・SIGKILLへ段階的にエスカレーションし、予期せぬ終了時はステートレスな設計を活かして単純に再起動・再送すれば復旧できます。Claude Codeでは、MCP_PROTOCOL_NEGOTIATION=autoを設定しない限りstdioサーバーは常に旧来のハンドシェイクで接続される点だけ覚えておくと、自作サーバーの動作確認で迷いません。
MCP全体の3層構造や利用面別の違いはMCPとはに、stdioとStreamable HTTPの使い分けはAgent SDKの文脈ですがAgent SDK MCP接続ガイドにまとめています。接続できないときの切り分け手順はMCPサーバーに接続できないときの切り分け手順を参照してください。