REVELUP
shwldshwld8 days ago

Cloudflare Access 配下の Workers 製 MCP サーバーを OAuth 対応で公開したときの詰まりどころ

Claude Code などの MCP クライアントから Cloudflare Workers 上の MCP サーバーへOAuth接続したいとき、[@cloudflare/workers-oauth-provider](https://github.com/cloudflare/workers-oauth-provider) を使いますが、

Cloudflare Access も有効でサイト全体をそのまま保護すると接続できないことがあります。

この記事では、実際に詰まったポイントと、最終的に動作した構成をまとめます。なお、ここで紹介する構成のうち一部は Cloudflare Access や MCP の一般ルールではなく、今回のアプリ実装に依存する判断を含みます。

先に結論

Cloudflare Access でホスト全体を保護したまま MCP を同居させる こと自体は可能でした。 ただし、MCP クライアントが参照する endpoint の一部を path 単位で Bypass しないと接続できませんでした。

今回の example-app では、結果的に次のような切り分けで動作しました。

  • UI 全体は Cloudflare Access 保護のまま

  • MCP 用 metadata / token / register / api endpoint は Bypass

  • /oauth/authorize は Bypass しない

この最後の点は、今回の実装で /oauth/authorize 側が Cloudflare Access の JWT を前提にユーザー特定していることに依存しています。一般化して断定できるものではありません。

症状

最初は Cloudflare Access で example.shwld.app/* を保護していました。

その状態で MCP endpoint (example.shwld.app/api/mcp) にアクセスすると、未認証時に返ってほしい 401 Unauthorized ではなく、Cloudflare Access のログイン URL への 302 が返っていました。

例えば、次のような状態です。

  • /api/mcp -> 302

  • /.well-known/oauth-authorization-server -> 302

  • /.well-known/oauth-protected-resource/api/mcp -> 302

この状態では、MCP クライアントは OAuth フローを開始できません。

期待される正しい挙動

MCP クライアントが接続できる状態では、未認証時に少なくとも次のようなレスポンスになる必要があります。

  • /.well-known/oauth-authorization-server -> 200

  • /.well-known/oauth-protected-resource/api/mcp -> 200

  • /api/mcp -> 401

  • /api/mcp の WWW-Authenticate ヘッダーに resource_metadata=... が含まれる

この状態になって初めて、Claude Code などのクライアントが OAuth metadata を読み取り、ブラウザ認証へ進めます。

今回動いた Cloudflare Access 構成

最終的に動作したのは次の構成です。

1. UI 全体を保護する application

  • name: example-app

  • host: example.shwld.app

  • path: 空欄

  • policy: Allow

これは通常の Web UI 用です。

2. MCP endpoint を Bypass する application

  • name: example-mcp-api

  • host: example.shwld.app

  • path: api/mcp

  • policy: Bypass

3. OAuth authorization server metadata を Bypass する application

  • name: example-mcp-authz-server

  • host: example.shwld.app

  • path: .well-known/oauth-authorization-server

  • policy: Bypass

4. OAuth protected resource metadata を Bypass する application

  • name: example-mcp-protected-resource

  • host: example.shwld.app

  • path: .well-known/oauth-protected-resource*

  • policy: Bypass

5. OAuth client registration endpoint を Bypass する application

  • name: example-mcp-register

  • host: example.shwld.app

  • path: oauth/register

  • policy: Bypass

6. OAuth token endpoint を Bypass する application

  • name: example-mcp-token

  • host: example.shwld.app

  • path: oauth/token

  • policy: Bypass

/oauth/authorize はどうしたか

今回の example-app では /oauth/authorize は Bypass しませんでした。結果として、この path は root の UI 用 Access application によって通常どおり保護される状態になっていました。

これは Cloudflare Access や MCP の一般的な必須構成として断定できるものではなく、今回のアプリ実装に依存する判断です。example-app では /oauth/authorize 側で Cloudflare Access の JWT を使って認証済みユーザーを特定していたため、この path まで Bypass すると都合が悪い構成でした。

一方で、MCP クライアントが直接参照する次の endpoint は 302 で Access ログインへ飛ばされると動作しませんでした。

  • /api/mcp

  • /.well-known/oauth-authorization-server

  • /.well-known/oauth-protected-resource*

  • /oauth/register

  • /oauth/token

そのため、今回の構成では上記の path を Bypass し、/oauth/authorize は Bypass しない形に落ち着きました。

なぜこの切り分けが必要だったか

MCP クライアントは次のような流れで endpoint を使います。

  1. /api/mcp に接続する

  2. 401 と WWW-Authenticate を受け取る

  3. /.well-known/oauth-protected-resource/... を読む

  4. /.well-known/oauth-authorization-server を読む

  5. /oauth/register で client 登録する

  6. /oauth/authorize をブラウザで開く

  7. /oauth/token で token 交換する

今回の実装では、

  • metadata

  • register

  • token

  • api/mcp

が Cloudflare Access に先に捕まると失敗しました。

一方で /oauth/authorize は、今回の実装では Cloudflare Access 保護下に置いたまま、そこで認証済みユーザーを解決する前提になっていました。

ハマったポイント

api/mcp だけ Bypass しても足りなかった

最初は /api/mcp だけ Bypass すればよいように見えましたが、それでは不十分でした。

実際には、次も必要でした。

  • /.well-known/oauth-authorization-server

  • /.well-known/oauth-protected-resource*

  • /oauth/register

  • /oauth/token

このどれかが 302 を返すと、OAuth フローの途中で止まりました。

root の Access app を消すと通常ログインが壊れた

一度 root の Access app を消したところ、通常のログイン画面のボタンが飛ぶ /cdn-cgi/access/login も使えなくなりました。

今回の UI は Cloudflare Access 前提だったため、

  • UI は root app が必要

  • MCP は一部 path だけ root app を回避する必要がある

という両立が必要でした。

接続確認に使った curl

metadata 確認

curl -i https://example.shwld.app/.well-known/oauth-authorization-server
curl -i https://example.shwld.app/.well-known/oauth-protected-resource/api/mcp

期待値は、どちらも 200 です。

未認証の MCP 初期化確認

curl -i -X POST https://example.shwld.app/api/mcp \
  -H 'content-type: application/json' \
  -H 'mcp-protocol-version: 2025-03-26' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.0.0"}}}'

期待値は次のとおりです。

  • 401 Unauthorized

  • WWW-Authenticate ヘッダーあり

register / token が Access に捕まっていないか確認

curl -i https://example.shwld.app/oauth/register
curl -i https://example.shwld.app/oauth/token

期待値は次のとおりです。

  • 302 ではない

  • アプリ由来の 405 や 401 が返る

実際の最終確認

Claude Code から /mcp で接続した結果は次でした。

Authentication successful. Connected to example-app.

ここまで出れば、Cloudflare Access 側の path 切り分けは成功していると判断できます。

まとめ

今回の構成では、Cloudflare Access 配下に UI と MCP を同居させるために、MCP クライアントが参照する path だけを Bypass する必要がありました。

重要だったのは次の 2 点です。

  • metadata / register / token / api endpoint は Bypass

  • /oauth/authorize は Bypass しない

ただし後者は、今回のアプリ実装に依存する判断です。同じ Cloudflare Access と MCP の組み合わせでも、認証の持ち方によって別の構成を取り得ます。

出典

  • Cloudflare, "Secure MCP servers with Access for SaaS" https://developers.cloudflare.com/cloudflare-one/access-controls/ai-controls/saas-mcp/

  • Cloudflare, "Application paths" https://developers.cloudflare.com/cloudflare-one/access-controls/policies/app-paths/

  • Cloudflare, "Access policies" https://developers.cloudflare.com/cloudflare-one/policies/access/

  • Model Context Protocol, "Authorization" https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization

shwldshwld

Cloudflare Access 配下の Workers 製 MCP サーバーを OAuth 対応で公開したときの詰まりどころ

8 days ago

2025年に聴いたAudible 67本まとめ

4 months ago

個人開発アプリにMCP/DXT統合を実装してClaude Desktopから操作できるようにした感動体験

8 months ago

小学一年生の娘がサンタに感じる不信感11選

a year ago

ストリートファイター6でマスターランク到達できたときのメモ

a year ago

Line Notifyサービス終了。移行先どうしよう

2 years ago