はじめに
MCP(Model Context Protocol)のstdioトランスポートを使用するサーバーを実装する際、Pythonの logging モジュールの出力先を誤って設定するとログが正常に動作しなくなる場合がありました。 本記事ではその解決方法を説明しています。
伊藤先生の技術記事を書く技術を参考に初めての技術ブログを記載しています。誤りなどございましたらコメントお待ちしております!
図書:https://www.shoeisha.co.jp/book/detail/9784798177045
実行環境
| 項目 | バージョン |
|---|---|
| OS | Windows 11 |
| Python | 3.12 |
| uv | 0.6.x |
| mcp | 1.x |
発生したエラー
以下のように logging.basicConfig の stream に sys.stdout を指定してMCPサーバーを実装しました。
# server_bad.py
import asyncio
import logging
import sys
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
logging.basicConfig(
stream=sys.stdout, # ← 問題箇所
level=logging.DEBUG,
format="%(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
app = Server("bad-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
logger.debug("list_tools via logging")
return [
types.Tool(
name="hello",
description="Say hello",
inputSchema={"type": "object", "properties": {}},
)
]
async def main():
logger.info("server starting...")
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
asyncio.run(main())
クライアント側で list_tools() を呼び出すと、以下のエラーが大量に発生します。
Failed to parse JSONRPC message from server
pydantic_core.ValidationError: 1 validation error for JSONRPCMessage
Invalid JSON: expected value at line 1 column 1
[input_value='DEBUG: Initializing server bad-server\r']
Failed to parse JSONRPC message from server
pydantic_core.ValidationError: 1 validation error for JSONRPCMessage
Invalid JSON: expected value at line 1 column 1
[input_value='DEBUG: list_tools via logging\r']
...(以降も同様のエラーが続く)
注意点として、最終的なツール取得自体は成功(✅ Success! Tools: ['hello'])するため、エラーに気づかない場合がありそうだと感じました。
エラーの原因
MCPのstdioトランスポートは、クライアント・サーバー間の通信にstdoutをJSON-RPCメッセージの専用チャンネルとして使用しています。公式仕様 (https://modelcontextprotocol.io/specification/2025-06-18/basic/transports) には "The server MUST NOT write anything to its stdout that is not a valid MCP message" と明記されている。クライアントはサーバーのstdoutから受け取った全行を JSONRPCMessage としてパースしようとするため、JSON以外のテキストが1行でも混入した時点でパースエラーが発生していると感がられます。
【stdoutに流れ込むもの】
logging (stream=sys.stdout)
├─ ユーザーコードのログ "DEBUG: list_tools via logging"
├─ SDKの内部ログ "DEBUG: Initializing server 'bad-server'"
└─ asyncioのシステムログ "DEBUG: Using proactor: IocpProactor"
↓ クライアントがJSONとしてパースしようとする
pydantic_core.ValidationError: Invalid JSON ×(メッセージ数分)
print() が問題にならない理由
print() はデフォルトで sys.stdout オブジェクトを参照する。しかしstdio_server() はコンテキスト開始時に sys.stdout を差し替えるため、print() の呼び出しは自動的に差し替え後の出力先(内部的にはstderr相当)へリダイレクトされる。そのため print() で出力することは問題になりませんでした。
| 出力方法 | stdoutオブジェクトの参照 | SDKのリダイレクト後 |
|---|---|---|
print() |
動的に sys.stdout を参照 |
✅ リダイレクト先に追従 |
logging(stream=sys.stdout) |
ファイルオブジェクトを固定 | ❌ 元のstdoutに書き続ける |
解決方法
logging.basicConfig の stream を sys.stderr に変更するだけで解決しました。公式仕様 (https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)では "The server MAY write UTF-8 strings to its standard error (stderr) for logging purposes" とされています。したがってstderrをログ出力に使用することが公式に認められています。
# server_good.py
import asyncio
import logging
import sys
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
logging.basicConfig(
stream=sys.stderr, # ✅ stderrに変更
level=logging.DEBUG,
format="%(levelname)s: %(message)s",
)
logger = logging.getLogger(__name__)
app = Server("good-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
logger.debug("list_tools via logging") # ✅ stderrに出力されるため無害
return [
types.Tool(
name="hello",
description="Say hello",
inputSchema={"type": "object", "properties": {}},
)
]
async def main():
logger.info("server starting...")
async with stdio_server() as (read, write):
await app.run(read, write, app.create_initialization_options())
asyncio.run(main())
| 出力先 | JSON-RPC への影響 | ログ用途 |
|---|---|---|
stdout (sys.stdout) |
❌ パースエラー発生 | 使用禁止 |
stderr (sys.stderr) |
✅ 影響なし | 推奨 |
まとめ
- MCPのstdioトランスポートはstdoutをJSON-RPCの専用チャンネルとして使用する(公式仕様で MUST NOT と規定)
-
logging.basicConfig(stream=sys.stdout)はstdio_server()のリダイレクトより前にstdoutを捕捉するため、SDKの保護をバイパスしてしまう -
print()はSDKのリダイレクトに追従するため問題にならないが、意図が不明瞭になるため非推奨 - ログの出力先は必ず
stream=sys.stderrを指定する - エラー発生時もツール呼び出し自体は成功するケースがあり、サイレントな障害になりやすい点に注意が必要
参考文献
- MCP Specification – Transports(stdioの仕様。MUSTによるstdout制約・stderrのログ用途が定義されている) エラーの原因・解決方法のセクションで参照
- Model Context Protocol – Python SDK(本記事のサーバー実装に使用) ← 実行環境・コード全般で参照
-
Python
logging— Logging facility for Python(basicConfigのstream引数の挙動) ← エラーの原因のセクションで参照