株式会社ネットワールドのエンジニアがお届けする技術情報ブログです。
各製品のエキスパートたちが旬なトピックをご紹介します。

Cloudflare Workers で作った MCP サーバーを企業 IdP で守る(Okta で検証)― AI エージェントから社内データへアクセスさせる時の権限設計

Table of Contents

こんにちは。ネットワールドで Cloudflare を担当しております。

Cloudflare Workers 上に MCP サーバーを作り、Okta のカスタム認可サーバーで保護する構成を実際に組んでみました。2本立ての1本目です。

記事の目的
AI エージェントに社内データを触らせるとき、「誰が」「どこまで」を制御する具体的な作り方を、実機の画面とともに示すことです。
検証の背景
この検証のきっかけは、2026年7月に Cloudflare の MCP サーバーポータルへ追加された手動 OAuth (Static OAuth) です。ポータルが上流の MCP サーバーへ接続する際、従来は動的クライアント登録が使えるプロバイダーしか選べず、企業で使われている多くの IdP はそのままでは上流に据えられませんでした。手動 OAuth はこの制約を外しています。これを実機で確かめるのが今回の企画です。ただし検証するには、Okta で保護された MCP サーバーが手元になければ始まりません。本記事は、その土台を作る回にあたります。
登場する製品の位置づけ
本検証の主役は、後編で扱う Cloudflare One の MCP サーバーポータル(手動 OAuth)です。Okta は「事前登録型で使う企業向け IdP」の代表例として、Workers 上の自作 MCP サーバーは「OAuth で保護された上流 MCP サーバー」の検証用の代役として使っています。同種の IdP や任意の上流 MCP サーバーに読み替えられる内容です。
今回検証した範囲
本記事(A編)で扱うのは、Cloudflare Workers 上に自作した MCP サーバーを Okta のカスタム認可サーバーで保護し、Claude から直接つないで、スコープとグループの2軸でツール単位の可否を制御できるかまでです。実際に権限を落として拒否される様子まで確認します。MCP サーバーポータルと手動 OAuth 本体の検証は、後編(B編)で扱います。
検証時点と前提
2026年8月時点の検証です。Cloudflare は Enterprise プラン、Okta は API Access Management が有効な組織を使用しています。MCP の認可仕様は改訂が続いている領域なので、画面や挙動が変わっている可能性があります。最新の情報は公式ドキュメントをご確認ください。

1. なぜこれを検証したのか

社内で AI の活用を進めようとすると、遅かれ早かれ同じ壁に当たります。「社内のドキュメントを AI に読ませたい。でも、契約情報や人事情報まで全員に見せるわけにはいかない」という壁です。

技術的に AI と社内システムを繋ぐこと自体は、そこまで難しくありません。API キーを1本発行して、AI にそれを持たせればいい。問題はその先です。

共有 API キー方式でできないこと なぜ困るか
誰がアクセスしているのか分からない 監査ログを見ても、全員が同じ「APIキー」として記録される
人によって見える範囲を変えられない 一番権限の低い人に合わせるか、全員に全部見せるかの二択になる
退職・異動時に個別に止められない キーを作り直して全員に配り直すことになる

結局のところ、「AI が誰の代わりに動いているのか」を、システム側が分かっていないのが原因です。ここを解決しないと、本番の業務データには繋げられません。

Cloudflare でどこまでできるか

この課題に対して、Cloudflare には必要な部品が一通り揃っています。

やりたいこと 使う Cloudflare の機能 扱う記事
MCP サーバーを動かす Cloudflare Workers 本記事(A編)
複数の MCP サーバーの入口をひとつにする MCP サーバーポータル(Cloudflare One) 後編(B編)
その入口を ID 基盤で守る Cloudflare Access 後編(B編)
誰がどのツールを呼んだか記録する ポータルの実行ログ 後編(B編)

本記事では、まず一番小さい単位から始めます。Workers 上に MCP サーバーを1つ作り、それを Okta で守る。ここが成立しないと、複数サーバーをまとめる話にも進めないためです。後編で扱う MCP サーバーポータルは、2026年7月に手動 OAuth という機能が追加されたことで、Dynamic Client Registration(動的クライアント登録)が使えない環境、つまり DCR 非対応、あるいは無認証での登録を許していない IdP を上流に据えられるようになりました。そちらが本命ですが、まずは土台からです。

2. MCP とは何か

本題に入る前に、前提となる MCP について少しだけ触れます。既にご存じの方は読み飛ばしてください。

MCP(Model Context Protocol)は、AI に外部の道具を使わせるための共通規格です。AI 単体では社内のデータベースも社内 Wiki も見られませんが、「こういう道具がありますよ」と教えてあげる仕組みがあれば、AI はそれを呼び出せます。

登場人物は3つです。

 

「ツール」は要するに関数です。名前と、受け取る引数と、やることが決まっている。MCP サーバーは「うちにはこういうツールがあります」という一覧を返し、AI はその中から必要なものを選んで呼びます。

ここで重要なのは、ツールを呼ぶかどうかを決めるのは AI ですが、呼ばせてよいかどうかを決めるのはサーバー側だという点です。今回の話は、まさにこの「呼ばせてよいか」の作り方です。

3. 今回作るもの

社内ナレッジを扱う MCP サーバーを Cloudflare Workers 上に作りました。ツールは3つです。

ツール やること 誰が使えるか
whoami 今使っているトークンの中身を見せる 接続できた人全員
wiki_search 社内ナレッジを検索する 接続できた人全員
contract_lookup 顧客の契約情報を参照する 特定のグループの人だけ

なぜ3つなのかを先に説明しておきます。この3つは、それぞれ違う役割を持っています。

whoami検証のための計測器です。実際の業務では要らないツールですが、これがあると「今このトークンには何の権限が載っているのか」が目で見えます。認可の仕組みを検証するとき、これが無いと何が起きているのか分かりません。

wiki_searchcontract_lookup対比のためのペアです。前者は誰でも使える一般ツール、後者は限られた人だけの特権ツール。この2つが同じサーバーに同居していて、同じ人が接続したときに片方だけ弾かれる、という状態を作りたかったのです。

4. 認可の設計:2つの軸を使い分ける

ここが今回の記事で一番伝えたい部分です。

OAuth には「スコープ」という仕組みがあります。トークンに「この権限を持っている」という印を付けるもので、今回で言えば wiki.read がそれにあたります。では contract_lookup 用に contract.read というスコープを作ればいいかというと、それではうまくいきません。

スコープだけでは足りない理由

ホテルのカードキーで例えます。

  ホテル OAuth
スコープ カードキーに書き込まれた「開けられる扉」の一覧 トークンに載る権限の一覧
グループ 宿泊者名簿の「この人はスイート客」という区分 ID基盤側の所属情報

カードキーを発行するとき、フロントは名簿を見て「この人はスイート客だからラウンジも開けられるキーを作ろう」と判断します。つまり名簿(グループ)が先にあって、キー(スコープ)はその結果です。

問題は、キーを発行する側が「全員に同じキーを配る」設計になっている場合です。実は後編で扱う Cloudflare の MCP サーバーポータルがまさにそれで、管理者が登録したスコープの一覧が全利用者に一律で使われます。ここで特権的なスコープを混ぜると、権限のない人にまでそれを要求してしまい、トークンの発行自体が失敗します。

そこでスコープは全員共通にして、人による差はグループで表現する設計にしました。

 
この設計の利点 権限を変えたいとき、Okta のグループにユーザーを足し引きするだけで済みます。認可サーバーの設定も、Workers のコードも、Claude 側の接続設定も、一切触る必要がありません。情シスの日常運用にそのまま乗ります。

5. Okta 側を用意する

前提:カスタム認可サーバーが使えること

最初に確認すべきことがあります。Okta 管理画面の セキュリティ > API に「認可サーバー」タブがあり、default という認可サーバーが存在するかどうかです。

Security > API の認可サーバー一覧

これはカスタム認可サーバーの機能が有効かどうかの判定です。公式にはこう書かれています。

Okta の API Access Management 製品は、カスタム認可サーバーを使用するための必須要件であり、本番環境ではオプションのアドオンです。 原文: Okta's API Access Management product — a requirement to use Custom Authorization Servers — is an optional add-on in production environments.
出典: https://developer.okta.com/docs/concepts/auth-servers/

ここが無い場合、今回の構成は組めません。本番の Okta 組織で試す前に、必ず確認してください。

認可サーバーを作る

mcp-kb という名前で新規作成します。既存の default は他の用途にも使われうるので、専用のものを立てます。

ここで最も重要な入力が「オーディエンス」です。MCP サーバーの URL を、末尾スラッシュなしで正確に入れます。この値が、後で Workers 側がトークンを検証するときの照合先になります。1文字違うだけで、以降すべての検証が失敗します。

作成した mcp-kb。オーディエンスが Worker の URL 

スコープを2つ作る

whoami.readwiki.read を追加します。オプションのチェックボックスはすべて既定のまま(オフ)で構いません。

カスタムスコープ2つを追加した状態

contract_lookup 用のスコープは作りません。4章で説明したとおり、こちらはグループで制御するためです。

groups クレームを追加する

クレームタブで、トークンにグループ情報を載せる設定をします。

フィールド 設定値
名前 groups
含めるトークンの種類 アクセストークン
値のタイプ グループ
フィルター 次で始まる:mcp-
groups クレーム。タイプが「アクセス」になっている点に注目

フィルターを空にしないでください。空にすると Everyone を含む全所属グループがトークンに載ります。公式にも明記されています。
フィルター:Groups を選択した場合に表示されます。グループフィルターを追加するために使用します。空欄のままにすると、このクレームはすべてのユーザーを含みます。 原文: Filter: Appears if you choose Groups. Use it to add a group filter. If you leave it blank, then this claim includes all users.
出典: https://help.okta.com/en-us/content/topics/security/api-config-claims.htm

そして、この設計がカスタム認可サーバーでしか成立しない理由もここにあります。

組織認可サーバーでは、groups クレームを持たせられるのは ID トークンのみで、アクセストークンには持たせられません。 原文: For an org authorization server, you can only create an ID token with a groups claim, not an access token.
出典: https://developer.okta.com/docs/guides/customize-tokens-groups-claim/main/

アプリとアクセスポリシー

Claude 用のアプリを OIDC のネイティブアプリケーションとして登録します。PKCE を使う公開クライアントになり、クライアントシークレットは発行されません。Claude のようなクライアントにシークレットを預けなくて済む形です。

ネイティブアプリとして登録。シークレットは無い

リダイレクト URI には https://claude.ai/api/mcp/auth_callback を指定します。

最後に、認可サーバーにアクセスポリシーを作ります。これが無いとトークンは1つも発行されません。ポリシーを作り、その中にさらにルールを追加する二階建ての構造です。

ポリシーとルール。この両方が必要

6. 繋ぐ前に、Okta 単体で確かめる

コードを書く前に、Okta が期待どおりのトークンを出すか確認します。ここを飛ばすと、後で失敗したときに原因がどちらにあるのか分からなくなります。

認可サーバーの「トークンのプレビュー」タブで、クライアント・ユーザー・スコープを指定するとトークンの中身が見られます。

ここで注意点があります。プレビュー画面には id_token と「トークン」の2つのタブがあり、確認すべきは後者(アクセストークン)です。ID トークンの aud はクライアント ID になるので、これを見て「オーディエンスの設定を間違えた」と勘違いしがちです。

ID トークン。aud はクライアント ID(これは正常)

アクセストークン。aud が Worker の URL、scp と groups が載っている

ここで aud が MCP サーバーの URL になっていること、scp に要求したスコープが入っていること、groups にフィルタどおりのグループだけが載っていることを確認します。3つ揃えば Okta 側は完成です。

7. Cloudflare Workers 側の実装

MCP サーバーは Cloudflare Workers 上に置きました。Cloudflare が公開しているテンプレートから始められます。

npm create cloudflare@latest -- internal-kb-mcp --template=cloudflare/ai/demos/remote-mcp-authless

テンプレート名に authless とあるとおり、初期状態では認証がありません。ここに認証を足していきます。

Workers を選んだ理由 MCP サーバーは HTTP で待ち受けるだけの軽い処理なので、常時起動のサーバーを用意するのは過剰です。Workers ならデプロイ1コマンドで公開でき、URL もその場で確定します。この URL を Okta 側のオーディエンスに設定する必要があるため、先に一度デプロイして URL を確定させてから Okta を作る、という順序が手順として素直でした。
MCP の仕様は改訂が続いています 2026年7月28日に MCP 仕様の大きな改訂が入り、プロトコルがステートレス化されました。公式ブログでは「ステートレスなプロトコルコア」を含む今回最大の改訂と説明されています。今回使ったテンプレートも Durable Object を使わないステートレス構成になっており、この方向に沿ったものでした。本記事は2026年8月時点の実装に基づきます。認可まわりは動きの速い領域なので、実際に組む際は仕様の最新版をご確認ください。

やることは3つだけ

コード全体は GitHub に置いてあります(こちら)。要点は3つです。

(1) 保護リソースのメタデータを返す

MCP の認可仕様は、RFC 9728 という規格に沿っています。サーバーは「自分を守っているのは誰か」を決まった場所で公開し、クライアントはそれを読んで認証先を知ります。

if (url.pathname === "/.well-known/oauth-protected-resource") {
  return Response.json({
    resource: env.MCP_RESOURCE,
    authorization_servers: [env.OKTA_ISSUER],
    scopes_supported: ["whoami.read", "wiki.read"],
    bearer_methods_supported: ["header"],
  });
}

この scopes_supported が後で効いてきます。10章で実際に検証します。

(2) トークンを検証する

jose というライブラリで JWT を検証します。発行者と宛先を指定するのが肝心なところです。

const result = await jwtVerify(token, getJwks(env.OKTA_ISSUER), {
  issuer: env.OKTA_ISSUER,
  audience: env.MCP_RESOURCE,
});

ここで audience に指定する値が、Okta の認可サーバーに設定した「オーディエンス」と一致している必要があります。5章で「1文字違うと失敗する」と書いたのはこの照合のことです。

検証に失敗したら 401 を返し、WWW-Authenticate ヘッダーでメタデータの場所を教えます。これがクライアントにとっての「認証してください」という合図になります。

(3) ツールごとに条件を見る

// スコープで守る
if (!scopes().includes("wiki.read")) {
  return refuse("このツールにはスコープ wiki.read が必要です。");
}

// グループで守る
if (!groups().includes("mcp-managers")) {
  return refuse("このツールは Okta グループ mcp-managers のメンバーのみ実行できます。");
}

たったこれだけです。認可の実体は、検証済みトークンの中身を見て分岐しているだけで、特別なことは何もしていません。

設定ファイル

発行者とリソース URL を wrangler.jsonc の環境変数として持たせます。

OKTA_ISSUER と MCP_RESOURCE の設定

デプロイ前に確認しておくと安全なこと 発行者 URI が正しいかは、{発行者URI}/.well-known/oauth-authorization-server にアクセスして JSON が返るかで確認できます。404 なら認可サーバー ID の写し間違いです。ここが違うと公開鍵が取得できず、すべてのトークン検証が失敗します。

8. Claude から繋ぐ

Claude の設定からカスタムコネクタを追加します。入力するのは MCP サーバーの URL と、Okta で発行されたクライアント ID だけです。シークレットは空欄です(ネイティブアプリなので存在しません)。

接続を押すと Okta のログイン画面が開き、認証が終わるとツールが3つ見えるようになります。

 

まず whoami を実行します。

whoami の結果。subject / scopes / groups / audience / issuer / expires_at

自分のメールアドレスが subject として返り、スコープとグループが載っています。AI が「誰として」動いているかが、サーバー側から見えている状態です。共有 API キーでは決して得られない情報です。

9. 権限を落として確かめる

ここからが本編です。設定が正しく効いているかは、権限の無い人で試して初めて分かります。

2人目のユーザーを Okta に作りました。mcp-users にだけ所属させ、mcp-managers には入れていません。

2人目は Everyone と mcp-users のみ

この状態で、2人目として Claude に接続し直します。

まず whoami で状態を確認

subject が2人目、groups は mcp-users のみ

subject が2人目のメールアドレスに変わり、groups から mcp-managers が消えています。接続する人が変われば、サーバーから見える人も変わる。当たり前のようですが、これが成立していることが今回の前提です。

3つのツールを叩いてみる

同じトークンで、wiki_search は成功、contract_lookup は拒否

ツール 必要な条件 このトークンの状態 結果
whoami スコープ whoami.read あり 成功
wiki_search スコープ wiki.read あり 成功
contract_lookup グループ mcp-managers なし 拒否

これが今回の検証で一番見せたかった状態です。同じ人・同じ接続・同じトークンで、ツールによって結果が分かれています。

グループ不足による拒否メッセージ

注目してほしいのは、contract_lookup に対応するスコープが存在しないのに、制御が効いている点です。OAuth のスコープは「あり/なし」しか表現できませんが、そこにグループという別の軸を足すことで、より細かい制御ができています。

10. スコープを外すと何が起きるか

もうひとつ確かめたいことがありました。クライアントは、どうやって「要求するスコープ」を知っているのか。

Claude の接続設定では、スコープを入力する欄はどこにもありません。にもかかわらず、トークンには whoami.readwiki.read がきちんと載っていました。7章で書いた scopes_supported を読んでいるのではないか、という推測はできますが、確証がありません。

そこで、Workers 側の宣言だけを変えて、Okta には一切触らずに試しました。

scopes_supported を whoami.read だけにする

デプロイして、コネクタを切断・再接続します。

ここで一度失敗しました デプロイ直後に whoami を叩いても、スコープは変わりませんでした。scopes_supported はクライアントが認可を要求する時点で読む値なので、既に発行済みのトークンには影響しません。トークンを取り直す必要があります。
再接続後、scp から wiki.read が消えた

消えました。Okta 側の設定は一切変えていません。Workers が宣言を変えたら、クライアントの要求が変わったのです。RFC 9728 に基づく発見の仕組みが、実際に動いていることの証明になります。

そして、この状態で wiki_search を叩くと、今度はスコープ不足で拒否されます。

スコープ不足の拒否と、状態別の対比表

状態 scopes groups wiki_search contract_lookup
変更前 wiki.read / whoami.read mcp-users 成功 拒否(グループ不足)
変更後 whoami.read mcp-users 拒否(スコープ不足) 拒否

同じユーザー・同じグループのまま、スコープの有無だけで挙動が分岐しました。9章のグループ軸と合わせて、2つの軸が独立して機能していることが実データで示せたことになります。

11. 分かったこと

スコープとグループは役割が違う

エラーメッセージが軸ごとに分かれているのが分かりやすい証拠です。

拒否理由 返る文言 制御している軸
スコープ不足 このツールにはスコープ wiki.read が必要です OAuth スコープ
グループ不足 このツールは Okta グループ mcp-managers のメンバーのみ実行できます ID基盤のグループ

スコープは「アプリケーションに何をさせてよいか」、グループは「この人が誰なのか」。前者は同意の話、後者は身元の話です。混ぜないほうが、設計としても運用としても素直になります。

権限変更はトークンの再発行で効く

Okta 側でグループから外しても、既に発行されているトークンはそのまま使えました。トークンにはその瞬間の情報が焼き付いており、次に発行されるまで更新されません。

  利点 制約
トークンに情報を載せる方式 毎回 ID 基盤に問い合わせないので速い。ID 基盤が落ちても動く 権限剥奪が即時に反映されない

アクセストークンの有効期限を短くするか、重要な操作のときだけ問い合わせるか。どこまで即時性が必要かは、扱うデータ次第です。設計時に決めておくべき論点として認識しておくとよいと思います。

拒否がどの層から返っているか

今回のツール側の拒否は、HTTP のステータスコードではなくツールの応答として返しています。トークン自体は正しいので認証は通っており、その先のアプリケーション判断で弾いている形です。

この区別は、トラブルシュートのときに効いてきます。「401 が返る」のか「ツールがエラーを返す」のかで、疑うべき場所がまったく違います。

12. Cloudflare 側から見た全体像と、次回

ここまでで、Cloudflare Workers 上の MCP サーバー1つを、Okta の権限で守る形ができました。改めて Cloudflare の観点で整理すると、本記事で使ったのは Workers だけです。

担当 本記事での状態
MCP サーバーの実行基盤 Cloudflare Workers 使用済み
トークン発行(認可サーバー) Okta 使用済み
集約と保護 Cloudflare One(MCPサーバーポータル / Access) 未使用
ツール単位の監査ログ MCPサーバーポータル 未使用

現実には、MCP サーバーは1つでは済みません。社内 Wiki 用、チケット管理用、CRM 用と増えていきます。そうなると接続先も認証も分散し、利用者は何個もコネクタを登録することになります。管理側も「誰がどのサーバーに繋いでいるか」を横断して把握できません。

後編では、Cloudflare の MCP サーバーポータルでこれらの入口をひとつにまとめます。ポータル自体は Cloudflare Access で保護し、その先の自作 MCP サーバーへは本記事で作った Okta の認可をそのまま使います。

そのとき問題になるのが、MCPサーバポータルが上流の MCP サーバーに接続するときの OAuth をどうするかです。動的クライアント登録が使えない上流プロバイダーは多いのですが、2026年7月に追加された手動 OAuth(Static OAuth) の機能により、事前登録した資格情報を使って Okta のような IdP を上流に据えられるようになりました。

加えて、ポータルにはツール単位の実行ログがあります。本記事で確認した「誰として動いているか」が、管理画面から一覧で追えるようになる。ここまで含めて、後編でお伝えします。

参考にした情報

▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
ネットワールドが開催する 年に一度の大イベント
【 Networld Wiz 2026 】お申込み受付中!!
本ブログでご紹介したメーカーもイベントへ出展します!
セッション & ブース出展情報 随時更新中
▼ぜひチェックしてください▼

Networld Wiz 2026