nikkie-ftnextの日記

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

Model Context Protocol 素振りの記:Quickstartのサーバとクライアントでtransportをstdioからsseに変更する

はじめに

長崎、いいね! 暖房入ってるね👍 nikkieです

今回もMCPの素振りです。
私はClaude Desktopを使う気がなく1、クライアントスクリプトにサーバのスクリプトを渡さない方法を探しました。

目次

前回のMCP! QuickstartをClaude、GPT、Geminiで動かす

MCPのドキュメントのQuickstartに沿って、Pythonでサーバとクライアントを実装しました。

  • サーバ
    • 天気の警報取得と天気予報取得の2つのtoolを提供
  • クライアント
    • Claude、GPT、Geminiにtoolsを渡して動かせています!

動かし方は uv run client.py ../weather/weather.py です。
サーバのスクリプトをクライアントに渡しています

最初の素振りの記事(上の2つのうち1つ目)にて

また、「MCPを採用して構築したアプリケーションってサーバのスクリプトを渡してはいないのでは?」という思いがあり、スクリプトを渡す以外にサーバを立ててHTTP通信する方法もあるのかも疑問に思っています

と書きました。
この記事ではスクリプトを渡す以外の動かし方を模索します。
結論としては、SSEなるもの2が提供されていました

MCPサーバとクライアントをSSEで動かす

2種類のtransport

modelcontextprotocol.io

このドキュメントの中のBuilt-in Transport Typesより

MCP includes two standard transport implementations:

  • stdio (Standard Input/Output)
    • これまでの素振りで採用していた方法です(MCPサーバのスクリプトをクライアントに渡す)
    • クライアントはサーバのスクリプトを実行するようです
      • python
      • node3
      • uv run4
  • SSE (Server-Sent Events)
    • この記事で扱う方法です
    • 文字通りサーバとクライアントです(スクリプトを渡すというようなことはしません)
      • サーバからクライアントにstreamingな通信

QuickstartのMCPサーバとクライアントを、SSEをサポートするように書き換えていきます。

SSEをサポートしたMCPサーバ

実はめちゃめちゃ簡単です。

if __name__ == "__main__":
-    mcp.run(transport="stdio")
+    mcp.run(transport="sse")

FastMCPインスタンスrunメソッドのtransport引数の値を変えるだけです。
このサーバのスクリプトuv runすると

% uv run weather.py 
INFO:     Started server process [72290]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

uvicornが立ち上がるんですね5!!

実装を覗くと
https://github.com/modelcontextprotocol/python-sdk/blob/v1.3.0/src/mcp/server/fastmcp/server.py#L476-L508

この実装によってuvicornが立ち上がったのでした。
これでMCPサーバのSSEサポートは完了です。
uv runでサーバを立ち上げておきます。

SSEをサポートしたMCPクライアント

Server-Sent Events (SSE)の「Python (Client)」の実装を参考にすると、以下のようになります

from mcp import ClientSession
from mcp.client.sse import sse_client


async def main():
    async with sse_client("http://localhost:8000/sse") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            await session.initialize()

            tools = await session.list_tools()
            print(tools)


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

このスクリプトuv runすると、ツールの一覧が取得できます!

Quickstartのclientと同様の実装に落とし込みます。
この機にcontextlib.AsyncExitStackのドキュメントを見ましたが、なかなか興味深かったです。
(非同期)コンテキストマネージャのstack、内部的にはwith文のネストなんですね

動作確認

gemini-2.0-flashとgpt-4oで動作確認しました(今回Claudeは省略)。
SSEでも動きます!

gemini-2.0-flashの例(uv run client.py

MCPサーバのログ(SSEの様子)はこちら
https://gist.github.com/ftnext/d8276f058b4e8c4d0b76fc0d9743fc0a

終わりに

SSEでMCPサーバとクライアントを動かせました!🙌

stdio transportでサーバのスクリプトを渡していたときと比べて、組合せやすくなるのではと期待しています。
SSEをサポートしさえすれば、サーバの実装の詳細は考えなくてよくなっています。
MCPサーバをDockerコンテナで立てるようなこともできるのではないでしょうか(要検証)

なおSSEで立てたMCPサーバですがCtrl+C 1回では終了せず、force quitになっちゃってます。
私なにか見落としているかもしれません

INFO: Waiting for background tasks to complete. (CTRL+C to force quit)

ソースコードの全容はこちらです


  1. だって、LLMもMCPサーバも任意の組合せができるんですよ! Claudeだけに固執することないですよ
  2. 出会いは初めてではなく、GitHub Copilot Extensionsにて出会っていました
  3. command = "python" if is_python else "node" ref:https://modelcontextprotocol.io/quickstart/client
  4. https://github.com/modelcontextprotocol/python-sdk/tree/main/examples/servers/simple-tool#example
  5. mcpパッケージはuvicornに依存しています https://github.com/modelcontextprotocol/python-sdk/blob/v1.3.0/pyproject.toml#L32