はじめに
七尾百合子さん、お誕生日 41日目 おめでとうございます! nikkieです。
みんなのPython勉強会 in 長野での学びを記事にします。
PythonでMCPサーバ(とクライアント)を書いたときに私は誤解していました。
MCPサーバの標準入力に仕様に沿ったJSONを入力すれば、なんと人間がMCPクライアントとなってtool callできます!
目次
- はじめに
- 目次
- これまでの学習パス:MCPサーバを単体で実行する
- tools/callリクエストすると、シェルからJSONを送るやり方はエラーとなる
- MCPサーバの標準入力にはtools/callリクエストを送れる!
- mcp.run(transport="stdio")が標準入力を受け取れるのは、どんな実装による?
- 終わりに
これまでの学習パス: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します
- Initialization
- ref: https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle
- 送るものは2つ
- initialize request
- (initialize responseを受けて) initialized notification
- 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の力を借りて、少し見てみました。
mcpはFastMCPインスタンスです。
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とは別の動作確認の選択肢を持てました🙌
#stapy #glnagano ああ〜完全に誤解してた。
— nikkie(にっきー) / にっP (@ftnext) 2025年4月26日
MCPサーバのスクリプトを実行したら、そこでSTDIO transportできたんだ!!
この記事のLLMとコード読んで見出した方法は遠回りだった〜〜https://t.co/gigiieNkA2
- Claude Codeと一緒に読みました https://gist.github.com/ftnext/a603aef8953dbe353600d7325c62f153↩
- ↩