Skip to main content
Glama
yamashin55

internal-kb-mcp

by yamashin55

internal-kb-mcp — Okta のカスタム認可サーバーで保護した MCP サーバー

Cloudflare Workers 上に構築した リモート MCP サーバーの検証用実装 です。 アクセス制御を MCP サーバー側で自前実装するのではなく、Okta のカスタム認可サーバーが発行したアクセストークン (JWT) の中身だけを根拠に、ツール単位で認可するという構成を検証します。

このリポジトリは検証記事の付属サンプルです。実運用を想定したものではありません。

何を検証できるか

検証したいこと

このリポジトリでの表現

MCP クライアントが「どこで認証すればよいか」を自力で見つけられるか

RFC 9728 の保護リソースメタデータ + 401 応答の WWW-Authenticate

スコープによるツール単位の認可

whoami / wiki_search

IdP のグループによる特権ツールの制御

contract_lookup

認可の状態を利用者が確認できるか

whoami が検証済みクレームをそのまま返す

Related MCP server: Access Self-Hosted MCP Server

認可フロー

sequenceDiagram
    autonumber
    participant C as MCP<br/>クライアント
    participant W as Worker<br/>(MCP サーバー)
    participant O as Okta<br/>認可サーバー

    C->>W: POST /mcp (トークンなし)
    W-->>C: 401 + WWW-Authenticate<br/>resource_metadata=...
    C->>W: GET /.well-known/<br/>oauth-protected-resource
    W-->>C: resource / authorization_servers<br/>/ scopes_supported
    C->>O: 認可リクエスト<br/>(audience = MCP_RESOURCE)
    O-->>C: アクセストークン JWT<br/>(scp / groups を含む)
    C->>W: POST /mcp<br/>Authorization: Bearer ...
    W->>O: JWKS 取得<br/>({issuer}/v1/keys)
    O-->>W: 公開鍵
    W-->>C: 署名 / iss / aud 検証 OK<br/>→ ツール実行

ポイントは、Worker 側がクライアントシークレットを一切持たないことです。 この Worker は OAuth のリソースサーバーであり、JWKS で取得した公開鍵を使って JWT を検証するだけです。認可の判断材料はすべてトークンの中にあります。

ツールと認可条件

ツール

認可条件

引数

内容

whoami

スコープ whoami.read

なし

検証済みトークンの subject / scopes / groups / audience / issuer / 有効期限を返す

wiki_search

スコープ wiki.read

query (必須)

社内ナレッジのダミーデータを全文検索する

contract_lookup

Okta グループ mcp-managers への所属

customer (任意)

顧客の契約情報のダミーデータを返す。省略時は全件

判定に使うクレームは次のとおりです。

  • スコープ: アクセストークンの scp クレーム

  • グループ: アクセストークンの groups クレーム(Okta 側でトークンに載せる設定が必要)

条件を満たさない場合はトランスポート層で弾かずに、ツールの応答として「なぜ実行できないか」を返しますisError: true)。認可の効き方が MCP クライアントの画面上で見えるようにするための、意図的な設計です。

データはすべてソースコード内のダミーです(WIKI / CONTRACTS)。実在の顧客情報は含まれていません。

セットアップ

前提

  • Cloudflare アカウント(Workers が有効)

  • Okta のテナント(カスタム認可サーバーを作成できるプラン)

  • Node.js と npm

1. Okta 側

Okta 管理コンソールの Security > API > Authorization Servers から、カスタム認可サーバーを作成し、以下を設定します(UI の名称は Okta のバージョンにより多少異なります)。

  1. カスタム認可サーバーを作成する

    • 作成すると issuer が https://<your-okta-domain>.okta.com/oauth2/<authorization-server-id> の形式で払い出されます。これが OKTA_ISSUER になります。

  2. Audience を Worker の URL に設定する

    • 後述の MCP_RESOURCE完全に一致させます。ここがずれていると、署名が正しくても aud 不一致で常に 401 になります。

  3. スコープを追加する

    • whoami.read

    • wiki.read

  4. グループを作成する

    • mcp-managers を作成し、特権ツールを使わせたいユーザーを所属させます。

    • 権限差を確認するために、所属していないユーザーも 1 人用意しておくと検証しやすくなります。

  5. アクセストークンに groups クレームを載せる

    • カスタム認可サーバーの Claims で、groups という名前のクレームを Access Token に対して追加します。

    • これを設定しないと contract_lookup は誰も実行できません(グループ判定が常に空になるため)。

  6. アクセスポリシー / ルールを設定する

    • どのクライアント・どのユーザーに、どのスコープを付与するかを定義します。

MCP クライアントが Okta からトークンを取得するための OAuth クライアントの登録方法(動的クライアント登録を使うか、事前に登録したクライアントを使うか)は、利用する MCP クライアントによって異なります。この部分は検証記事側の手順を参照してください。

2. Cloudflare 側

git clone https://github.com/yamashin55/okta-mcp-cloudflare-demo.git
cd okta-mcp-cloudflare-demo
npm install

wrangler.jsoncvars を、手順 1 で確定した自分の環境の値に必ず書き換えます。

"vars": {
  "OKTA_ISSUER": "https://<your-okta-domain>.okta.com/oauth2/<authorization-server-id>",
  "MCP_RESOURCE": "https://<worker-name>.<your-subdomain>.workers.dev"
}

MCP_RESOURCE はデプロイ後に確定する URL なので、一度デプロイして URL を確認してから書き換え、もう一度デプロイする流れになります。

npx wrangler deploy

OKTA_ISSUERMCP_RESOURCE はどちらも秘密情報ではありません。次項のメタデータエンドポイントから無認証で公開される値です。そのため wrangler secret ではなく平文の vars で管理しています。

3. 接続確認

保護リソースメタデータは無認証で取得できます。

curl -s https://<your-worker-url>/.well-known/oauth-protected-resource
{
  "resource": "https://<your-worker-url>",
  "authorization_servers": ["https://<your-okta-domain>.okta.com/oauth2/<authorization-server-id>"],
  "scopes_supported": ["whoami.read", "wiki.read"],
  "bearer_methods_supported": ["header"]
}

トークンなしでツールを呼ぶと、401 と WWW-Authenticate が返ります。MCP クライアントはこのヘッダーを見て、認可サーバーの場所を知ります。

curl -i -X POST https://<your-worker-url>/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
HTTP/2 401
www-authenticate: Bearer resource_metadata="https://<your-worker-url>/.well-known/oauth-protected-resource"

認可が効いていることの確認

確認したいこと

操作

期待される結果

トークンが検証されている

whoami を実行

subject / scopes / groups / audience / issuer が返る

スコープで守られている

wiki.read を含まないトークンで wiki_search を実行

「このツールにはスコープ wiki.read が必要です。」

グループで守られている

mcp-managers に所属していないユーザーで contract_lookup を実行

「このツールは Okta グループ mcp-managers のメンバーのみ実行できます。」

audience が効いている

MCP_RESOURCE と Audience をずらす

すべてのリクエストが 401

whoami を最初に実行して、実際にどのスコープとグループがトークンに載っているかを確認してから他のツールを試すと、原因の切り分けが楽になります。

実装

すべて src/index.ts の 1 ファイルです。

箇所

役割

fetch ハンドラ

/.well-known/oauth-protected-resource の応答、Bearer トークンの取り出し、jwtVerify による検証

getJwks()

issuer ごとに JWKS を使い回すキャッシュ

unauthorized()

RFC 9728 に沿った WWW-Authenticate 付きの 401 応答

createServer()

3 つのツールの登録と、スコープ / グループによる認可判定

claimsOf() / scopesOf() / groupsOf()

検証済みクレームをツールから参照するためのヘルパー

検証済みのクレームは createMcpHandlerauthContext 経由でツールに渡しています。ツール側はトークンの検証を一切行わず、検証済みの事実だけを見て判断する構造です。

開発

npm run dev          # ローカル開発サーバー (wrangler dev)
npm run type-check   # 型チェック (tsc --noEmit)
npm run lint:fix     # oxlint
npm run format       # oxfmt
npm run cf-typegen   # wrangler types (バインディング変更時)
npm run deploy       # wrangler deploy

wrangler dev でローカル起動した場合も Okta のトークン検証は有効なままです(JWKS の取得にネットワークアクセスが発生します)。ローカルで検証を通すには、MCP_RESOURCE と Okta の Audience を合わせておく必要があります。

注意事項

  • 本リポジトリは 検証・学習を目的としたサンプル実装です。無保証で提供され、実運用環境での利用は想定していません。

  • 収録しているナレッジ・契約情報はすべてダミーデータです。

  • wrangler.jsonc の値はプレースホルダです。そのままデプロイしても動作しません。

  • 内容の正確性・動作について、作者はいかなる責任も負いません。利用は自己責任でお願いします。

Related MCP Connectors

Related MCP Servers