cloudflare-mcp-sample
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cloudflare-mcp-samplesum the sales column"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cloudflare-mcp-sample
Cloudflare Workers 上にステートレス方式の MCP サーバを建てるサンプル。
agents パッケージの createMcpHandler を直に使い、テンプレート任せにしない。
非推奨の McpAgent は一切使っていない。
認証なしの最小構成から始めて、OAuth、D1、stdio 版と 4 つの Phase に分けて作った。 Phase ごとにタグを切ってあり、それぞれに解説記事がある。
なぜ Phase に分けたか
理由は 3 つある。
1. 記事が指す先を固定するため。
記事は「動くコード」を指す必要がある。main を指すと、コードが進むたびに
過去の記事の説明とズレていく。タグを指せば、1 年後に読んでも記事のとおりに動く。
2. 未知を 1 つずつ潰すため。 Phase 2(OAuth をステートレスハンドラに繋ぐ)は、着手時点で公式に動く実例が存在しなかった。 ここが通らなければ Phase 3 以降の実装は無駄になる。先に一番不確かなものを試して、 通らなければ引き返せるようにしてある。実際、各 Phase の着手前に撤退条件を決めていた。
3. 前の Phase を壊さないため。
phase1 の認証なし構成は、いまも別の Worker として動いている。
記事に「この URL を叩くとこう返る」と書いた以上、あとから壊すと記事が嘘になる。
だから Phase 2 では上書きせず、別名でデプロイした。
各 Phase の中身は「その時点で最小限のもの」に絞ってある。ついでの機能追加をしないという 制約でもあり、これが無いとスコープが際限なく膨らむ。
Related MCP server: Hello MCP Server
Phase と対応する記事
タグごとにコードが完結している。記事はタグを指しているので、あとから読んでも説明とコードがずれない。
Phase | タグ | 何をしたか | 記事 |
1 | 認証なしの最小構成。ツールは | ||
2 | GitHub OAuth を追加。ステートレスハンドラのまま | ||
3 |
| ||
4 |
| stdio 版を追加して二層構成に |
稼働中のエンドポイント
認証なし(
phase1の構成):https://cloudflare-mcp-sample.ma2no4413.workers.dev/mcpGitHub OAuth(
phase2以降):https://cloudflare-mcp-sample-oauth.ma2no4413.workers.dev/mcp
別々の Worker として同時に動いている。main をそのままデプロイすると OAuth 版になる。
認証なしの構成を見たいときは phase1 タグを参照すること。
動作確認済みバージョン
パッケージ | バージョン |
Node.js | 22.23.2 |
| 0.20.1 |
| 2.0.0(完全固定) |
| 0.10.3( |
| 4.4.3 |
| 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 starthttp://127.0.0.1:8787/mcp で待ち受ける。
ドキュメントの手順には
8788と書かれているが、素の Worker をwrangler devで起動した場合は 8787 になる。 起動ログのReady on ...を必ず読むこと。
別ターミナルで MCP Inspector を起動して接続する。
npx @modelcontextprotocol/inspector@latestInspector に 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 を、ブラウザは話さない。
stdio 版(ローカル)
同じツールを、ローカルの SQLite に対して動かせる。HTTP 版とツール定義を共有しているので、
違うのはデータの置き場所だけ。認証は無い(ローカルプロセスに OAuth を挟む相手がいない)ため、
whoami も無い。
データを用意する
node scripts/gen-dataset.mjs --rows 1000 --id small --out schema/seed-small.sql
node scripts/gen-dataset.mjs --rows 10000 --id medium --out schema/seed-medium.sql
node -e "
const {DatabaseSync}=require('node:sqlite');const fs=require('fs');
const db=new DatabaseSync('local.db');
db.exec(fs.readFileSync('schema/schema.sql','utf8'));
for (const f of ['small','medium']) db.exec(fs.readFileSync('schema/seed-'+f+'.sql','utf8'));
"D1 と同じスキーマ・同じ生成スクリプトを使う。だから両方で同じ数字が返る。
起動する
npm run build:stdio
node dist/stdio.mjs local.dbMCP Inspector から確認する場合:
npx @modelcontextprotocol/inspector@latest --cli node dist/stdio.mjs local.db --method tools/listSQLite は Node 22 に同梱されている(
node:sqlite)。追加の依存は要らない。 実験的機能なので起動時にExperimentalWarningが出るが、stderr に出るので問題ない。stdout は MCP のプロトコル専用。
console.logを 1 回でも書くと JSON-RPC が壊れる。 ログを出すなら必ず stderr へ。
デプロイ
npx wrangler@latest deploymain(OAuth 版)をデプロイする場合は、先に以下が要る。
npx wrangler kv namespace create "OAUTH_KV" # 出力された id を wrangler.jsonc に書く
npx wrangler secret put GITHUB_CLIENT_ID # 値はプロンプトに入力する
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY # openssl rand -hex 32 など
wrangler secret put <値>と書かないこと。引数は「名前」で、値はプロンプトに入力する。 逆にすると値がシークレット名として登録され、wrangler secret listで誰でも読める状態になる。
稼働中のエンドポイント:
phase1(認証なし): https://cloudflare-mcp-sample.ma2no4413.workers.dev/mcpphase2(OAuth): https://cloudflare-mcp-sample-oauth.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.mcp.json には認証なし版と OAuth 版の両方を入れてある。OAuth 版は初回接続時に
ブラウザが開き、GitHub のログインを求められる。
認証なし版は URL を知っていれば誰でも叩ける。機微データを流さないこと。
ツール
ping
引数なし。サーバ名・バージョンとサーバ側の現在時刻 (UTC) を JSON で返す。
{
"server": "cloudflare-mcp-sample",
"version": "0.1.0",
"now": "2026-08-16T10:06:55.511Z"
}list_datasets
引数なし。集計できるデータセットの一覧と行数を返す。
[{ "id": "small", "label": "店舗別・月次売上(合成データ / 1000 行)", "rows": 1000 }]describe_dataset
引数 | 型 | 説明 |
|
|
|
列名・型・列ごとの欠損数・対象月の範囲を返す。aggregate を呼ぶ前に見るためのもの。
{
"id": "large", "rows": 100000,
"month_range": { "from": "2024-01", "to": "2026-12" },
"columns": [
{ "name": "sales", "type": "REAL", "aggregatable": true, "missing": 6698 },
{ "name": "customers", "type": "INTEGER", "aggregatable": true, "missing": 4107 }
]
}aggregate
引数 | 型 | 説明 |
|
|
|
|
| 集計する列名 |
|
|
|
データ本体は受け取らない。指定列を合計し、数値化できない行の件数も返す。
{ "dataset_id": "medium", "column": "sales", "month": null,
"sum": 10478099262.01, "counted": 9329, "skipped": 671, "rows": 10000 }counted + skipped = rows が常に成立する。同じ値が D1 版と stdio 版の両方で返る。
列名は allowlist で照合している。外れると isError: true と、集計できる列の一覧を返す。
sum_csv_column(phase1 のみ)
main には存在しない。CSV 本体を引数で渡す設計は、データがまるごとモデルの
コンテキストを通るため破棄した。認証なし版の Worker と phase1 タグには残っている。
whoami
HTTP 版のみ(phase2 以降)。stdio 版には無い。引数なし。認証済みユーザの GitHub アカウント情報を返す。
{ "login": "ma2no4413", "name": "ma2no4413", "githubId": 130893267 }getMcpAuthContext() が返す props を読んでいる。props は OAuthProvider が
アクセストークンに封入したもの。GitHub のアクセストークンは意図的に保持していない
(ツールから GitHub API を叩かないため)。
サンプルデータ
examples/store-sales.csv に、店舗別・月次売上を模した架空データを置いてある。
phase1 の sum_csv_column 用(D1 / SQLite 向けのデータは scripts/gen-dataset.mjs が作る)。
集計で現実に効いてくる「汚れ」を意図的に混ぜてある。
S004(休業)—salesもcustomersも空欄S006(POS 障害)—salesだけN/Aでcustomersは生きている
同じファイルでも、集計する列によってスキップ数が変わる。
列 |
|
|
|
|
| 6,054,200 | 6 | 2 | 8 |
| 2,161 | 7 | 1 | 8 |
sales の合計は 8 店舗中 6 店舗分でしかない。skipped を返さない設計だと、
これが「8 店舗の合計」として読まれる。ツールの戻り値に信頼度の材料を含める理由がこれ。
引用符付きフィールドの壊れ方
store_id,store_name,sales
S001,"Shibuya, Tokyo",1284500
S002,"Shinjuku, Tokyo",1650000sum: 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 向けにラップしたもの。次のオプションを足している。
オプション | 既定値 | 内容 |
|
| この Worker が処理する pathname(完全一致) |
| — | CORS ヘッダ。 |
| localhost と | 受け付ける |
| 同上 | 受け付けるブラウザ |
| — |
|
これに加えて、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. createMcpHandler という名前は 3 箇所にある
インポート元 | 正体 |
| これが正解。 |
| 互換用のオーバーロード。ファクトリを渡せば同じ挙動だが、SDK v1 のサーバを渡すと deprecated 経路に落ちる |
| MCP SDK 自身のもの。Workers 向けのラッパではない |
厄介なのは 2 番目で、型が通りファクトリを渡している限り動くので気付けない。
import { createMcpHandler } from "agents/mcp/server" と、/server まで書かれているかを確認すること。
所感 — CSV を引数で渡す方式の限界(Phase 1 時点)
sum_csv_column は本命(店舗別 CSV の横断集計)の最小プロトタイプとして書いた。
実際に動かして分かった限界を残しておく。
ここに書いた 1 つ目の限界は Phase 3 で解消した。dataset_id で D1 上のデータを
指す形に移してある(経緯)。
CSV 本体を引数で渡す設計は、そのままではスケールしない。 文字列がまるごとモデルのコンテキストを通るため、 数百 KB の実データを渡した時点でトークンを食い潰す。集計は「データを送る」のではなく 「データの置き場所を指す ID を送る」形にすべきで、本命は R2/D1 に置いたデータを
dataset_idで指す設計になる。skippedを返す設計は正解だった。 数値化できない行を黙って捨てると、モデルは合計値を無条件に信じる。 スキップ件数が返っていれば「5 行中 2 行落ちています」とモデル自身が但し書きを付けられる。 ツールの戻り値は「答え」だけでなく「答えの信頼度を判断する材料」を含めるべき。エラーは例外ではなく
isError: trueで返す。 列名が無いときに実際のヘッダ一覧を添えて返すと、 モデルはその場で列名を直して再実行できる。エラーメッセージがそのままリトライの入力になる。MCP Inspector の CLI は
--tool-argに改行を含む値を渡せない。 改行で引数が分割され、 ヘッダ行だけが届いてrows: 0が返る。複数行の値を試すときは Inspector の UI を使うか、fetchで JSON-RPC を直接叩くこと(CLI のパーサの制約であって、サーバ側の問題ではない)。
Phase ごとに決めたこと
Phase 1 は「往復が通ること」と「正しい API を使っていること」だけを成果物にした。 そこから 1 つずつ足していった。各 Phase で何を決めたかを残しておく。
Phase 1 の内容は上の「なぜ McpAgent ではなく createMcpHandler なのか」と
「実装上の落とし穴」にある。
Phase 2 — 認証(OAuth 2.1)— 実装済み
@cloudflare/workers-oauth-provider が OAuth 2.1 のプロバイダ側を肩代わりし、
GitHub を上流 IdP として使う。認証済みユーザの情報はツール内から getMcpAuthContext() で読む。
この組み合わせの動く実例は、着手時点で公式に存在しなかった。
authless のデモは createMcpHandler に移行済みだが、OAuth のデモ
(cloudflare/ai/demos/remote-mcp-github-oauth) は McpAgent + Durable Objects のままで
this.props を使っている。一方ドキュメントは getMcpAuthContext() を使えと書いている。
型定義と実装を読んで確定させた結論は 3 つ。
apiHandlerは「fetchを持つオブジェクト」を正式に受け付ける。 型はExportedHandlerWithFetch<Env> | (new (ctx, env) => WorkerEntrypointWithFetch<Env>)。McpAgent.serve()はその前者を返しているだけで、特別な仕組みではない。authContextを渡す必要はない。 ハンドラがctx.propsを自動で解決する (handler-stateless.jsのresolvedAuthContext)。渡した場合は上書きになる。durable_objects/migrations/nodejs_compatはいずれも不要。 公式デモには 3 つとも入っているが、McpAgentと octokit/hono のためと思われる。
const apiHandler = {
fetch(request, env, ctx) {
return createMcpHandler(createServer)(request, env, ctx);
},
};
export default new OAuthProvider({
apiRoute: "/mcp",
apiHandler,
defaultHandler: GitHubHandler,
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
});src/github-handler.ts は公式デモを写していない。認可要求は Cookie ではなく
GitHub の state に載せて往復させ、HMAC-SHA256 で署名して改竄を防いでいる
(そのぶん承認ダイアログの実装が要らない)。GitHub のアクセストークンは props に保持しない。
この件は cloudflare/agents#2124 に報告し、 検証結果を返信してある。
Phase 3 — データの置き場所(D1)— 実装済み
sum_csv_column で分かったとおり、CSV 本体を引数で渡す設計はスケールしない。
文字列がまるごとモデルのコンテキストを通るため、実データ規模でトークンが破綻する。
「データを送る」のではなく「データの置き場所を指す ID を送る」形に変えた。
引数は
dataset_idとクエリ条件だけ。実体は D1 に置く集計はアプリ側でループを回さず D1 の SQL に寄せる
describe_datasetで列名・型・列ごとの欠損数を返す。モデルに列名を推測させない空欄も
N/Aも NULL に倒すと、COUNT(col)とCOUNT(*)の差がそのままskippedになる
SQL に寄せる理由は「CPU 制限に当たるから」ではなかった。 着手時はそう考えていたが、実測すると Free プランでも 100 万行の Worker ループ集計が通る。 正しい理由はスケールが平坦だからで、行数を 1000 倍にしても SQL 版は 0.62 → 0.69 秒、 ループ版は 0.67 → 2.65 秒だった。詳細は実測の記事に書いた。
Phase 4 — 二層構成 — 実装済み
同じツールを、HTTP(Workers + D1)と stdio(ローカル + SQLite)の両方で動かす。
ツール定義は src/tools.ts の 1 箇所にある。データソースは最小のインターフェース
(prepare / bind / all / first の 4 つだけ)で受け取るので、
ツール側に D1 も SQLite も出てこない。汎用の DB 抽象レイヤは作っていない。
src/tools.ts ツール定義(共有)
src/index.ts HTTP 版。D1 を渡す
src/stdio.ts stdio 版。node:sqlite を渡すSQLite は Node 22 同梱の node:sqlite を使うので、実行時の追加依存はゼロ。
whoami は HTTP 版にしかない。ローカルプロセスに OAuth を挟む相手がいないので、
stdio 版には返すものが無い。
npm 公開と公開レジストリへの掲載はやっていない。合成データのデモであって、 他人が入れる価値が無く、レジストリのノイズになるため。掲載手順そのものは別プロジェクトで通してある。
テスト — 入れた(Phase 4)
Phase 1 では入れていなかった。ツールが 2 本で、検証が Inspector の往復で足りているうちは、 テストを置いても実際には守られないと判断したため。
Phase 3 で集計ロジックが SQL に移った時点で条件を満たしたので、Phase 4 で入れた。
npm test# tests 7
# pass 7守っているのは「壊れても気付けない」性質のものだけ。網羅率は追っていない。
counted + skipped = rowsが常に成立すること同じデータでも列によって欠損数が変わること
monthで絞れること集計できない列・存在しない
dataset_idで、使える値を添えて失敗することdescribe_datasetが列ごとの欠損数を返すこと
インメモリの SQLite に対して実行するので、D1 も Workers も要らない。
Db インターフェースを切った副産物で、実行環境を用意せずにテストできるようになった。
テストランナーは Node 22 同梱の node:test、TypeScript は
--experimental-strip-types でそのまま実行している。追加依存はゼロ。
CI はまだ無い。手元で npm test が走ればよい規模なので、
GitHub Actions を足すのは、他人が PR を送ってくるようになってからでよいと考えている。
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Related MCP Connectors
Cloudflare Workers MCP server: interactive-api-playground
Cloudflare Workers MCP server: api-mock-server
Cloudflare Workers MCP server: ai-gateway
Cloudflare Workers MCP server: ai-model-router
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceDeploy a stateless remote MCP server on Cloudflare Workers without authentication, with custom tool support and compatibility for MCP clients.-
- FlicenseNot gradedqualityBmaintenanceA stateless MCP server for Cloudflare Workers that uses createMcpHandler to expose a simple greeting tool, requiring no persistent state.-
- AlicenseNot gradedqualityBmaintenanceDeployable stateless remote MCP server on Cloudflare Workers without auth, with support for registering custom tools and connecting to MCP clients.12MIT
- FlicenseNot gradedqualityBmaintenanceEnables deploying stateless MCP servers on Cloudflare Workers with minimal setup, using createMcpHandler to handle MCP protocol details and tool registration.-
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ma2no4413/cloudflare-mcp-sample'
If you have feedback or need assistance with the MCP directory API, please join our Discord server