Cloudflare Workers でリモート MCP サーバーを作るなら、2026 年 10 月時点では agents パッケージの createMcpHandler(ステートレスなハンドラー)を使います。これまで定番だった McpAgent は 2026 年 7 月に非推奨・機能凍結になりました1。認証は @cloudflare/workers-oauth-provider の OAuthProvider で /mcp を包むだけで OAuth 2.1 になり、Claude Code からは claude mcp add --transport http でつながります。この記事では、テンプレートからの作成、OAuth での保護、wrangler dev と curl での確認、Claude Code からの接続までを、筆者が手元で実行した結果つきで説明します。
リモート MCP サーバーは、インターネット越しに HTTP で呼べる MCP(Model Context Protocol、AI とツールをつなぐ標準プロトコル)サーバーです。手元で npx 起動する stdio 型と違い、URL を渡すだけでチームの誰の Claude Code からも同じツールを使えます。
Cloudflare の公式ドキュメントは、リモート接続の標準の通信方式を Streamable HTTP(1 つの HTTP エンドポイントで双方向にやり取りする方式)とし、SSE(Server-Sent Events)方式は非推奨としています2。Workers で作ると、次の点が楽になります。
- サーバー管理が要らない:
wrangler deploy 1 回で https://<名前>.<サブドメイン>.workers.dev/mcp に公開できる3
- ステートレスで載せやすい: MCP 仕様 2026-07-28 版でセッションと
initialize ハンドシェイクが不要になり、リクエスト単位で完結する Workers と相性がよくなった4
- OAuth を自前で書かなくてよい:
workers-oauth-provider が認可サーバーの役(クライアント登録・トークン発行・検証)を引き受ける5
全体の流れは次の図のとおりです。Claude Code は最初トークン無しで /mcp を叩き、401 の応答から認可サーバーを見つけて、ブラウザで同意したあとトークン付きで呼び直します。
Claude Code がトークン無しで 401 を受け取り、OAuthProvider で同意とトークン発行を経て、createMcpHandler のツールを呼ぶまでの流れ図: 筆者作成
理由は、2026 年 7〜8 月に MCP 仕様と Cloudflare の SDK が大きく変わったからです。2025 年に書かれた記事のコード(McpAgent と /sse)は、今から新しく作るには勧められません。
| 時期 | 出来事 |
|---|
| 2026-07-27 | Agents SDK v0.20.0 が MCP 仕様 2026-07-28 に対応。createMcpHandler が SDK v2 のサーバーを作る関数(ファクトリ)を受け取る形になり、McpAgent は機能凍結1 |
| 2026-07-28 | Cloudflare のドキュメントが「新しい McpAgent サーバーは作らず、ステートレスな createMcpHandler を使う」と明記2 |
| 2026-08-06 | Cloudflare ブログが新仕様を解説。ハンドシェイクと Mcp-Session-Id の廃止、server/discover、Mcp-Method / Mcp-Name ヘッダー、動的クライアント登録(DCR)の非推奨化など4 |
検索の関心も落ちていません。Google トレンド(日本・過去 90 日、2026-10-05 取得)の相対値で、「MCP サーバー」は直近 4 週の平均が 5.0、その前の 8 週の平均が 4.8 とほぼ同じ水準を保ちました。「Cloudflare Workers」は 2.0 と 0.8 で、値は小さいものの上向きです6。
Google トレンドの相対値(日本・過去 90 日。直近 4 週平均と前 8 週平均)データを表で見る
Google トレンドの相対値(日本・過去 90 日。直近 4 週平均と前 8 週平均)| 語 | 前8週 | 直近4週 |
|---|
| MCP サーバー | 4.8 | 5 |
|---|
| Cloudflare Workers | 0.8 | 2 |
|---|
| Hono | 0.8 | 2.7 |
|---|
| AWS Lambda | 1.9 | 2.3 |
|---|
出典: Google トレンド(日本・過去 90 日、2026-10-05 取得)の相対値。複数回の取得(Hono は別の回)。どの回も基準語 GitHub Copilot の直近 4 週平均は 26.7 で同じ尺度
Cloudflare 公式の MCP サーバーを使う側の話は Cloudflare の MCP サーバー(Code Mode)を Claude Code につなぐ にまとめています。この記事は「自分で作る側」です。
新しく作るなら createMcpHandler 一択です。違いは「プロトコルのセッションを Durable Object(状態を持つ Workers の実行単位)に持つかどうか」です。
| 項目 | createMcpHandler(推奨) | McpAgent(非推奨) |
|---|
| 状態 | ステートレス。リクエストごとにサーバーを作る | Durable Object にセッションを持つ |
| 対応仕様 | MCP 2026-07-28 と、従来のクライアントのステートレスな要求7 | 従来の仕様 |
| 必要な設定 | Workers だけ(Durable Object のバインディング不要) | Durable Object のバインディングとマイグレーション |
| 公式の扱い | 新規はこちら2 | 機能凍結。削除時期は未発表1 |
| アプリのデータ | Durable Objects・D1・KV・R2 に明示的に置く8 | セッションの状態に置けた |
createMcpHandler の主なオプションは次のとおりです(2026 年 10 月時点のドキュメント)7。
| オプション | 既定値 | 用途 |
|---|
route | "/mcp" | MCP を受けるパス |
legacy | "stateless" | 従来のクライアントを受けるか("reject" で拒否) |
responseMode | "auto" | "json" / "sse" で応答形式を固定 |
allowedHostnames | localhost か workers.dev | カスタムドメインで公開するときに足す |
allowedHostnames の既定は localhost と workers.dev なので、独自ドメインで公開するときは忘れずに足してください。
まず認証なしの最小構成を作り、ツールが見えることを確かめます。前提は Node.js 24(筆者は v24.6.0)です。
npm create cloudflare@latest -- remote-mcp-server-authless --template=cloudflare/ai/demos/remote-mcp-authless
cd remote-mcp-server-authless
このコマンドは公式ガイドのものです3。2026-10-05 に作ると、src/index.ts はすでに createMcpHandler と @modelcontextprotocol/server(MCP の TypeScript SDK v2)を使う形になっていました。要点だけ抜き出すと次のとおりです。
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler } from "agents/mcp/server";
import { z } from "zod";
function createServer() {
const server = new McpServer({ name: "Authless Calculator", version: "1.0.0" });
server.registerTool(
"add",
{ inputSchema: z.object({ a: z.number(), b: z.number() }) },
async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }),
);
return server;
}
const handler = createMcpHandler(createServer);
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return handler(request, env, ctx);
},
} satisfies ExportedHandler<Env>;
ポイントは 3 つです。
createServer はサーバーを返す関数で、リクエストのたびに呼ばれる。グローバル変数に状態を置かない
- ツールは
server.registerTool(名前, { description, inputSchema }, 処理) で登録する。SDK v1 の server.tool() ではない8
wrangler.jsonc に Durable Object の設定は要らない
型チェックとデプロイの予行演習は次のコマンドで通りました(Wrangler 4.147.0)。
npx tsc --noEmit
npx wrangler deploy --dry-run
npx wrangler dev で起動し、curl で tools/list を送ります。MCP 2026-07-28 版では、リクエストごとに MCP-Protocol-Version ヘッダーと、params._meta にプロトコルの版・クライアント情報を入れます。_meta が無いと、サーバーは「必要な _meta が無い」という Invalid params エラーを返しました。
curl -s http://localhost:8787/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0.0"},
"io.modelcontextprotocol/clientCapabilities":{}}}}'
筆者の環境では 8787 番が別のプロセスに使われていたため、--port 8931 で起動して確かめました。応答は次のとおりです(JSON Schema の中身は一部省略)。
{"result":{"tools":[{"name":"add","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]}},{"name":"calculate","inputSchema":{...}}],"resultType":"complete","ttlMs":0,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Authless Calculator","version":"1.0.0"}}},"jsonrpc":"2.0","id":2}
ttlMs と cacheScope は、新仕様で入ったキャッシュのヒントです4。同じ方法で server/discover を送ると "supportedVersions":["2026-07-28"] が返り、tools/call で add に 2 と 3 を渡すと "text":"5" が返りました。
従来の仕様のクライアント向けに、initialize(protocolVersion: "2025-11-25")を送っても serverInfo が返り、そのまま tools/list も通りました。1 つの /mcp で新旧両方のクライアントを受けられるというドキュメントの説明どおりです7。
wrangler dev で起動した MCP サーバーに curl で tools/list を送り、OAuth 版はトークン無しで 401 になり、Claude Code から add ツールを呼んで 5 が返るまで2026-10-05 に筆者が実行した出力。長い JSON とコマンドは … で省略。8931 が認証なし版、8932 が OAuth 版
ブラウザで確かめたいときは、公式ガイドのとおり MCP Inspector を使えます3。
npx @modelcontextprotocol/inspector@latest
公開する MCP サーバーには認証を付けます。OAuthProvider で Worker 全体を包むと、/mcp にはトークンを検証済みのリクエストだけが届き、それ以外のパス(/authorize など)は自分で書くハンドラーに回ります5。
npm i agents@0.26.0 @modelcontextprotocol/server@2.0.0 @cloudflare/workers-oauth-provider@1.2.1 zod@4
npm i -D wrangler typescript
agents@0.26.0 は @modelcontextprotocol/server を 2.0.0 ちょうどで peer 依存にしています。最新の 2.3.0 を入れると npm i が ERESOLVE で止まりました(2026-10-05 時点)。
workers-oauth-provider は、トークンと認可コードを OAUTH_KV という名前で束ねた KV に保存します5。
npx wrangler kv namespace create "OAUTH_KV"
{
"name": "my-remote-mcp",
"main": "src/index.ts",
"compatibility_date": "2026-07-02",
"compatibility_flags": ["nodejs_compat"],
"kv_namespaces": [{ "binding": "OAUTH_KV", "id": "<KV の ID>" }],
"dev": { "port": 8788 }
}
ログインしたユーザーは getMcpAuthContext() で取れます。props には、同意のときに completeAuthorization() へ渡した値が入ります9。
import { McpServer } from "@modelcontextprotocol/server";
import { getMcpAuthContext } from "agents/mcp/server";
import { z } from "zod";
export function createServer() {
const server = new McpServer({ name: "my-remote-mcp", version: "1.0.0" });
server.registerTool(
"add",
{ description: "2 つの数を足す", inputSchema: z.object({ a: z.number(), b: z.number() }) },
async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }] }),
);
server.registerTool(
"whoami",
{ description: "OAuth でログインしたユーザーを返す", inputSchema: z.object({}) },
async () => {
const auth = getMcpAuthContext();
const userId = (auth?.props.userId as string | undefined) ?? "anonymous";
return { content: [{ type: "text", text: `userId: ${userId}` }] };
},
);
return server;
}
workers-oauth-provider 1.x では resourceMetadata.resource(クライアントが接続する URL)が必須です。これが全トークンの宛先(audience)になります。http はループバック(localhost)だけで許されるので、本番では https://…workers.dev/mcp に書き換えます5。
import OAuthProvider, { type OAuthHelpers } from "@cloudflare/workers-oauth-provider";
import { createMcpHandler } from "agents/mcp/server";
import { createServer } from "./server";
type AuthEnv = Env & { OAUTH_PROVIDER: OAuthHelpers };
const MCP_RESOURCE = "http://localhost:8788/mcp";
const mcpHandler = createMcpHandler(createServer);
const authHandler: ExportedHandler<AuthEnv> = {
async fetch(request, env) {
if (new URL(request.url).pathname !== "/authorize") return new Response("Not found", { status: 404 });
const oauth = env.OAUTH_PROVIDER;
if (request.method === "GET") {
const authReq = await oauth.parseAuthRequest(request);
const details = await oauth.describeConsent(authReq);
const consent = await oauth.beginConsent(authReq);
consent.headers.set("Content-Type", "text/html; charset=utf-8");
const esc = (s: string) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
const html = `<!doctype html><meta charset="utf-8">
<h1>${esc(details.clientName)} に MCP サーバーへのアクセスを許可しますか?</h1>
<p>送り先: ${esc(details.redirectHost)}</p>
<form method="post">
<input type="hidden" name="handle" value="${esc(consent.handle)}">
<button name="decision" value="approve">許可</button>
<button name="decision" value="deny">拒否</button>
</form>`;
return new Response(html, { headers: consent.headers });
}
const form = await request.formData();
const handle = String(form.get("handle"));
if (form.get("decision") !== "approve") {
const denied = await oauth.denyConsent(request, handle);
return new Response(null, { status: 302, headers: denied.headers });
}
const approved = await oauth.approveConsent(request, handle);
const userId = "demo-user";
const { redirectTo } = await oauth.completeAuthorization({
request: approved.request,
userId,
metadata: {},
scope: approved.request.scope,
props: { userId },
});
approved.headers.set("Location", redirectTo);
return new Response(null, { status: 302, headers: approved.headers });
},
};
export default new OAuthProvider<AuthEnv>({
apiRoute: "/mcp",
apiHandler: { fetch: (req, env, ctx) => mcpHandler(req, env, ctx) },
defaultHandler: authHandler,
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
resourceMetadata: { resource: MCP_RESOURCE, authorization_servers: [new URL(MCP_RESOURCE).origin] },
});
同意画面の作りは、ライブラリ同梱の「consent page」ドキュメントに沿っています。クライアント名は攻撃者が決められる値なのでエスケープし、beginConsent() が返すヘッダー(フレーム埋め込みの禁止とブラウザに結びついた Cookie)を必ず付けます。
このコードは npx tsc --noEmit と npx wrangler deploy --dry-run を通りました(Total Upload: 1428.94 KiB / gzip: 255.49 KiB)。
wrangler dev で起動し(筆者は 8932 番で確認)、トークン無しで /mcp を叩くと 401 が返ります。WWW-Authenticate の resource_metadata が、クライアントが認可サーバーを探す入口です。
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="OAuth", resource_metadata="http://localhost:8932/.well-known/oauth-protected-resource/mcp"
続けて、curl で次の順に実行しました。
/register にクライアントを登録(DCR)→ client_id が返る
- PKCE の
code_challenge を付けて /authorize を GET → 上の同意画面の HTML が返る
- 「許可」を POST →
Location ヘッダーで http://localhost:33418/callback に code と state、iss を付けてリダイレクトされる
/token に code と code_verifier を送る → token_type: bearer、expires_in: 3600、refresh_token が返る
Authorization: Bearer <トークン> で tools/call の whoami を呼ぶ → "text":"userId: demo-user"
/.well-known/oauth-authorization-server の応答では、code_challenge_methods_supported が ["S256"]、registration_endpoint が /register でした。
Claude Code では、HTTP の MCP サーバーを次のコマンドで追加します10。
claude mcp add --transport http my-remote-mcp https://my-remote-mcp.<サブドメイン>.workers.dev/mcp
claude mcp add --transport http --scope project my-remote-mcp https://my-remote-mcp.<サブドメイン>.workers.dev/mcp
OAuth が必要なサーバーは、Claude Code の中で /mcp を開くか、シェルで claude mcp login <名前> を実行するとブラウザが開きます。トークンは安全に保存され、自動で更新されます10。
筆者は認証なし版を --scope project で追加し、claude -p で呼べることを確かめました(Claude Code 2.1.289)。
claude mcp add --transport http --scope project calc http://localhost:8931/mcp
claude -p "calc の add ツールで 2 と 3 を足して、結果だけ答えて" \
--mcp-config .mcp.json --strict-mcp-config --allowedTools "mcp__calc__add"
.mcp.json に書いたプロジェクトスコープのサーバーは、最初に claude を起動したときに承認するまで Pending approval と表示されます。claude mcp list で状態を確かめてから使ってください。
本番に出すときは次の順で進めます3。
npx wrangler kv namespace create "OAUTH_KV"
npx wrangler deploy
自作のリモート MCP サーバーが役に立つのは、「社内にしかない情報や操作」を Claude Code に渡したいときです。
| 場面 | ツールの例 | 置き場所 |
|---|
| 設計 | 社内 API の仕様書や ADR を検索する search_docs | R2 や D1 に置いた文書を読む |
| 実装 | 開発用 DB のスキーマを返す describe_table | D1 |
| レビュー | 社内のコーディング規約を返す get_guideline | KV |
| 運用 | 検証環境のフラグを切り替える toggle_flag | Durable Objects |
ステートレスになったので、ツールの処理で使うデータは D1・KV・R2・Durable Objects に明示的に置きます。公式の移行ガイドも、プロトコルのセッション ID ではなく、アプリが発行したハンドルでデータを指すよう勧めています8。
Workers 自体の作り方は Claude Code で Cloudflare Workers アプリを作る全手順 を、Go で MCP サーバーを書く場合は Go で MCP サーバーを作る:公式 Go SDK 入門 を、状態を持つエージェントそのものを作るなら Cloudflare Agents SDK で AI エージェントを作る を参照してください。
| 症状 | 原因 | 対処 |
|---|
npm i が ERESOLVE で止まる | agents@0.26.0 が @modelcontextprotocol/server@2.0.0 を peer 依存で固定 | @modelcontextprotocol/server@2.0.0 を指定して入れる |
Property 'resourceMetadata' is missing | workers-oauth-provider 1.x で必須になった | resourceMetadata: { resource: "…/mcp" } を足す |
Invalid params: … missing the required per-request envelope key(s): _meta | MCP-Protocol-Version: 2026-07-28 を付けたのに _meta が無い | params._meta に版とクライアント情報を入れる |
wrangler dev が Address already in use で落ちる | 8787 / 8788 番を別の Worker が使っている | --port と --inspector-port を変える。OAuth 版は MCP_RESOURCE のポートもそろえる |
| カスタムドメインからのリクエストが通らない | allowedHostnames の既定は localhost と workers.dev | createMcpHandler のオプションに足す7 |
セキュリティ面では、次の 3 点を守ってください。
- この記事の同意画面はデモです。誰でも
demo-user としてトークンを得られます。本番では /authorize で Cloudflare Access や GitHub などにサインインさせます。GitHub 経由の流れは beginUpstream() / finishUpstream() を使う手順がライブラリのドキュメントにあります
- DCR は新仕様で非推奨です。MCP 2026-07-28 版は事前登録、次に Client ID Metadata Document(CIMD)を優先し、DCR は 2027 年夏以降に削除予定とされています4。この記事では Claude Code など既存クライアントとの互換のために
/register を残しています
- 公式の GitHub OAuth テンプレートはまだ
McpAgent です。remote-mcp-github-oauth の 2026-09-11 時点のコードは McpAgent と SDK v1 を使っていました。参考にするときは、ツール部分を createMcpHandler の形に書き換えます8
- 2026 年 10 月時点、Workers のリモート MCP サーバーは
createMcpHandler(ステートレス)で作る。McpAgent は機能凍結
- 通信は Streamable HTTP の
/mcp 1 本。MCP 2026-07-28 版のクライアントも従来のクライアントも同じルートで受けられる
- OAuth は
OAuthProvider で /mcp を包み、OAUTH_KV と resourceMetadata.resource を設定する
- Claude Code からは
claude mcp add --transport http で追加し、/mcp か claude mcp login で認証する
- 次にやること: 同意画面を実際のサインインに置き換え、
wrangler deploy して MCP_RESOURCE を本番 URL にする
すぐに止まるわけではありません。Cloudflare は McpAgent を機能凍結としていますが、削除の時期は発表していません1。移行ガイドには isLegacyRequest() で新旧のリクエストを振り分け、両方のルートを並行して動かす方法が載っています8。
新しく作るなら不要です。Cloudflare のドキュメントは SSE を非推奨とし、Streamable HTTP を標準としています2。Claude Code も SSE を非推奨としており、HTTP を先に試します10。
MCP のセッションを持つためには使いません。ツールが扱うアプリのデータを持つ場所としては、D1・KV・R2 と並んで今も選択肢です8。
Streamable HTTP と OAuth 2.1 に対応した MCP クライアントならつながります。ブラウザで試すなら MCP Inspector(npx @modelcontextprotocol/inspector@latest)に /mcp の URL を入れます3。
筆者の wrangler dev での実行では、/token の応答の expires_in は 3600 秒でした。リフレッシュトークンも同時に発行され、Claude Code は自動で更新します10。