nikkie-ftnextの日記

イベントレポートや読書メモを発信

mcp.run(transport="stdio") しているMCPサーバのスクリプトは、プロトコル仕様に沿ったJSONが入力されるのを待ち受けていたのか!

はじめに

七尾百合子さん、お誕生日 41日目 おめでとうございます! nikkieです。

みんなのPython勉強会 in 長野での学びを記事にします。
PythonMCPサーバ(とクライアント)を書いたときに私は誤解していました。
MCPサーバの標準入力に仕様に沿ったJSONを入力すれば、なんと人間がMCPクライアントとなってtool callできます!

目次

これまでの学習パス:MCPサーバを単体で実行する

MCPサーバとして mcp.run(transport="stdio") するPythonスクリプトを書きました。
MCPのquickstartに沿った、お天気MCPサーバです。

MCPサーバとクライアントのtransportがJSON-RPCと知って、JSONを送ることもやりました。

% echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"bash-client","version":"1.0.0"}}}' | uv run weather.py

このやり方でお天気MCPサーバからtoolの一覧を取得できています。
しかし、この記事の以下の部分は誤解とこのたび認識しました。

作る中で試しに、MCPサーバのスクリプトPython処理系で実行してみたのですが、何も起こりません。

% uv run weather.py  # Ctrl+C連打で抜けます

tools/callリクエストすると、シェルからJSONを送るやり方はエラーとなる

JSONを送る方法でよく分からなかった点が1つありました。
「toolの一覧が取れるのなら実行もできるのかな」と試してみると

% uv run weather.py <<EOF
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"bash-client","version":"1.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_alerts","arguments":{"state":"NY"}}}
EOF
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"weather","version":"1.6.0"}}}
[04/27/25 17:36:50] INFO     Processing request of type CallToolRequest                                                  server.py:534
                    INFO     HTTP Request: GET https://api.weather.gov/alerts/active/area/NY "HTTP/1.1 200 OK"         _client.py:1740
  + Exception Group Traceback (most recent call last):
  (略)
  |     raise ClosedResourceError
  | anyio.ClosedResourceError
  +------------------------------------

toolを呼び出すログは出力されているのですが、外部の天気APIへの通信中に出力が閉じてしまう1ようで、tool呼び出し結果を出力できないためにエラーとなります。
このことからMCPサーバのスクリプトの実行でできることは限定的と思っていたのですが、これは私がやり方を間違えていただけでした。

MCPサーバの標準入力にはtools/callリクエストを送れる!

MCPのtoolのプロトコルに沿ってtools/list, tools/callします

  1. Initialization
  2. Operation

まずはInitialization。

% .venv/bin/python weather.py 
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"bash-client","version":"1.0.0"}}}
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"weather","version":"1.6.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}

tools/listリクエス

{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
[04/27/25 17:47:41] INFO     Processing request of type ListToolsRequest                                                 server.py:534
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"get_alerts","description":"Get weather alerts for a US state.\n\n    Args:\n        state: Two-letter US state code (e.g. CA, NY)\n    ","inputSchema":{"properties":{"state":{"title":"State","type":"string"}},"required":["state"],"title":"get_alertsArguments","type":"object"}},{"name":"get_forecasts","description":"Get weather forecasts for a location.\n\n    Args:\n        latitude: Latitude of the location\n        longitude: Longitude of the location\n    ","inputSchema":{"properties":{"latitude":{"title":"Latitude","type":"number"},"longitude":{"title":"Longitude","type":"number"}},"required":["latitude","longitude"],"title":"get_forecastsArguments","type":"object"}}]}}

tools/callリクエス

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_alerts","arguments":{"state":"NY"}}}
[04/27/25 17:48:12] INFO     Processing request of type CallToolRequest                                                  server.py:534
[04/27/25 17:48:13] INFO     HTTP Request: GET https://api.weather.gov/alerts/active/area/NY "HTTP/1.1 200 OK"         _client.py:1740
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"No active alerts for this state."}],"isError":false}}

エラーにならずに呼べたあああああ!

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_alerts","arguments":{"state":"CA"}}}

NYをCAに変えても呼べて、こちらは警告が返ってきました(レスポンスは省略します)

ソースコードはこちらです

mcp.run(transport="stdio")が標準入力を受け取れるのは、どんな実装による?

DeepWikiの力を借りて、少し見てみました。

mcpFastMCPインスタンスです。
mcp.run(transport="stdio")は、anyio.run(self.run_stdio_async)を呼び出しています。
https://github.com/modelcontextprotocol/python-sdk/blob/v1.6.0/src/mcp/server/fastmcp/server.py#L158-L159

FastMCPクラスのrun_stdio_asyncメソッドは、非同期コンテキストマネージャmcp.server.stdio.stdio_serverを使います。
https://github.com/modelcontextprotocol/python-sdk/blob/v1.6.0/src/mcp/server/fastmcp/server.py#L458

async def run_stdio_async(self) -> None:
    async with stdio_server() as (read_stream, write_stream):
        # 省略

stdio_serverでは標準入力(sys.stdin)を扱っており
https://github.com/modelcontextprotocol/python-sdk/blob/v1.6.0/src/mcp/server/stdio.py#L33
上の例で送ったJSONはここで処理されたんだなと認識しました。

anyioのcreate_memory_object_stream()create_task_group()は、もっと理解を深められる余地を残しています。

終わりに

Python SDKで作るMCPサーバを よ う や く 完全に理解したと思います!
MCPサーバをスクリプトとして実行し、プロトコルに沿ったJSONを送って動作確認できるので、Inspector2とは別の動作確認の選択肢を持てました🙌