Skip to main content
Glama
ma2no4413

cloudflare-mcp-sample

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-cloudflarewranglerengines: { node: ">=22.0.0" } を宣言していて、 Node 20 では警告ではなく Wrangler requires at least Node.js v22.0.0.停止する

@modelcontextprotocol/server にキャレット (^2.0.0) を付けないこと。 agents@0.20.1peerDependencies"@modelcontextprotocol/server": "2.0.0"完全一致で固定されている。 公式ドキュメントも "Use the exact MCP versions required by your installed Agents release." と書いている。

Related MCP server: remote-mcp-server-authless

セットアップ

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 deploySuccess! を返し、警告 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(休業)— salescustomers も空欄

  • S006(POS 障害)— sales だけ N/Acustomers は生きている

同じファイルでも、集計する列によってスキップ数が変わる。

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 なのか

McpAgentdeprecated かつ機能凍結されている。公式ドキュメントの Handler API が 明示的に "McpAgent is deprecated and feature-frozen" と書いており、新規サーバは createMcpHandler を使うことになっている。createLegacyMcpHandler はレガシー移行専用。

構造的な理由は状態の置き場所にある。

  • McpAgent は Durable Object にセッション状態を持たせる設計だった。接続ごとに DO インスタンスが立つ。

  • createMcpHandlerステートレス。リクエストごとにファクトリ関数から McpServer を作って捨てる。 状態が要るなら D1 / KV / R2 / Durable Objects に明示的に置く。

状態を持たないサーバに DO を強制されないぶん、コールドスタートも課金も素直になる。 今回のような「引数を受け取って計算して返すだけ」のツールに、セッションは要らない。

agents/mcp/servercreateMcpHandler は、実体は createStatelessMcpHandler の別名で、 MCP SDK 側の createMcpHandler を Workers 向けにラップしたもの。次のオプションを足している。

オプション

既定値

内容

route

"/mcp"

この Worker が処理する pathname(完全一致)

corsOptions

CORS ヘッダ。false で無効化

allowedHostnames

localhost と workers.dev

受け付ける Host

allowedOriginHostnames

同上

受け付けるブラウザ OriginOrigin なしの非ブラウザクライアントは常に通る

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 のプロバイダ側を肩代わりさせ、 認証済みユーザの情報を createMcpHandlerauthContext 経由でツールに渡す。 ツール内では 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 に移り、 「壊れたら気付けない」状態になった時点で入れる。

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Related MCP Servers

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

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