Cloudflare の Python Workers では、公式の MCP Python SDK(mcp パッケージ 2.x)がそのまま動きます。MCPServer でツールを書き、workers.asgi で Worker の fetch につなげば、D1 を読み書きするリモート MCP サーバーになります。この記事では、手順書(ランブック)を検索・追加する MCP サーバーを Python Workers で作り、Bearer トークンで保護して Claude Code から呼ぶところまでを、手元で実際に動かした結果で説明します(2026 年 10 月時点)。
Python Workers の始め方・FastAPI・KV・料金は入門記事「Python Workers 入門:GA 後の始め方と FastAPI 」で扱っています。この記事は、その続きとして「AI エージェントから使う」使い道に絞ります。
Python Workers の MCP サーバーとは、MCP(Model Context Protocol。AI エージェントに外部のツールやデータをつなぐ共通の手順)のサーバーを、Python で書いて Cloudflare Workers に置いたものです。Cloudflare は 2026 年 9 月 21 日の GA 発表で、openai・langchain・mcp などの AI ライブラリが Python Workers でそのまま動くと書いています1 。公式のサンプル集にも、MCP Python SDK と D1 を使った mcp-server の例があります2 。
全体の流れは次の図のとおりです。
Claude Code が POST /mcp を送り、Python Worker の fetch が認証を確かめてから asgi 経由で MCPServer のツールを呼び、D1 を読み書きする流れ 図: 筆者作成
Worker の入口(Default.fetch) : パス・メソッド・トークンを確かめる。普通の Python Workers と同じ WorkerEntrypoint のクラス
MCPServer : MCP Python SDK のサーバー。@server.tool() を付けた関数がツールになる。streamable_http_app() で ASGI アプリに変える
workers.asgi : ASGI アプリ(FastAPI などと同じ形)を Worker のリクエストにつなぐ Cloudflare の部品1
D1 : Cloudflare の SQLite 互換データベース。env.DB で Python から直接呼べる
TypeScript で作る場合との違いは後の節で表にまとめます。
2026 年の夏から秋にかけて、Python で MCP サーバーを Workers に置くための部品が続けてそろいました。
日付 出来事 2026-07-28 MCP の仕様 2026-07-28 版が公開。initialize の握手とセッション ID が無くなり、1 リクエストで完結するステートレスな方式になった3 2026-07-28 MCP Python SDK 2.0.0 が PyPI に公開。FastMCP は MCPServer に改名4 5 2026-08-06 Cloudflare が新しい MCP 仕様への対応を発表。「MCP 自体はもう Durable Object を必要としない」と説明6 2026-09-08 Python Workers で Python 3.14 を使う互換フラグ python_workers_314 が既定になる日付7 2026-09-21 Python Workers が GA。mcp ライブラリがそのまま動くと明記1
ポイントは、MCP がステートレスになったことです。以前の MCP はセッションを保つ必要があり、Workers では Durable Objects を使うのが定番でした。2026-07-28 版からは 1 回の POST で 1 回のツール呼び出しが完結するので、Python の普通の Worker 1 つで足ります6 。
日本の開発者の関心も伸びています。Qiita の cloudflare タグ(ストック 5 以上の記事)は、2026 年 10 月 14 日までの 14 日間で 3 本と、その前の 14 日間の 1 本から 3 倍になりました(筆者集計。Qiita API の記事一覧から数えた値)。
結論から言うと、既存の Python 資産(社内ライブラリ・データ処理・ML の前処理)をツールとして AI エージェントに渡したいなら Python Workers、OAuth で守る本格的なサーバーを作るなら TypeScript が向いています。TypeScript での作り方は「リモート MCP サーバーを Cloudflare Workers で作る 」で解説しています。
項目 Python Workers TypeScript(Agents SDK) SDK MCP Python SDK(mcp 2.x の MCPServer)2 MCP TypeScript SDK と createMcpHandler6 Worker へのつなぎ方 workers.asgi.fetch() で ASGI アプリを呼ぶcreateMcpHandler() の戻り値を fetch で返す認証 公式の Python 例は認証なし2 。本記事では Bearer トークンを自作 Workers OAuth Provider で OAuth 2.1 に対応6 同梱サイズ(筆者計測) 約 18.8 MiB(非圧縮) 本記事では測っていない 向いている用途 Python のライブラリ・既存コードをツールにする 公開サービス・多人数で OAuth が要るサーバー
前提は次のとおりです。筆者は macOS で、2026 年 10 月 14 日に確かめました。
uv 0.12.3 以上(pywrangler が要求する。0.8 系では uv version at least 0.12.3 required で止まった)
Node.js(pywrangler が内部で npx wrangler を呼ぶ。筆者は v24.6.0)
確認したバージョン: workers-py(pywrangler)1.17.7、wrangler 4.150.0、mcp 2.3.0、Claude Code 2.1.299
ディレクトリ構成は次のとおりです。公式の mcp-server 例2 と同じ形にしています。
runbook-mcp/
├── pyproject.toml
├── wrangler.jsonc
├── .dev.vars # ローカル用のトークン(Git に入れない)
├── migrations/0001_runbooks.sql
└── src/entry.py
pyproject.toml に MCP Python SDK を書きます。2.x で API が変わったので、バージョンは固定します。
[project]
name = "runbook-mcp"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
"mcp==2.3.0" ,
]
[dependency-groups]
dev = [
"workers-py" ,
"workers-runtime-sdk" ,
]
wrangler.jsonc で D1 をつなぎます。compatibility_date を 2026-09-08 以降にすると、Python 3.14 が使われます7 。
{
"name" : "runbook-mcp" ,
"main" : "src/entry.py" ,
"compatibility_date" : "2026-10-01" ,
"compatibility_flags" : [ "python_workers" ] ,
"d1_databases" : [
{
"binding" : "DB" ,
"database_name" : "runbook-mcp" ,
"database_id" : "00000000-0000-0000-0000-000000000000" ,
"migrations_dir" : "./migrations"
}
]
}
database_id はローカルではこのままで動きます。本番では手順 5 で作った ID に置き換えます。テーブルと最初のデータは migrations/ 0001_runbooks.sql に書きます。
CREATE TABLE runbooks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL ,
body TEXT NOT NULL ,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO runbooks (title, body) VALUES
('本番 DB のコネクション上限に達したとき' , '1. ダッシュボードで接続数を確認する
2. 長時間のクエリを止める
3. 接続プールの上限を一時的に上げる' ),
('Workers のデプロイを切り戻す' , '1. wrangler deployments list で直前の版を確認する
2. wrangler rollback で戻す' );
最後に、ローカル用のトークンを .dev.vars に置きます。
echo 'MCP_TOKEN="dev-token-change-me"' > .dev.vars
ツールは、型ヒントと docstring を付けた async 関数に @server.tool() を付けて書きます。MCP Python SDK が型ヒントから入力の JSON Schema を作り、docstring をツールの説明として AI エージェントに渡します。
import hmac
from urllib.parse import urlparse
from workers import Response, WorkerEntrypoint, asgi
def build_app (env ):
from mcp.server import MCPServer
server = MCPServer("Runbook server" )
@server.tool()
async def search_runbooks (query: str ) -> list [dict ]:
"""手順書をタイトルと本文で検索し、id とタイトルの一覧を返す。"""
rows = await (
env.DB.prepare(
"SELECT id, title, updated_at FROM runbooks"
" WHERE title LIKE ?1 OR body LIKE ?1"
" ORDER BY updated_at DESC LIMIT 20"
)
.bind(f"%{query} %" )
.all ()
)
return [
{"id" : r.id , "title" : r.title, "updated_at" : r.updated_at}
for r in rows.results
]
@server.tool()
async def get_runbook (id : int ) -> dict :
"""id を指定して手順書の本文を返す。"""
row = await (
env.DB.prepare("SELECT id, title, body, updated_at FROM runbooks WHERE id = ?" )
.bind(id )
.first()
)
if row is None :
return {"error" : "runbook not found" , "id" : id }
return {"id" : row.id , "title" : row.title, "body" : row.body, "updated_at" : row.updated_at}
@server.tool()
async def add_runbook (title: str , body: str ) -> dict :
"""手順書を 1 件追加し、追加した id を返す。"""
if len (title) > 200 or len (body) > 10000 :
return {"error" : "title or body is too long" }
row = await (
env.DB.prepare("INSERT INTO runbooks (title, body) VALUES (?, ?) RETURNING id" )
.bind(title, body)
.first()
)
return {"id" : row.id , "title" : title}
return server.streamable_http_app(
streamable_http_path="/mcp" ,
stateless_http=True ,
)
def authorized (request, token: str ) -> bool :
header = request.headers.get("Authorization" ) or ""
return hmac.compare_digest(header.encode(), f"Bearer {token} " .encode())
class Default (WorkerEntrypoint ):
def __init__ (self, ctx, env ):
super ().__init__(ctx, env)
self .app = build_app(env)
async def fetch (self, request ):
if urlparse(request.url).path != "/mcp" :
return Response("Not found" , status=404 )
if request.method != "POST" :
return Response("Method not allowed" , status=405 , headers={"Allow" : "POST" })
if request.headers.get("Origin" ):
return Response("Forbidden" , status=403 )
if not authorized(request, self .env.MCP_TOKEN):
return Response("Unauthorized" , status=401 )
return await asgi.fetch(self .app, request, self .env)
公式の例2 との違いは fetch の中だけです。書いた理由は次のとおりです。
stateless_http=True : セッションを持たない動かし方にする。Workers では次のリクエストが同じインスタンスに届くとは限らないので、状態をメモリに持たない形にする
GET と DELETE に 405 : 2026-07-28 版の仕様は、古いクライアントからの GET や DELETE に 405 Method Not Allowed を返すよう求めている8
Origin があれば 403 : 仕様は DNS リバインディング対策として Origin の検証を必須にしている8 。Claude Code などの CLI は Origin を送らないので、付いていたら拒否する単純な方法にした
hmac.compare_digest に bytes を渡す : 文字列のまま渡すと、ASCII 以外を含むヘッダーで TypeError になる。比べる時間からトークンを推測されないよう、== ではなくこれを使う
D1 の値は ? で bind する : ツールの引数は AI エージェントが作る値なので、SQL に文字列を直接つながない
ローカルの D1 にテーブルを作ってから、開発サーバーを起動します。
uv run pywrangler d1 migrations apply runbook-mcp --local
uv run pywrangler dev --port 8788
pywrangler dev で起動し、トークンなしの POST は 401、GET は 405、トークン付きの tools/list で 3 つのツール名が返る様子 筆者の macOS で実行した出力(2026-10-14)
起動すると env.DB と env.MCP_TOKEN が読み込まれ、同梱モジュールは 877 個・19,270.73 KiB と表示されました。別のターミナルから、MCP 2026-07-28 版の形で tools/ list を呼びます。新しい版では、initialize を先に送る必要はありません。代わりに、メソッド名を Mcp-Method ヘッダーと本文の両方に入れます8 。
#!/bin/sh
curl -s -X POST "${MCP_URL:-http://localhost:8788/mcp} " \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer ${MCP_TOKEN:-dev-token-change-me} " \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
--data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
./list-tools.sh | jq -r '.result.tools[].name'
応答の inputSchema には、型ヒントから作られたスキーマが入ります。たとえば get_runbook は {"id": {"type": "integer"}} が必須になりました。返り値が list[dict] の search_runbooks には outputSchema も付き、呼ぶと structuredContent に結果が入ります。
古い版のクライアントも確かめました。2025-06-18 版の initialize を送ると、サーバーは protocolVersion: "2025-06-18" で応答しました(公式例 + mcp 2.2.0 で確認)。新旧どちらのクライアントからも使えます。
Claude Code には claude mcp add --transport http でつなぎます。トークンはファイルに書かず、環境変数を ${VAR} で参照します。Claude Code は .mcp.json の headers で環境変数を展開します9 。
claude mcp add --transport http --scope project runbook http://localhost:8788/mcp \
--header 'Authorization: Bearer ${RUNBOOK_MCP_TOKEN}'
export RUNBOOK_MCP_TOKEN=dev-token-change-me
--scope project にすると、チームで共有する .mcp.json に次のように書かれます。値は ${RUNBOOK_MCP_TOKEN} のまま残るので、Git に入れてもトークンは漏れません。
{
"mcpServers" : {
"runbook" : {
"type" : "http" ,
"url" : "http://localhost:8788/mcp" ,
"headers" : {
"Authorization" : "Bearer ${RUNBOOK_MCP_TOKEN}"
}
}
}
}
Claude Code が runbook の MCP サーバーで search_runbooks と get_runbook を呼び、DB の手順書を 3 行で答える様子 筆者の macOS で実行した出力(2026-10-14)。モデルは Sonnet
claude -p で頼むと、Claude Code は search_runbooks {"query": "DB"} で手順書を探し、get_runbook {"id": 1} で本文を読んで 3 行で答えました。別の実行では「Hyperdrive の接続エラーを調べる」という手順書を add_runbook で追加し、id 3 が返りました。
プロジェクトの .mcp.json は、対話モードの claude で最初に承認するまで使われません(claude mcp list では Pending approval と表示)。上の GIF では、承認を待たずに試すため --mcp-config .mcp.json --strict-mcp-config で読み込んでいます。
トークンをわざと間違えると、Claude Code は「サーバーが設定済みの Authorization ヘッダーを拒否しました(HTTP 401)」と答え、ツールを使いませんでした。
自作の MCP サーバーは、AI エージェントに「チームだけが知っている情報」を渡すのに向いています。
障害対応 : 手順書・過去の障害記録をツールにし、Claude Code に「この手順書どおりに確認して」と頼む
レビュー : 社内の設計ルールやコーディング規約を検索するツールを作り、レビュー時に引かせる
データ処理 : pandas などで書いた集計・前処理を Python のままツールにして、エージェントから呼ぶ
本番のコマンドは次のとおりです。pywrangler は sync と types 以外のコマンドを wrangler に渡すので、D1 やシークレットの操作も pywrangler から呼べます(pywrangler --help で確認)。筆者はデプロイまではしていないので、ここは wrangler の通常の手順として読んでください。
uv run pywrangler d1 create runbook-mcp
uv run pywrangler d1 migrations apply runbook-mcp --remote
openssl rand -hex 32
uv run pywrangler secret put MCP_TOKEN
uv run pywrangler deploy
デプロイ後は Claude Code の URL を https:/ / runbook-mcp.<サブドメイン>.workers.dev/ mcp に変えます。人によってトークンを分けたい、失効させたいといった要件が出てきたら、OAuth に対応した TypeScript 版に切り替えるのが早道です。
MCP Python SDK を入れた Worker は、非圧縮で約 18.8 MiB でした。Workers の上限は無料・有料とも非圧縮 64 MiB で、圧縮後のサイズには上限がありません10 。上限には十分に収まります。
公式のサンプルと比べるため、pywrangler deploy --dry-run でアップロードサイズを測りました。
Python Workers のアップロードサイズ(pywrangler deploy --dry-run) データを表で見る Python Workers のアップロードサイズ(pywrangler deploy --dry-run) Worker 非圧縮(MiB) gzip(MiB) hello(依存なし) 0.11 0.03 fastapi 8.75 2.24 本記事の MCP サーバー(mcp 2.3.0) 18.82 5.11 django-todo-d1 23.08 5 langchain 27.41 6.5
出典: 筆者計測(2026-10-14。cloudflare/python-workers-examples の各例と本記事のコード。workers-py 1.17.7・wrangler 4.150.0 の Total Upload を MiB に換算)
MCP SDK は、pydantic・starlette・cryptography などを一緒に連れてくるため FastAPI だけの例の約 2 倍になります。サイズが大きいと起動にかかる時間も気になります。ローカルの pywrangler dev では、起動後の最初のリクエストに約 3.9 秒、2 回目以降は約 0.1 秒かかりました。これは手元の値で、本番のコールドスタート(メモリスナップショットで短くなる仕組み)とは別物です。仕組みは入門記事 を参照してください。
症状 原因 対処 uv version at least 0.12.3 requireduv が古い uv self update で更新する(筆者の環境は 0.8.17 で止まった)No module named 'mcp.server.fastmcp'mcp 2.x で FastMCP が MCPServer に改名された5 from mcp.server import MCPServer に直す。1.x のコードを使うなら mcp<2 に固定する-32020 mcp-method header does not matchMcp-Method ヘッダーと本文の method が違う仕様上、サーバーは 400 で拒否する8 。curl で試すときは両方をそろえる Claude Code で Pending approval プロジェクトの .mcp.json が未承認 対話モードの claude で承認する 401 が返る トークンが違う、環境変数が空 echo $RUNBOOK_MCP_TOKEN で確認する。本番は pywrangler secret put で入れたか確かめる
ほかに気をつける点です。
無料プランの CPU 時間 : HTTP リクエスト 1 回あたりの CPU 時間は、無料プランが 10 ms、有料プランは既定 30 秒です10 。MCP の 1 回の処理にかかる CPU 時間は本記事では測っていないので、本番ではダッシュボードで確かめてください
ツールの返り値の大きさ : 一覧系のツールには LIMIT を付けます。返り値が大きいと、AI エージェントのコンテキストを無駄に使います
書き込み系のツール : add_runbook のような書き込みは、Claude Code の --allowedTools で許可するツールを絞ると安全です
Python Workers では、公式の MCP Python SDK(mcp 2.x)の MCPServer がそのまま動く。workers.asgi.fetch() で Worker につなぐ
MCP 2026-07-28 版でステートレスになったので、Durable Objects なしの普通の Worker 1 つでリモート MCP サーバーが作れる
公式の例は認証なし。fetch で 405・403・401 を先に返し、Bearer トークンで守ると Claude Code から安全に使える
同梱サイズは非圧縮で約 18.8 MiB。上限 64 MiB に収まる
次にやることは、手元の Python のスクリプトを 1 つ選び、@server.tool() を付けて Claude Code から呼んでみることです。
はい。Cloudflare は 2026 年 9 月の GA 発表で、mcp ライブラリが Python Workers でそのまま動くと書いています1 。公式のサンプル集にも、MCP Python SDK と D1 を使った MCP サーバーの例があります2 。
mcp 2.x では使えません。FastMCP は MCPServer に改名され、mcp.server.fastmcp の import は ModuleNotFoundError になります5 。stateless_http などの引数もコンストラクタから streamable_http_app() に移りました。
ツールを呼ぶだけなら不要です。MCP 2026-07-28 版でセッションが無くなり、Cloudflare も「MCP 自体はもう Durable Object を必要としない」と説明しています6 。複数のリクエストで状態を共有したいときだけ、Durable Objects を使います。
筆者の確認では、つながりました。mcp 2.2.0 を使う公式例に 2025-06-18 版の initialize を送ると、その版で応答しました。Claude Code 2.1.299 からも問題なく使えました。
2026 年 10 月時点で、Cloudflare の Python 向け MCP の例は認証なしです2 。Cloudflare の OAuth の部品(Workers OAuth Provider)は TypeScript 向けの説明が中心です6 。OAuth が必要なら、TypeScript でのリモート MCP サーバーの作り方 を参考にしてください。
Python Workers are now generally available — Cloudflare, 2026-09-21(2026-10-14 参照) ↩ ↩2 ↩3 ↩4
python-workers-examples / mcp-server — Cloudflare(GitHub), 2026-10-14 参照 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
The 2026-07-28 Specification — Model Context Protocol Blog, 2026-07-28(2026-10-14 参照) ↩
mcp(PyPI) — Python Package Index, 2026-10-14 参照(2.0.0 は 2026-07-28、2.3.0 は 2026-10-02 公開) ↩
Migration Guide(v1 to v2) — MCP Python SDK, 2026-10-14 参照 ↩ ↩2 ↩3
The next generation of MCP — Cloudflare, 2026-08-06(2026-10-14 参照) ↩ ↩2 ↩3 ↩4 ↩5 ↩6
Compatibility flags — Cloudflare Docs, 2026-10-14 参照 ↩ ↩2
Streamable HTTP(2026-07-28) — Model Context Protocol Specification, 2026-10-14 参照 ↩ ↩2 ↩3 ↩4
Connect Claude Code to tools via MCP — Anthropic, 2026-10-14 参照 ↩
Limits · Cloudflare Workers — Cloudflare Docs, 2026-10-14 参照 ↩ ↩2