powered by TechFeed
表示モード
ハウツー

PythonでMCPサーバーを作る — セッション不要のステートレス新仕様で、任意のワーカーが任意のリクエストを処理できる

10月9日、KDNuggetsが「Build Your First MCP Server in Python (Stateless Spec Edition)」と題した記事を公開した。2026-07-28仕様で刷新されたステートレスMCPプロトコルに準拠したPython製MCPサーバーの実装方法を、コードを交えて詳しく紹介している。

10月9日、KDNuggetsが「Build Your First MCP Server in Python (Stateless Spec Edition)」と題した記事を公開した。2026-07-28仕様で刷新されたステートレスMCPプロトコルに準拠したPython製MCPサーバーの実装方法を、コードを交えて詳しく紹介している。


MCPとは何か、なぜ今注目されているか

Model Context Protocol(MCP)は、AIモデル(LLM)と外部ツール・データソースを接続するためのオープンプロトコルだ。Anthropicが主導して策定し、Claude・ChatGPT・Geminiなど主要なAIホストアプリが対応を進めている。

MCPが注目される理由は、AIエージェントがどのように外部機能を呼び出すかを標準化する点にある。MCPがなければ、各AIサービスがそれぞれ独自の連携方法を定義することになり、ツール側の実装コストが跳ね上がる。MCPはそのグルーコードを共通化し、一度実装したサーバーをあらゆるMCP対応ホストから呼び出せるようにする。


MCPがステートレスになった、その意味

2026-07-28仕様によって、プロトコルのコアがステートレスに変わった。これは2026年夏における重要な仕様変更だ。

旧来の仕様では、クライアントはリクエストの前にセッションを確立し、Mcp-Session-Idをやり取りする必要があった。

Client
  |
  | initialize
  v
Server
  |
  | Mcp-Session-Id
  v
Client
  |
  | later request + session id
  v
Same logical session

この設計はスティッキールーティング(同一クライアントを同一サーバーに固定する仕組み)や共有セッション基盤を必要とし、スケールアウトの障壁になっていた。

新仕様では、各リクエストが自己完結する。

Request 1 --> Server A
Request 2 --> Server C
Request 3 --> Server B

セッションがなければ、任意のワーカーが任意のリクエストを処理できる。 uvicorn server:app --workers 4 の一行でマルチワーカー展開が素直に機能するようになった。

同時期に公式Python SDKもv2に移行し、MCPServerという高レベルAPIが提供されるようになっている。


何を作るか

チュートリアルで構築するのは開発者向けナレッジベースサーバーだ。MCPの3つのプリミティブ(基本要素)を実装する。

Tool:     search_kb(query, limit)
Resource: kb://articles
Prompt:   draft_support_reply(customer_message)
  • Tool:モデルが能動的に呼び出す関数。検索・計算・外部API呼び出しなど副作用を伴う操作に向く
  • Resource:ホストアプリがコンテキストに読み込めるデータを公開する。GETに近い受動的な読み取りに相当する
  • Prompt:再利用可能なプロンプトテンプレートを公開する。モデルではなくユーザーやホストが起点となる

サーバーはユーザーセッション状態を一切持たない。ステートレスMCPモデルの典型例として機能する。


セットアップ

Python 3.10以上が必要。uvを使う場合:

mkdir first-mcp-server
cd first-mcp-server
uv init
uv add "mcp[cli]"

pipの場合:

pip install "mcp[cli]"

必要なファイルは server.py と client.py の2つだけだ。


サーバー実装のポイント:型ヒントがそのままスキーマになる

記事で最も実用的な部分が、ツール定義の書き方だ。

from mcp.server import MCPServer

mcp = MCPServer(
    "Developer Support KB",
    instructions=(
        "Use the knowledge-base tools to answer support questions. "
        "Prefer retrieved KB information over guessing."
    ),
)

@mcp.tool()
def search_kb(query: str, limit: int = 3) -> list[dict[str, str]]:
    """Search the support knowledge base.
    Args:
        query: Words or phrases to search for.
        limit: Maximum number of articles to return.
    """
    query = query.lower()
    matches = []
    for article in ARTICLES:
        searchable_text = (article["title"] + " " + article["body"]).lower()
        if query in searchable_text:
            matches.append(article)
    return matches[:limit]

JSONスキーマを手書きする必要がない。 SDKがPython関数の型ヒントからMCPのinputSchemaを自動生成する。limit: int = 3 のようなデフォルト値は、生成されるスキーマでそのままオプション引数になる。

関数シグネチャがそのままインターフェース定義になるこの設計は、開発体験として素直だ。


Resource・Promptの追加

Resourceはホストアプリがコンテキストに読み込めるデータを公開する。ツールがモデルから能動的に呼ばれるのに対し、リソースはGETに近い受動的な読み取りに相当する。

@mcp.resource("kb://articles")
def list_articles() -> str:
    """Return the available knowledge-base articles."""
    return "\n".join(
        f"{article['id']}: {article['title']}"
        for article in ARTICLES
    )

Promptは再利用可能なプロンプトテンプレートを公開する。モデルが自律的に呼び出すのではなく、ユーザーやホストが起点となる。

@mcp.prompt()
def draft_support_reply(customer_message: str) -> str:
    """Create a prompt for drafting a concise support response."""
    return f"""
You are a technical support assistant.
Write a concise and helpful response to this customer message:
{customer_message}
Use the support knowledge base when relevant.
Do not invent product policies.
""".strip()

起動とHTTP展開

開発時はMCP Inspectorつきで起動できる。

uv run mcp dev server.py

Inspector UIからツール一覧の確認や呼び出しテストが行える。

本番向けHTTPサーバーとして起動する場合:

if __name__ == "__main__":
    mcp.run("streamable-http")
uv run python server.py

デフォルトのエンドポイントは http://127.0.0.1:8000/mcp だ。FastAPIやStarletteと組み合わせる場合は、ASGIアプリとして取り出せる。

app = mcp.streamable_http_app()

Uvicornで起動:

uvicorn server:app

Pythonクライアントで動作確認

import asyncio
from mcp import Client

async def main() -> None:
    async with Client("http://127.0.0.1:8000/mcp") as client:
        print("Protocol:", client.protocol_version)
        tools = await client.list_tools()
        for tool in tools.tools:
            print("-", tool.name)
        result = await client.call_tool(
            "search_kb", {"query": "rate limit", "limit": 2}
        )
        print(result.structured_content or result.content)

asyncio.run(main())

v2のClientはHTTP URLを直接受け取り、自動的にStreamable HTTPを使う。実行結果:

Protocol: 2026-07-28
Available tools:
- search_kb
  {'result': [{'id': 'api-rate-limit', 'title': 'Understanding API rate limits', ...}]}

アプリケーション状態が必要な場合は?

「ステートレスプロトコル」はアプリケーションが状態を持てないという意味ではない。MCP自体がプロトコルセッション内に状態を隠さなくなったということだ。

アプリケーションレベルの状態が必要であれば、識別子をツールの引数として明示的にやり取りするパターンをMCPメンテナーが推奨している。

@mcp.tool()
def create_basket() -> dict[str, str]:
    basket_id = create_new_basket()
    return {"basket_id": basket_id}

@mcp.tool()
def add_item(basket_id: str, product_id: str) -> dict:
    return add_product(basket_id, product_id)
旧: プロトコルが状態を保持する
新: アプリケーションが状態を保持し、識別子を明示的に受け渡す

詳細はBuild Your First MCP Server in Python (Stateless Spec Edition)を参照していただきたい。