0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCP stdioサーバーで logging をstdoutに向けるとJSON-RPCが壊れる

0
Posted at

はじめに

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.basicConfigstreamsys.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.basicConfigstreamsys.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 を指定する
  • エラー発生時もツール呼び出し自体は成功するケースがあり、サイレントな障害になりやすい点に注意が必要

参考文献

  1. MCP Specification – Transports(stdioの仕様。MUSTによるstdout制約・stderrのログ用途が定義されている) エラーの原因・解決方法のセクションで参照
  2. Model Context Protocol – Python SDK(本記事のサーバー実装に使用) ← 実行環境・コード全般で参照
  3. Python logging — Logging facility for Python(basicConfig のstream引数の挙動) ← エラーの原因のセクションで参照
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?