cloudflare-mcp-sample
A stateless MCP server on Cloudflare Workers providing a ping tool and a CSV column summarizer, demonstrating the createMcpHandler API.
README
cloudflare-mcp-sample
Cloudflare Workers 上にステートレス方式の最小 MCP サーバを立てるサンプル。
agents パッケージの createMcpHandler を直に使い、テンプレート任せにしない構成にしてある。
ツールは疎通確認用の ping と、CSV の 1 列を合計する sum_csv_column の 2 本だけ。
動作確認済みバージョン
| パッケージ | バージョン |
|---|---|
| Node.js | 22.23.2 |
agents |
0.20.1 |
@modelcontextprotocol/server |
2.0.0(完全固定) |
zod |
4.4.3 |
wrangler |
4.123.0 |
| TypeScript | 5.9.3 |
Node.js 22 以上が必須。
create-cloudflareもwranglerもengines: { node: ">=22.0.0" }を宣言していて、 Node 20 では警告ではなくWrangler requires at least Node.js v22.0.0.で停止する。
@modelcontextprotocol/serverにキャレット (^2.0.0) を付けないこと。agents@0.20.1のpeerDependenciesは"@modelcontextprotocol/server": "2.0.0"と完全一致で固定されている。 公式ドキュメントも "Use the exact MCP versions required by your installed Agents release." と書いている。
セットアップ
npm install
ローカル実行
npm start
http://127.0.0.1:8787/mcp で待ち受ける。
ドキュメントの手順には
8788と書かれているが、素の Worker をwrangler devで起動した場合は 8787 になる。 起動ログのReady on ...を必ず読むこと。
別ターミナルで MCP Inspector を起動して接続する。
npx @modelcontextprotocol/inspector@latest
Inspector に http://127.0.0.1:8787/mcp を入力 → Connect → List Tools。
CLI モードでも確認できる。
npx @modelcontextprotocol/inspector@latest --cli http://127.0.0.1:8787/mcp --transport http --method tools/list
/mcpをブラウザで直接開いても確認にはならない。MCP クライアントが話す JSON-RPC を、ブラウザは話さない。
デプロイ
npx wrangler@latest deploy
稼働中のエンドポイント: https://cloudflare-mcp-sample.ma2no4413.workers.dev/mcp
npx @modelcontextprotocol/inspector@latest --cli \
https://cloudflare-mcp-sample.ma2no4413.workers.dev/mcp \
--transport http --method tools/list
初回は workers.dev サブドメインの登録が要る。 未登録のままでも
wrangler deployはSuccess!を返し、警告 1 行を出すだけで成功したように見える。 しかし実際にアクセスすると DNS はワイルドカードで引けるのに TLS ハンドシェイクで落ちる (Windows ではSEC_E_ILLEGAL_MESSAGE)。ダッシュボードの Workers & Pages → Subdomain で登録し、 もう一度wrangler deployを実行すると URL が正しくなる。 証明書が行き渡るまで実測で約 75 秒かかった。サブドメインはアカウントに 1 つしか持てず、配下の全 Worker の URL に入る。 Worker 名と同じ文字列を選ぶと
foo.foo.workers.devになるので、ハンドル名にしておくのが無難。
Claude Code から使う
.mcp.json を同梱してあるので、clone してディレクトリを開けばそのまま接続できる。
{
"mcpServers": {
"cloudflare-mcp-sample": {
"type": "http",
"url": "https://cloudflare-mcp-sample.ma2no4413.workers.dev/mcp"
}
}
}
初回のみ信頼の承認プロンプトが出る(project スコープの MCP サーバは無条件には読み込まれない)。
自分で建てたサーバに向ける場合は URL を書き換えるか、claude mcp add で入れ直す。
claude mcp add --transport http --scope project <name> https://<worker>.<subdomain>.workers.dev/mcp
認証は付いていない。 URL を知っていれば誰でも叩ける。機微データを流さないこと。
ツール
ping
引数なし。サーバ名・バージョンとサーバ側の現在時刻 (UTC) を JSON で返す。
{
"server": "cloudflare-mcp-sample",
"version": "0.1.0",
"now": "2026-08-16T10:06:55.511Z"
}
sum_csv_column
| 引数 | 型 | 説明 |
|---|---|---|
csv |
string |
ヘッダ行を含む CSV 文字列 |
column |
string |
合計したい列のヘッダ名 |
指定列を数値として合計する。数値化できない行(空欄・非数値)はスキップし、その件数も返す。
{ "column": "sales", "sum": 400, "counted": 3, "skipped": 2, "rows": 5 }
列名が存在しない場合は isError: true と、実際のヘッダ一覧を返す。
CSV パースは外部ライブラリを使わない素朴な実装で、引用符付きフィールド ("a,b") には対応していない。
サンプルデータ
examples/store-sales.csv に、店舗別・月次売上を模した架空データを置いてある。
集計で現実に効いてくる「汚れ」を意図的に混ぜてある。
S004(休業)—salesもcustomersも空欄S006(POS 障害)—salesだけN/Aでcustomersは生きている
同じファイルでも、集計する列によってスキップ数が変わる。
| 列 | sum |
counted |
skipped |
rows |
|---|---|---|---|---|
sales |
6,054,200 | 6 | 2 | 8 |
customers |
2,161 | 7 | 1 | 8 |
sales の合計は 8 店舗中 6 店舗分でしかない。skipped を返さない設計だと、
これが「8 店舗の合計」として読まれる。 ツールの戻り値に信頼度の材料を含める理由がこれ。
引用符付きフィールドの壊れ方
store_id,store_name,sales
S001,"Shibuya, Tokyo",1284500
S002,"Shinjuku, Tokyo",1650000
sum: 0, counted: 0, skipped: 2 が返る。引用符内のカンマで列がずれ、
sales の位置に Tokyo" が来て全行スキップされる。
間違った合計を返すのではなく skipped が全行に立つので、
戻り値だけで壊れていると分かる。素朴なパーサとしては悪くない壊れ方といえる。
なぜ McpAgent ではなく createMcpHandler なのか
McpAgent は deprecated かつ機能凍結されている。公式ドキュメントの
Handler API が
明示的に "McpAgent is deprecated and feature-frozen" と書いており、新規サーバは
createMcpHandler を使うことになっている。createLegacyMcpHandler はレガシー移行専用。
構造的な理由は状態の置き場所にある。
McpAgentは Durable Object にセッション状態を持たせる設計だった。接続ごとに DO インスタンスが立つ。createMcpHandlerはステートレス。リクエストごとにファクトリ関数からMcpServerを作って捨てる。 状態が要るなら D1 / KV / R2 / Durable Objects に明示的に置く。
状態を持たないサーバに DO を強制されないぶん、コールドスタートも課金も素直になる。 今回のような「引数を受け取って計算して返すだけ」のツールに、セッションは要らない。
agents/mcp/server の createMcpHandler は、実体は createStatelessMcpHandler の別名で、
MCP SDK 側の createMcpHandler を Workers 向けにラップしたもの。次のオプションを足している。
| オプション | 既定値 | 内容 |
|---|---|---|
route |
"/mcp" |
この Worker が処理する pathname(完全一致) |
corsOptions |
— | CORS ヘッダ。false で無効化 |
allowedHostnames |
localhost と workers.dev |
受け付ける Host |
allowedOriginHostnames |
同上 | 受け付けるブラウザ Origin。Origin なしの非ブラウザクライアントは常に通る |
authContext |
— | getMcpAuthContext() から読める props |
これに加えて、SDK 側の legacy / onerror / responseMode / maxSubscriptions / keepAliveMs が渡せる。
実装上の落とし穴
1. ハンドラを default export に直接置かない
// NG: Wrangler が「関数の default export」を WorkerEntrypoint クラスとして解釈する
export default createMcpHandler(createServer);
// OK
export default {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
} satisfies ExportedHandler<Env>;
NG 版で起きることを実際に確かめた。wrangler deploy --dry-run は通る。
バンドルは成功し Total Upload: 991.85 KiB まで表示される。落ちるのはランタイムの起動時。
X [ERROR] service core:user:cloudflare-mcp-sample:
Uncaught TypeError: Class extends value (request, _env, ctx) => serve(request, void 0, ctx)
is not a constructor or null
at wrapWorkerEntrypoint
X [ERROR] The Workers runtime failed to start.
Wrangler は関数の default export を WorkerEntrypoint として extends しようとする。
関数はコンストラクタではないのでここで死ぬ。ビルドが通ったことは何の保証にもならない。
2. サーバインスタンスではなくファクトリを渡す
グローバルに 1 つ McpServer を作って使い回さない。createMcpHandler に渡すのは関数そのもので、
ハンドラがリクエストごとにサーバを生成する。
3. 引数なしツールでは inputSchema を「省略」する
inputSchema: {} を渡すと registerTool のオーバーロード解決が壊れ、
戻り値の type: "text" が string に広がって型エラーになる。空オブジェクトではなくキーごと消す。
4. import { createMcpHandler } の出どころが 2 つある
@modelcontextprotocol/server にも createMcpHandler が存在する。
Workers で使うのは agents/mcp/server の方(route / CORS / Host 検証を持つラッパ)。
エディタの自動インポートが SDK 側を選んでいないか確認すること。
所感 — CSV を引数で渡す方式の限界
sum_csv_column は本命(店舗別 CSV の横断集計)の最小プロトタイプとして書いた。
実際に動かして分かった限界を残しておく。
- CSV 本体を引数で渡す設計は、そのままではスケールしない。 文字列がまるごとモデルのコンテキストを通るため、
数百 KB の実データを渡した時点でトークンを食い潰す。集計は「データを送る」のではなく
「データの置き場所を指す ID を送る」形にすべきで、本命は R2/D1 に置いたデータを
dataset_idで指す設計になる。Workers の CPU 10ms 制約を SQL に逃がす話とも整合する。 skippedを返す設計は正解だった。 数値化できない行を黙って捨てると、モデルは合計値を無条件に信じる。 スキップ件数が返っていれば「5 行中 2 行落ちています」とモデル自身が但し書きを付けられる。 ツールの戻り値は「答え」だけでなく「答えの信頼度を判断する材料」を含めるべき。- エラーは例外ではなく
isError: trueで返す。 列名が無いときに実際のヘッダ一覧を添えて返すと、 モデルはその場で列名を直して再実行できる。エラーメッセージがそのままリトライの入力になる。 - MCP Inspector の CLI は
--tool-argに改行を含む値を渡せない。 改行で引数が分割され、 ヘッダ行だけが届いてrows: 0が返る。複数行の値を試すときは Inspector の UI を使うか、fetchで JSON-RPC を直接叩くこと(CLI のパーサの制約であって、サーバ側の問題ではない)。
この先の拡張
Phase 1 は「往復が通ること」と「正しい API を使っていること」だけを成果物にしている。 以下は意図的に外してあるが、捨てたわけではなく順に足していく。
Phase 2 — 認証(OAuth 2.1)
@cloudflare/workers-oauth-provider で OAuth 2.1 のプロバイダ側を肩代わりさせ、
認証済みユーザの情報を createMcpHandler の authContext 経由でツールに渡す。
ツール内では getMcpAuthContext() から読む。
ここは公式に動く実例がまだ無い。authless のデモは
createMcpHandlerに移行済みだが、 OAuth のデモ (cloudflare/ai/demos/remote-mcp-github-oauth) はMcpAgent+ Durable Objects のままで、this.propsを使っている。 一方ドキュメントはgetMcpAuthContext()/context.http.authInfoを使えと書いている。 ライブラリが健在であることと、新しいハンドラに繋がることは別の話で、後者は未検証。
Phase 3 — データの置き場所(R2 / D1)
sum_csv_column で分かったとおり、CSV 本体を引数で渡す設計はスケールしない。
文字列がまるごとモデルのコンテキストを通るため、実データ規模でトークンが破綻する。
「データを送る」のではなく「データの置き場所を指す ID を送る」形に変える。
- 引数は
dataset_idとクエリ条件だけ。実体は R2 / D1 に置く - 集計はアプリ側でループを回さず D1 の SQL に寄せる(Workers の CPU 10ms 制約を設計で回避する)
describe_datasetのような「列とデータ型を返すツール」を用意する。 Phase 1 でエラー時に実ヘッダを返す設計が効いたのと同じ理屈で、 モデルに列名を推測させない
Phase 4 — 配布
@modelcontextprotocol/server の ./stdio エクスポートで stdio 版を同梱し、
ローカル実行と Workers ホスト版の二層構成にする。npm への公開もこの段階。
テスト / CI
Phase 1 では入れていない(スキャフォルドの vitest も削除した)。 ツールが 2 本で、検証が Inspector の往復で足りているうちは、 テストを置いても実際には守られない。Phase 3 で集計ロジックが SQL に移り、 「壊れたら気付けない」状態になった時点で入れる。
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.