cloudflare-mcp-sample

cloudflare-mcp-sample

A stateless MCP server on Cloudflare Workers providing a ping tool and a CSV column summarizer, demonstrating the createMcpHandler API.

Category
Visit Server

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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured