MCP stdio serverが起動時に接続できないとき、stdoutのJSON-RPC混入を調べる

MCP serverが起動しているのにhostが接続できない、JSON parse errorになる、initializeで応答を読めないときは、まずstdioの役割を確認してください。

AIものさし編集部

MCP serverが起動しているのにhostが接続できない、JSON parse errorになる、initializeで応答を読めないときは、まずstdioの役割を確認してください。stdio transportではstdinがrequest、stdoutがprotocol message、stderrが診断ログです。起動バナー、console.log、進捗表示など通常ログがstdoutへ一行でも出ると、hostはそれをJSON-RPC/MCP messageとして読み、接続前にparse errorになることがあります。ただし、すべての初期接続が現在もinitializeを使うとは限りません。実際のhostとprotocol versionが旧式のinitialize handshakeを使うのか、2026-07-28仕様のserver/discover flowを使うのかを確かめ、stdoutの破損とhandshake/version不一致を分けて直します。

最初にtransportとprotocol versionを確認する

stdioはhostがserverを子processとして起動し、stdin/stdoutで双方向通信するtransportです。MCP specificationでは、clientがstdinへJSON-RPC messageを送り、serverがstdoutへresponseやnotificationを返します。一つのmessageはnewline-delimitedで、各行に一つのJSON-RPC messageが入ります。serverはstdoutへ有効なMCP message以外を書いてはいけません。stderrは情報・debug・error等のloggingに使えますが、clientが表示するか無視するかはhostによって異なります。

次に、clientとserverがどのprotocol versionで接続を試みているかを確認します。古い仕様や対応clientでは、通常initialize requestとinitialize responseのhandshakeが使われます。現行の2026-07-28 specificationでは、modern protocolのclientはserver/discoverを使ってversionを選択でき、legacy versionとの互換性を保つ場合はその結果に応じてinitializeへfallbackする手順が定義されています。したがって「initializeがログにないから必ず壊れている」とは断定できません。host、SDK、設定が想定するversionと、そのversionで期待される最初の交換を照合します。

まずstdoutとstderrを別々に捕まえる

起動失敗の最初の兆候は、画面に表示されるerror messageだけではなく、processの実出力にあります。hostと同じcommand、arguments、working directory、environmentでserverを起動し、stdinへ適切なprotocol requestを送ったときのstdoutとstderrを別ファイルまたは別streamとして記録します。secret、OAuth token、利用者データを採取ログに含めないでください。

確認する順番:

  1. 起動commandが合っているか。 command、引数、実行path、working directory、runtime version、必要なenvironment variableがhost設定と一致するか確認します。

  2. processが起動して待機するか。 起動直後に終了・crashする場合は、stderrのstack traceやruntime errorを確認します。これはprotocol parse errorとは別の問題です。

  3. stdoutの先頭行を見る。 先頭バイトからJSON-RPC messageの外形が始まっているか確認します。Starting server...、Listening...、debug log、package managerのbanner、依存moduleのconsole.logなど、人間向け文字列が一行でも混ざれば疑います。

  4. stderrを別に確認する。 診断ログはstderrへ出し、stdoutにはMCP messageだけを残します。SDKがprotocol responseを出す前の初期化ログも調べます。

  5. 一行ごとに構文を確かめる。 JSONとしてparseできても、JSON-RPC/MCP messageの構造や期待するmethodと一致するとは限りません。単なるJSON、空行、途中で分割されたJSON、複数messageを一行に連結した出力も確認します。

  6. host側の読み取り位置を確認する。 serverが正しいstdoutへ応答しているか、hostが別process・古いcommand・別の設定を起動していないか突き合わせます。

stdoutを壊す典型例

次のような出力は、運用ログとしては自然でも、stdio transportではprotocolを壊す可能性があります。

  • 起動時の「MCP server started」をstdoutへprintする。

  • Pythonでprint()、Node.jsでconsole.log()をdebug用途に使う。

  • package managerやshell wrapperがbanner、warning、progressをstdoutへ出す。

  • 依存libraryが標準出力へデバッグ内容を書く。

  • JSON-RPC応答を組み立てる前に環境・configをstdoutへdumpする。

  • serverの一部だけJSON-RPCを出し、別moduleが独自の行をstdoutへ出す。

対策はログを単に消すことではありません。通常ログはstderr、または使用SDKがサポートするprotocol loggingへ送り、stdoutの所有者をstdio transportに一本化します。エラーの調査に必要な診断はstderrに残し、hostのserver logで確認できるようにします。SDKがstdoutを内部管理する場合は、独自に同じstreamへ書き込まないでください。

stdoutとstderrの最小検査表

表は横にスクロールできます

観察結果主な原因候補次の確認
stdout先頭が普通の文章起動banner、print、依存libraryのlog書き込み元を検索しstderrまたはprotocol loggerへ移す
stdoutが空でprocessが終了command、runtime、env、module import時の例外exit codeとstderrを先に調べる
stdoutはJSONだがhostがrejectJSON-RPC構造、method、protocol version、改行framing期待messageと仕様versionを比較する
stderrにwarningがありstdoutは正常warning自体はprotocolを壊さないが原因調査に有用hostがstderrを記録しているか確認する
responseが正しいがhostがtimeoutstdin/stdoutのpipe、buffering、process lifetime、handshake期待差hostと同じ起動条件でInspectorまたは対応clientを使い再現する
initializeが見えないhostがmodern server/discoverを使用、またはrequest前に失敗client/serverのprotocol versionと最初のrequestを確認する

表の「原因候補」は診断の入口です。実際のstdout、stderr、exit code、request logを見ずに、すべてをconsole.logのせいにしないでください。

handshake・version問題をstdout汚染と分ける

旧式のinitialize handshakeを使う接続では、clientからのinitialize request、serverからのinitialize response、必要な後続通知など、versionごとに定義された手順を確認します。一方、2026-07-28のMCP仕様ではmodern clientがserver/discoverで対応versionを選ぶflowが記載され、legacy serverと話す場合のfallbackも定義されています。現在動かしているclient/serverが旧版、modern版、両方対応のどれかを先に特定します。

次の点を照合してください。

  • hostまたはSDKのMCP protocol version。

  • 最初にstdinへ送るrequest methodと、そのmessageのJSON-RPC形式。

  • serverが返すmethod、id、version、response shape。

  • client/serverが共通して対応するprotocol version。

  • stdoutへ別の文字列が混ざる前に接続が切れていないか。

  • legacy fallbackを実装するclientが、仕様に沿った条件で切り替えているか。

たとえば、hostがserver/discoverを送るのにserverがlegacy initializeだけを想定していると、stdoutがきれいでも最初のmethodが一致しない可能性があります。逆に、protocol versionの取り違えに見えても、実際にはstdoutのbannerが先に届いてhost parserがrequest/responseを読めていない場合があります。両方を一度に変更せず、まずstreamの生出力を確認します。

起動commandとenvironmentの比較

開発shellでは動くのにhostでは起動しないときは、server本体の不具合とは限りません。hostはUIの設定にあるcommandを直接起動するため、対話shellのPATHやshell profileを引き継がないことがあります。host設定から、実行ファイル、引数、working directory、環境変数名を読み、同じ条件で再現します。tokenやsecret値は比較・報告へ出さず、必要なvariableの有無と権限だけを確認します。

ログには次のような安全な情報を残せます。

  • server version、runtime version、選択transport。

  • commandの実行ファイルと、secretを含まない引数。

  • process exit codeと起動から終了までの時間。

  • stdoutの混入行番号・文字種・bytes数。ただし認証情報を含む生内容はマスク。

  • stderrのerror classと該当stack位置。

  • 使用protocol version、最初のmethod名、responseのmethod/idの一致。

  • Inspectorやhostで接続できたか、tools/list等の次段階へ進んだか。

修正後の再検証

修正を一度に何種類も混ぜず、原因を一つずつ変えて再実行します。

  1. stdoutに人間向けlogがあるなら、その呼び出しをstderrへ移す。

  2. serverをhostと同じcommand・environmentで再起動する。

  3. stdoutに不要な文字列、空行、壊れたJSON、framing違反がないことを確認する。

  4. 利用するprotocol versionに応じた最初のrequest/responseを確認する。

  5. MCP Inspector等、対象server commandを起動する公式の検査clientが利用できる環境で接続する。

  6. handshakeまたはdiscover後にserver情報・capabilities・toolsの一覧を確認する。

  7. 最小のtool callを一度実行し、期待するresultが返るか確認する。

  8. hostへ設定し直して同じ操作を行い、host固有のenvironment差がないか確認する。

Inspectorで成功してhostで失敗する場合は、host側のcommand、PATH、working directory、protocol version、environmentを比較します。両方で失敗する場合は、MCP specに対するstdout framingやserver実装を見ます。接続成功だけでtool resultの正しさまでPASSにせず、一覧と最小callを別に検証します。

secretを漏らさず診断する

起動ログを丸ごと共有する前に、Authorization header、API key、OAuth token、環境変数のsecret、利用者の入力・音声・ファイルpathなどを削除してください。stdout汚染調査では、問題の行を原文のまま公開する必要はありません。「JSON-RPC以外の先頭行あり」「行番号4、ASCII banner、42 bytes」など、構造と長さの情報だけで原因を追える場合があります。

stderrへ移すことは、ログを無制限に出してよいという意味ではありません。hostやSDKのログに機密値が含まれないか点検し、必要な情報だけを残します。MCPのprotocol loggingを使う場合は、clientがlogging capabilityを通知しているかなど、対応protocolの条件を確認します。

復旧判断フロー

  • processがすぐ終了する:stderr・exit code・command・runtime環境を確認する。

  • stdoutにprotocol外の行がある:出力元を特定し、stderrまたは対応するloggingへ移す。

  • stdoutが有効なmessageだけで、method/versionが違う:hostとserverの対応protocolと最初のrequest/responseをそろえる。

  • responseが返るがhostがtimeoutする:message framing、改行、process lifetime、stream接続先、hostの設定を確認する。

  • Inspectorは通るがhostでは失敗する:hostの起動環境、PATH、working directory、protocol versionを差分比較する。

  • 原因を説明できない:安全にredactした最小ログと再現条件を揃えて、使用SDKまたはhostの公式issue/supportへ相談する。追加のstdout debug行を足して調査しない。

結論:stdoutはprotocolとして扱う

stdio MCP serverでは、stdoutはユーザー向けログではなく、JSON-RPC/MCP messageを運ぶwireです。起動文字列やdebug logをstdoutへ出すと、hostが最初の行からprotocol messageとしてparseし、接続を失うことがあります。まず生streamをstdoutとstderrに分けて確認し、process起動、stdout汚染、framing、protocol version/handshakeを順に切り分けます。旧式initializeか現行のserver/discoverかは使用client・serverのversionで決め、予想だけでhandshakeを書き換えないでください。

Sources