CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/Claude Code
Claude Code/2026-04-24中級

自作 MCP サーバーのログが消える — Claude Code で stderr を正しく拾う

Claude Code で自作の MCP サーバーを書いていて、console.log を足したのにログがどこにも出ない。実はサーバー自体が応答しなくなっている、という落とし穴の正体と、stdio を汚染しない正しいログ出力の置き換え方をまとめました。

claude-code129mcp18troubleshooting61loggingstdio

MCP サーバーを自作していて最初に詰まるのは、エラーメッセージではなく「何も起きない」状態ではないでしょうか。console.log("reached here") を仕込んでもターミナルに何も出てこありません。Claude Code 側では「ツールが応答しません」と表示されるだけで、どこが悪いのかまったく手がかりがありません。この無言の失敗は、ほぼ例外なく stdio 汚染 が原因です。

stdio トランスポートの MCP サーバーは、標準入出力そのものを JSON-RPC のチャネルとして使います。つまり、あなたの console.log が書き込んでいる「ログ」は、Claude Code から見ると「壊れた JSON レスポンス」になります。パーサはメッセージを破棄し、ハンドシェイクが失敗し、サーバーは接続されたまま静かに沈黙する—これが実際に起きていることです。

まず現象を確認する

ログを直す前に、自分のサーバーが本当に stdio 経由で動いているのか確認します。Claude Code の設定(~/.claude.json のプロジェクトセクション、または .mcp.json)を開いて、以下のような形になっていたら stdio モードです。

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["./mcp/server.js"]
    }
  }
}

type: "http"url が指定されていればネットワーク経由なので、この記事の内容は当てはまりません。stdio の場合のみ、stdout への書き込みがプロトコルと衝突します。

次に /mcp コマンドか claude mcp list でサーバーの状態を見ます。failed ではなく connected なのに反応しない、というのが典型的な症状です。サーバー側がクラッシュしたら failed と出るので、ここで connected のままなのは「プロセスは生きているが通信が成立していない」サインとして読んでください。

原因: stdout は MCP の通信路である

Node.js の console.log は内部的に process.stdout.write を呼びます。Python の print も同じで、デフォルトの送り先は stdout です。MCP の stdio トランスポートが使うのはまさにここで、1 行 1 メッセージの JSON-RPC をやりとりしています。

つまり、あなたが書いた console.log("user id:", userId) は、Claude Code のパーサから見ると次のようなノイズです。

user id: 42
{"jsonrpc":"2.0","result":{...},"id":1}

最初の行は JSON として不正なので、パーサはエラーを出すか、実装によってはその行以降をすべて捨てます。結果として、正しく返したはずのツール結果までまとめて失われます。ログを書いた「つもり」で、実際にはプロトコルを壊している—これが一番多いパターンです。

正しい書き換え: stderr に流す

標準エラー出力(stderr)は MCP プロトコルが使わないチャネルなので、ここに書く分にはいくらログを吐いても問題ありません。Claude Code のログビューアは、サーバープロセスの stderr をキャプチャして保存してくれます。

Node.js の場合は単純に console.error に置き換えます。

// ❌ サーバーを黙らせる
console.log("Tool called:", toolName);
 
// ✅ stderr に流す
console.error("Tool called:", toolName);
 
// ✅ もっと明示的に書きたい場合
process.stderr.write(`[mcp] tool=${toolName}\n`);

Python の場合は sys.stderr に file 引数で流します。

import sys
 
# ❌ stdout を汚す
print(f"Tool called: {tool_name}")
 
# ✅ stderr に流す
print(f"Tool called: {tool_name}", file=sys.stderr, flush=True)

flush=True を付けておくのは、バッファリングで終了直前のログが消えるのを避けるためです。クラッシュ時こそログが欲しいのに、バッファに溜まったまま失われると原因が追えなくなります。

ロガーライブラリは要注意

Winston、pino、Bunyan、loguru といったロガーは、設定しない限りデフォルトで stdout に書きます。「console.log は書いていないから大丈夫」と油断していると、実はロガーが静かにプロトコルを壊していることがあります。

// pino の例: stderr に向ける
import pino from "pino";
const logger = pino({}, pino.destination(2)); // 2 = stderr
logger.info("server started"); // ← 安全
# loguru の例: sink を stderr にする
from loguru import logger
import sys
 
logger.remove()          # デフォルトの sink を外す
logger.add(sys.stderr)   # 明示的に stderr を指定
logger.info("server started")

私は以前、pino のデフォルト設定のまま本番相当の MCP サーバーを配布してしまい、「ローカルでは動くのに配った先で全員壊れる」という報告を受けたことがあります。ロガーの出力先は、サーバーを公開する前に必ず確認しておきたいポイントです。

Claude Code 側でのログの見方

stderr に流し始めたら、今度は Claude Code 側でそのログを探します。macOS / Linux なら次のパスに保存されています。

# Claude Code の MCP サーバーログ
ls -lt ~/Library/Logs/Claude/mcp-server-*.log  # macOS
ls -lt ~/.cache/claude/logs/mcp-server-*.log   # Linux の代表例

ファイル名にサーバー名が含まれているので、自作サーバーに対応するものを tail -f で追いかけると、リクエストと自分のログが時系列で流れてくるのが見えます。ここまで来れば、あとは通常のサーバーデバッグと変わりません。

開発中に手元で早く確認したいときは、Claude Code から起動するのではなく、ターミナルで直接サーバーを起動し、手でリクエスト JSON を流すのが一番早いです。

# 手動で initialize リクエストを流す
echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}},"id":1}' | node ./mcp/server.js

この実行では stdout に JSON-RPC レスポンス、stderr にあなたのログが出るので、どちらが何を返しているかをターミナル上で直接切り分けられます。

落とし穴と回避策

実際に踏んだものを具体的に挙げておきます。

1. サードパーティ SDK が裏で stdout に書く OpenTelemetry の自動計測や一部の DB ドライバは、初期化時に起動バナーを stdout に出すことがあります。疑わしい場合は、サーバー起動の最初の行で process.stdout.write をフックし、予期しない書き込みを stderr にリダイレクトする手が使えます。

2. unhandledRejectionuncaughtException の出力先 Node.js のデフォルトはエラーを stderr に出しますが、一部のフレームワークはこれを stdout に付け替えます。サーバーのエントリポイントに process.on("uncaughtException", err => console.error(err)) を明示的に書いておくと確実です。

3. 本番では stderr も捨てたい気持ちになる パフォーマンスを気にして stderr をゼロにしたくなりますが、問題発生時の診断材料がなくなります。ログレベルを環境変数で切り替える設計(MCP_LOG_LEVEL=error)にして、stderr は常に生かしておくほうが後で助かります。

30秒でできる切り分けレシピ

コードを触る前に、本当に stdout が原因かを確かめる方法を紹介します。ターミナルから手元で initialize リクエストを投げ、stdout と stderr を別ファイルに分けて保存するだけです。

echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0.1"}},"id":1}' \
  | node ./mcp/server.js \
  > /tmp/mcp.stdout 2> /tmp/mcp.stderr

/tmp/mcp.stdout には整形済みの JSON が 1 件だけ入っているはずです。バナー文字列や複数行のテキストが混ざっていたら、それが汚染源です。/tmp/mcp.stderr にはログが何行あっても構いません。コードを読み直すより、このコマンド一発で切り分けたほうが早いことが多いので、覚えておいて損はありません。

非同期・子プロセスの落とし穴

ハンドラの中で非同期に子プロセスを起動する場合、その子の stdout が親の stdout に漏れる事故があります。MCP サーバーとしては致命的です。

// ❌ 子プロセスの stdout を継承してしまう(= MCP の通信路に流れ込む)
const child = spawn("convert", args, { stdio: "inherit" });
 
// ✅ stdout だけは切り離す。stderr はログとして受け取ってよい
const child = spawn("convert", args, {
  stdio: ["ignore", "ignore", "inherit"],
});

Python の subprocess.run(..., stdout=subprocess.PIPE) はデフォルトで安全ですが、レガシーなラッパーが stdio=inherit 相当になっているケースは普通にあります。シェルアウトするツールハンドラを書いたら、自分のロガーの次に子プロセスの stdio 設定を疑うのが順序として正解です。

ストーム前に CI で防ぐ

本番で事故るより、CI で 1 行追加して防ぐほうが安上がりです。initialize リクエストを流して、stdout が「1 行・JSON のみ」であることを確認するだけの軽い smoke test で十分機能します。

import { spawn } from "child_process";
 
const p = spawn("node", ["./mcp/server.js"]);
p.stdin.write(JSON.stringify({
  jsonrpc: "2.0", method: "initialize",
  params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "t", version: "0.1" } },
  id: 1,
}) + "\n");
 
let out = "";
p.stdout.on("data", (c) => (out += c));
setTimeout(() => {
  const lines = out.trim().split("\n");
  if (lines.length !== 1) throw new Error("stdout が汚染されています");
  JSON.parse(lines[0]);
  p.kill();
}, 500);

このテストは決定的で、発生後に再現させるのが一番つらい症状を、マージ前に捕まえてくれます。

まず今日やること

自作 MCP サーバーを持っているなら、エディタで開いて console.logprint( を検索してください。見つかったものは全部 console.errorprint(..., file=sys.stderr) に置き換える。ロガーを使っているなら destination が stderr になっているかを確認します。これだけで「なぜか動かない」系の症状の大半は消えます。残った問題は、stderr に出るログを見ながら切り分けられる、通常のサーバーデバッグの話に変わります。

MCP まわりで詰まったら、Claude Code MCP 接続エラー完全解決ガイド と Claude Code の /doctor コマンドで設定トラブルを3分で自己診断する もあわせて参考になると思います。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-05-05
Claude Code MCP 接続トラブル完全診断ガイド
Claude Code の MCP サーバーが繋がらない・途中で切れる・ツールが見つからない問題を根本から診断する。設定ファイルの構造から通信ログの読み方、よくある落とし穴まで、実際のエラーパターンをもとに体系的に解説します。
Claude Code2026-04-24
Claude Code エラー完全対処辞典 — 認証・決済・応答停止・ツール失敗を症状から逆引きする
Claude Code で実際に遭遇する 40 超のエラーパターンを、症状 → 原因 → 復旧手順の順で整理した実務向けハンドブックです。認証・決済・応答停止・ツール失敗・MCP・Hook まで、根本原因まで追える資料として活用いただけます。
Claude Code2026-07-17
テーブルが「… 2,847 more rows」で切れていた朝 — 描画上限とトークン計上を切り離して、ツール出力を設計し直す
Claude Code 2.1.209 で 200 行超の markdown テーブルは先頭 200 行+残り件数の表示になりました。省略されるのは描画だけで、モデルは全行を受け取ります。この差を測り、ツール出力を集約型へ組み替える設計をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →