workers-oauth-provider v1、MCP の認可サーバーを別 Worker に分ける構成
Cloudflare の workers-oauth-provider v1 で認可サーバーと MCP サーバーを別 Worker に分離できる。MCP 2026-07-28 対応と 0.x からの移行点を整理する。
Cloudflare の workers-oauth-provider v1 で認可サーバーと MCP サーバーを別 Worker に分離できる。MCP 2026-07-28 対応と 0.x からの移行点を整理する。
Cloudflare Workers で MCP サーバーの OAuth を組んでいるなら、@cloudflare/workers-oauth-provider の v1 で取れる構成が一つ増えた。サインインとトークン発行は認可サーバー役の Worker に任せ、MCP サーバーはリソースサーバーとして別の Worker で動かせる。トークンを受け取った MCP サーバーは Service Binding で認可サーバーに検証を頼むので、その通信は公開インターネットを通らない。Cloudflare の changelog(2026年10月1日)にそう書かれている。
今の構成をすぐに作り替える必要はない。認可サーバーと MCP サーバーを OAuthProvider 一つで兼ねる従来の書き方も v1 で使える。changelog によると、0.x の大半のデプロイで必須の変更は resourceMetadata: { resource } の追加だけだ。ただし公式 migration guide では、使っている API によって追加変更が必要になる。例えば resourceMatchOriginOnly は削除、resolveExternalToken は audience の返却が必要で、該当するコードがないか先に検索するよう案内している。
changelog が挙げる分割のメリットは、どれも運用面の話だ。
サインイン画面を狙うトラフィックと MCP の呼び出しは性質が違う。Worker ごとにルールを変えられれば、片方に合わせて締めたルールでもう片方が詰まることがなくなる。
公式の例から、calendar MCP の Worker 部分を抜き出す。
// calendar MCP Worker: checks tokens with AuthServer over a Service Binding.
export const calendar = new OAuthResourceServer<Env, AuthProps>({
resourceMetadata: {
resource: "https://calendar.example.com/mcp",
authorization_servers: ["https://auth.example.com"],
},
requiredScopes: ["calendar:read"],
validateToken: (env) => env.AUTH_SERVER.validateToken,
handler: {
fetch(request, env, ctx) {
if (
request.method === "POST" &&
!ctx.auth.scope.includes("calendar:write")
) {
return insufficientScope(ctx.auth, ["calendar:read", "calendar:write"]);
}
return handleMcp(request, ctx.props);
},
},
});
env.AUTH_SERVER.validateToken の呼び出しが Service Binding だ。バインディングは wrangler の設定で、認可サーバー側 Worker のエントリポイント AuthServer を指す。
{
"name": "calendar-mcp",
"main": "src/index.ts",
// Set this to today's date
"compatibility_date": "2026-10-02",
"services": [
{
"binding": "AUTH_SERVER",
"service": "auth-server",
"entrypoint": "AuthServer",
},
],
}
OAuthResourceServer は、MCP クライアントが認可サーバーを見つけるための RFC 9728 の protected resource metadata を公開する。トークンなしのリクエストには、そのメタデータを指す 401 のチャレンジを返す。別のリソース向けに発行されたトークンは拒否するので、同じ認可サーバーから calendar と drive の両方にトークンを出しても、drive 用のトークンで calendar は叩けない。
認可サーバー側は OAuthAuthorizationServer に issuer、発行先の resources(例では calendar と drive の 2 つの URL)、scopesSupported を渡す。WorkerEntrypoint を継承した AuthServer クラスが validateToken(resource, token) を公開し、これがリソースサーバーから呼ばれる。/authorize は自前の同意画面に回し、それ以外のパスはライブラリの fetch に流す形になっている。
v1 は MCP 2026-07-28 の認可仕様に対応し、Client ID Metadata Documents と issuer identification を含む。changelog は、Dynamic Client Registration を使う古いクライアントも引き続き動くとしている。
公式の例では、認可サーバーの設定に clientIdMetadataDocumentEnabled: true を明示している。省略した場合の既定値は changelog からは分からない。新しい仕様のクライアントを受け入れたいなら、この値を明示しておくほうが確実だ。
上のコードの insufficientScope() が step-up 認可の部分だ。最初は requiredScopes で calendar:read だけを要求し、書き込み(POST)の時点で calendar:write がなければ、必要なスコープを添えて追加の認可を求める。changelog の言い方では「1行で」書ける。返るレスポンスの具体的な形式までは changelog に載っていない。
更新は npm i @cloudflare/workers-oauth-provider@latest(yarn、pnpm、bun の例もある)。npm パッケージに移行用の skill が同梱されていて、コーディングエージェントに node_modules/@cloudflare/workers-oauth-provider/skills/migrate-to-1.0/SKILL.md を読ませれば更新作業を任せられる。
手で進める場合、公式 migration guide の「Am I affected?」で自分のコードにある項目だけを拾う。0.x から 1.x では少なくとも次を確認する。
OAuthProvider に resourceMetadata がなければ canonical resource を追加するresourceMatchOriginOnly を使っていれば削除するresolveExternalToken を使っていれば、検証したトークンの audience を返すrequiredScopes、redirect URI、トークンやクライアントの有効期限など、guide に列挙された後続変更も使用箇所だけ確認する分割 API は必須ではない。既存 OAuthProvider の互換対応を先に終え、その後に WAF やレート制限を分けたい場合だけ Worker を分割する、という段階的な移行もできる。v1 ではほかに次のものが入った。
refreshTokenIdleTTL によるリフレッシュトークンのスライディング有効期限purgeExpiredData() による再開可能な KV のクリーンアップonError に渡るすべてのエラーへの内部 reason の付与自前で同意画面や上流 IdP へのサインインを書いている場合は、confused deputy 対策のヘルパーに置き換えられるかを最初に確認しておきたい。