Start Debugging

公式 SDK を使って Python でカスタム MCP サーバーを構築する方法

公式の mcp 1.27 SDK と FastMCP を使って Python で動作する Model Context Protocol サーバーを構築します。Pydantic スキーマ、stdio の stdout 落とし穴、mcp dev / mcp install、Claude Desktop と Claude Code への登録までカバーします。

Python のエコシステムには「自分のエージェントに使わせたいもの」のカタログが最も深く揃っています。SQLAlchemy ORM、pandas の DataFrame、scikit-learn のパイプライン、AWS の boto3 クライアント、データ チームがすでに書いた内製スクリプトなど。これらを Model Context Protocol サーバーとしてラップするには、公式 SDK で 30 行で済みます。結果は Claude Desktop、Claude Code、Cursor、そして MCP 仕様を話すあらゆるクライアントから呼び出し可能です。

本ガイドは、Python 3.10 以上の上で mcp 1.27.0 SDK (2026 年 4 月リリース) を用い、高水準 API として FastMCP を使って、実際に動く Python の MCP サーバーを構築します。最後には、SQLite データベースをエージェントに 3 つのツールで公開する db-mcp サーバーが手に入ります。きちんとした Pydantic スキーマ、エラー処理、そしてドキュメントが軽く触れるだけで毎日使うことになる 2 つのデバッグ コマンド (mcp devmcp install) 付きです。

こうしたサーバーで Python が正解になる理由

TypeScript SDK は問題ありません。C# SDK も問題ありません。しかし公開したいシステムが既に Python スクリプト、FastAPI アプリ、ノートブックのエクスポートなのであれば、別言語に書き直して MCP をネジ止めするのは無駄な作業です。Python SDK なら既存関数の上に @mcp.tool() を載せて出荷できます。

Python が決定的に勝つ具体的なケースは 2 つ:

公式 SDK は modelcontextprotocol/python-sdk にあります。FastMCP 1.0 は 2024 年末にこの公式 SDK にマージされている 点に注意してください。PyPI には別途、サードパーティの fastmcp パッケージ (現時点で 3.x) が存在しますが、これは別プロジェクトです。新規コードでは公式の mcp パッケージを優先し、mcp.server.fastmcp から FastMCP をインポートしてください。両者を混ぜると、微妙なインポート エラーやバージョンずれにつながります。

uv を使ったプロジェクト セットアップ

Python 3.10 以上が必要です。1.27 SDK は 3.10 から 3.13 までをサポートします。SDK のドキュメントが推奨するパッケージ マネージャーは uv です。mcp installmcp dev コマンドの土台になっているからですが、インストール ステップ自体は pip でも動きます。

# Python 3.10+, uv 0.5+
mkdir db-mcp
cd db-mcp
uv init
uv add "mcp[cli]"

[cli] エキストラが、mcp devmcp install を提供する mcp コマンド ライン ツールを引き入れます。これがないとサーバー自体は実行できますが、インスペクターと Claude Desktop の登録コマンドは存在しません。

ソース ファイルを作成します:

mkdir src
touch src/server.py

例にクエリするデータがあるよう、SQLite シード スクリプト (seed.py) を追加します。これはデモのためだけのもので、サーバーの一部ではありません:

# seed.py -- creates a sample SQLite DB for the MCP server to expose
import sqlite3

conn = sqlite3.connect("inventory.db")
cur = conn.cursor()
cur.executescript("""
CREATE TABLE IF NOT EXISTS products (
  id INTEGER PRIMARY KEY,
  sku TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL,
  stock INTEGER NOT NULL DEFAULT 0
);
INSERT OR IGNORE INTO products (sku, name, stock) VALUES
  ('SKU-001', 'Mechanical keyboard', 12),
  ('SKU-002', 'Trackball mouse', 0),
  ('SKU-003', 'USB-C dock', 4);
""")
conn.commit()
conn.close()

python seed.py を一度実行します。MCP サーバーはこのファイルを読み取り専用で読みます。

すべての Python stdio サーバーを壊す stdout の落とし穴

ツール ハンドラーを 1 行書く前に、これを叩き込んでください: stdio MCP サーバーでは絶対に stdout に出力しないこと

stdio MCP サーバーが起動すると、クライアント (Claude Desktop、Claude Code、Cursor) は stdinstdout を通じて、行区切りの JSON-RPC でやり取りします。stdout に書いた、有効な JSON-RPC メッセージでないバイトはストリームを破壊します。クライアントは「MCP server disconnected」や「failed to parse response」といった一般的なエラーを記録して諦めます。

Python では犯人は、見方さえ知っていれば明白です:

# mcp 1.27.0, stdio transport

# Bad -- corrupts the JSON-RPC stream
print("Loaded 47 rows from inventory.db")

# Bad -- logging.basicConfig() defaults to stderr in modern Python,
# but if you reroute it to stdout you have the same problem
import logging
logging.basicConfig(stream=sys.stdout)  # do not do this

# Good -- write diagnostics to stderr
import sys
print("Loaded 47 rows from inventory.db", file=sys.stderr)

# Good -- the standard logging module defaults to stderr
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("db-mcp")
log.info("Loaded 47 rows from inventory.db")

これが TypeScript の作者より Python の作者を多く捕まえる理由は、print() が Python のデフォルトのデバッグ手段だからです。ツール ハンドラー内に紛れ込んでもローカルでは何もクラッシュしません。MCP クライアントがレスポンスをパースしようとして JSON の前にゴミを見つけたときに初めて失敗が見えます。普段 print() を書く場所すべてに file=sys.stderr を加え、構造化されたものには logging を使ってください。

FastMCP による最小サーバー

src/server.py を開きます。配線が動くことを確認するため、ツール 1 つのサーバーから始めます:

# src/server.py
# mcp 1.27.0, Python 3.10+, MCP spec 2025-03-26

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("db-mcp")

@mcp.tool()
def ping() -> str:
    """Return 'pong' to confirm the server is reachable."""
    return "pong"

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

これが動くサーバーに必要な面のすべてです。デコレーターは型ヒント (ここでは無し) から入力スキーマを、docstring から説明を推論します。mcp.run(transport="stdio") はプロセスをブロックし、クライアントが切断するまで stdin から JSON-RPC メッセージを読みます。

クライアントの設定なしでテストするには、インスペクターを起動します:

uv run mcp dev src/server.py

mcp dev はサーバーを起動し、localhost で MCP Inspector UI を開き、生の JSON-RPC トラフィックを表示します。ping を呼んでレスポンスを確認し、ストリームを破壊する余分な出力がないことを確かめられます。これは SDK の中で最も有用な単一コマンドで、ドキュメントはサブページに埋もれさせています。

Pydantic スキーマによる本物のツール

ping プレースホルダーを、Pydantic モデルに支えられた 3 つの実用的なツールに置き換えます。SDK は Pydantic を入力検証と構造化出力の両方に使うので、JSON Schema を手書きしなくてもツール スキーマが堅牢になります:

# src/server.py
# mcp 1.27.0, Pydantic 2.x

import sqlite3
from pathlib import Path
from typing import Annotated

from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("db-mcp")

DB_PATH = Path(__file__).parent.parent / "inventory.db"

class Product(BaseModel):
    """A row from the products table."""
    id: int
    sku: str
    name: str
    stock: int = Field(description="Units currently in stock")

class StockUpdate(BaseModel):
    """Result of a stock-adjustment call."""
    sku: str
    previous_stock: int
    new_stock: int

def _connect() -> sqlite3.Connection:
    # Open read/write but with a timeout so a long write doesn't wedge the agent
    conn = sqlite3.connect(DB_PATH, timeout=5.0)
    conn.row_factory = sqlite3.Row
    return conn

@mcp.tool()
def list_products(
    low_stock: Annotated[
        bool,
        Field(description="If true, return only products with stock < 5."),
    ] = False,
) -> list[Product]:
    """List products in the inventory database."""
    with _connect() as conn:
        if low_stock:
            rows = conn.execute(
                "SELECT id, sku, name, stock FROM products WHERE stock < 5"
            ).fetchall()
        else:
            rows = conn.execute(
                "SELECT id, sku, name, stock FROM products"
            ).fetchall()
        return [Product(**dict(r)) for r in rows]

@mcp.tool()
def get_product(
    sku: Annotated[str, Field(description="Stock-keeping unit, e.g. SKU-001")],
) -> Product:
    """Look up a single product by SKU."""
    with _connect() as conn:
        row = conn.execute(
            "SELECT id, sku, name, stock FROM products WHERE sku = ?",
            (sku,),
        ).fetchone()
        if row is None:
            raise ValueError(f"No product found with sku={sku!r}")
        return Product(**dict(row))

@mcp.tool()
def adjust_stock(
    sku: Annotated[str, Field(description="SKU to adjust")],
    delta: Annotated[
        int,
        Field(
            description="Positive to add stock, negative to remove. "
                        "Tool will refuse to drive stock below zero.",
        ),
    ],
) -> StockUpdate:
    """Adjust stock for a SKU by a positive or negative delta."""
    with _connect() as conn:
        row = conn.execute(
            "SELECT stock FROM products WHERE sku = ?", (sku,)
        ).fetchone()
        if row is None:
            raise ValueError(f"No product found with sku={sku!r}")
        previous = row["stock"]
        new = previous + delta
        if new < 0:
            raise ValueError(
                f"Refusing to set stock below zero (would be {new})."
            )
        conn.execute(
            "UPDATE products SET stock = ? WHERE sku = ?", (new, sku)
        )
        conn.commit()
        return StockUpdate(sku=sku, previous_stock=previous, new_stock=new)

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

重要な細部:

Claude Desktop への接続

道は 2 つあります。シンプルな方は SDK 自身の mcp install コマンドを使います:

uv run mcp install src/server.py --name "Inventory DB"

これは Claude Desktop の設定をパッチして、作業ディレクトリも含めた正しい uv run 呼び出しでサーバーを指すようにしてくれます。環境変数が必要な場合 (API キー、データベース URL、何らかの秘密情報) は -v で渡します:

uv run mcp install src/server.py --name "Inventory DB" \
  -v DB_URL=postgres://... -v API_KEY=abc123

設定を手で管理したい場合は claude_desktop_config.json を編集します。macOS では ~/Library/Application Support/Claude/claude_desktop_config.json、Windows では %AppData%\Claude\claude_desktop_config.json にあります:

{
  "mcpServers": {
    "inventory-db": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/db-mcp",
        "run",
        "python",
        "src/server.py"
      ]
    }
  }
}

Claude Desktop を再起動します。MCP インジケーターに list_productsget_productadjust_stock が並ぶはずです。「在庫の少ない商品は?」と尋ねれば、Claude が list_products(low_stock=True) を呼ぶのが見えます。

Claude Code に接続するには、プロジェクト ディレクトリから:

claude mcp add inventory-db -- uv run python src/server.py

または同じ mcpServers ブロックをプロジェクト ルート配下の .claude/settings.json に追加します。

本番運用 Python サーバーの落とし穴

必要なときに非同期ツール。 上記のハンドラーは同期です。FastMCP は async def ハンドラーも受け入れ、ツールがリモート API (httpx) や別の LLM を呼ぶ場合の正解です。同期と非同期の混在は問題ありません。実際にブロックしない限り、同期ライブラリを asyncio.to_thread で包む必要はありません。

作業ディレクトリの驚き。 Claude Desktop がサーバーを起動すると、プロセスの作業ディレクトリは Claude Desktop が起動した場所であって、あなたのプロジェクトではありません。ファイル パスは Path(__file__).parent で固定 (例のように) するか、ツール引数として絶対パスを渡してください。os.getcwd() を頼ると、ユーザーが別のチャット セッションを開いた瞬間に壊れます。

仮想環境の隔離。 Claude Desktop の設定が素の python を呼び出すと、システム PATH 上の Python を使い、プロジェクトの .venv ではありません。uv run python ... の形式がこれを解決します。uvpyproject.toml からプロジェクトの環境を解決し、毎回正しいインタプリタを起動します。python3 を直接指す自作の設定は、依存関係を最初に追加した瞬間に失敗します。

大きなクエリ結果。 Pydantic モデル 100 万行のリストとして返すとクライアントのコンテンツ サイズ上限を超えてストールします。明示的な limitoffset パラメータでページングするか、ツール内で要約 (count、集計) してエージェントにフォローアップを任せてください。MCP 仕様にハードな上限はありませんが、現実のクライアント上限は数百 KB 程度の構造化コンテンツです。

並行性。 SQLite は既定で書き込みを直列化します。2 つのツール呼び出しが同時に adjust_stock を発火し、片方が 5 秒の timeout を超えて書き込みロックを保持すると、もう片方は OperationalError: database is locked を投げます。実ワークロードでは PostgreSQL に切り替えるか、コネクション プールを使ってください。ローカル エージェント デモなら _connect() の 5 秒タイムアウトで十分です。

Streaming HTTP トランスポート。 SDK はリモート デプロイ向けに transport="streamable-http" と古い transport="sse" をサポートします。クライアントごとに起動するのではなく長寿命サービスとしてサーバーを動かす予定なら、ここでトランスポートを切り替え、サーバーをリバース プロキシの背後に置きます。ローカルなエージェント作業では stdio が正解です。

このパターンが解き放つもの

中心となる動き — 関数を装飾し、Pydantic モデルを返し、エラーで投げる — は、チームが既に持つすべての Python 統合にスケールします。簡単な次の手をいくつか:

主に TypeScript で作業しているなら、CLI をラップする TypeScript の同じパターン@modelcontextprotocol/sdk 1.29 で Node.js 側をカバーします。.NET 側では、.NET 10 上の C# から Microsoft の MCP 配線で Model Context Protocol サーバーを動かす が C# 等価物を示します。IDE が MCP サーバーをネイティブに同梱するときの感触をつかむには、Visual Studio 2022 17.14.30 内の Azure MCP サーバー が現実的な参照点として有用です。さらに、生の MCP の先 — マルチエージェント オーケストレーション — を見ているなら、Microsoft Agent Framework 1.0 が MCP の止まったところを引き継ぐ SDK です。

MCP サーバー自体は、ツールがデータベースをラップするか、REST クライアントをラップするか、200 行の pandas パイプラインをラップするかを気にしません。型付き入力スキーマ (Pydantic が無料で提供) と、SDK がシリアライズできる戻り値、そして余分なバイトの混じらないトランスポートだけが必要です。

ソース リンク

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る