1. 認可のためのラボとしての MCP Jam
MCP Jam は「よくわからない道具」ではなく、MCP クライアントの役割をこなせるあなたの実験用スタンドです。要するに、ChatGPT が MCP サーバーとやり取りする際の挙動をエミュレートします。.well-known/oauth-protected-resource を読み、OAuth フローを実行し、トークンをリクエストに付与し、何がうまくいかなかったのかを可視化してくれます。
実務的にとても重要なポイントです。もし MCP Jam で Default OAuth フローを成功させられたなら、実際の ChatGPT App との統合準備はおよそ 80% 完了したと言えます。アカウント連携時に ChatGPT が行うことは(リンク操作も含めて)Jam ですでに再現可能で、しかもログや操作がより透明です。
前回の講義では、学習用 MCP サーバー GiftGenius に基本的な認可を設定しました。トークン検証方式(JWT か introspection)を選び、.well-known/oauth-protected-resource を実装し、ツールを保護するミドルウェアを用意しました。今回は、これらが MCP Jam のさまざまな認可モードでどう振る舞うかを見ていきます。
この講義の目標:
- Jam の認可モード(None、Bearer、OAuth with credentials、Default OAuth)を意図通りに切り替えられるようにする;
- 各モードで Jam が MCP サーバーに何を送るかを理解する;
- どの部分が壊れているのか(MCP Server・Auth Server・メタデータ)を診断できるようにする;
- 保護されたツールはトークン必須で動き、公開ツールはトークンなしでも動くことを確認する。
2. 学習用 MCP サーバー: 何をテストするのか
具体的に話すため、コンテキストを簡単に振り返ります。学習用アプリ GiftGenius の話を続けます。これは ChatGPT App で、ギフト選びを支援し、ユーザーの注文やウィッシュリストを表示します。
MCP サーバー側では既に以下があります:
- 公開ツール(例: search_gifts)— 匿名で呼び出し可能;
- 保護されたツール(例: list_user_orders)— 認証済みユーザーのみが利用でき、mcp:tools スコープを要求。
サーバーができること:
- .well-known/oauth-protected-resource を公開する;
- トークンを検証する(JWT または introspection — 前回の講義でどちらかを選択済み);
- トークンから sub(user id)、scope、aud を取り出し、ツールのハンドラーに渡す。
Node.js/TypeScript における典型的なトークン検証ミドルウェアは次のようになります:
// middleware/auth.ts
export function requireScope(requiredScope: string) {
return async (req: any, res: any, next: () => void) => {
const header = req.headers["authorization"];
if (!header?.startsWith("Bearer ")) {
res
.status(401)
.set(
"WWW-Authenticate",
`Bearer realm="mcp", resource_metadata="${process.env.BASE_URL}/.well-known/oauth-protected-resource", scope="${requiredScope}"`
)
.json({ error: "unauthorized" });
return;
}
// ここでトークン(署名、exp、aud、scope など)を検証し、
// 結果を req.user に格納します
next();
};
}
このミドルウェアは MCP の保護ツールの前で使用します。トークンがなければ 401 と、仕様で求められている WWW-Authenticate(resource_metadata を含む)を返します。トークン検証の詳細や補助関数の実装は前回の講義で扱いました。ここでは既知の前提として利用します。
3. MCP Jam の認可モード: 概観
MCP Jam には MCP サーバーに接続するための複数の認可モードがあります。これらは典型的な OAuth パターンに対応しており、トークンなしから本格的な Authorization Code + PKCE までカバーします。
概要:
- None (No Auth) — Jam は Authorization ヘッダーを一切追加しません。匿名アクセスです。公開 MCP サーバーの利用や、保護リソースが 401 と WWW-Authenticate で正しく拒否するかの確認に適しています。
- Bearer Token — Jam は Authorization: Bearer <トークン> を追加します。トークンは UI で手動入力します。すでに取得済みのトークン(curl や Keycloak UI など)で MCP リソースの挙動を手早く検証するのに適しています。
- OAuth with credentials (Client Credentials) — Jam が Auth Server に対して指定の Client ID/Secret を使い、client_credentials でトークンを自動取得します。ユーザー不在のサーバー間認可に近い「機密クライアント」モードです。
- Default OAuth (Authorization Code + PKCE) — ChatGPT のようなクライアント(secret を持たない public client)の主力モード。Jam は resource_metadata を読み、Auth Server を見つけ、/authorize にブラウザで遷移し、PKCE フローでユーザートークンを取得します。
分かりやすく表にまとめます。
| Jam のモード | Jam が送るもの | トークン取得者 | 典型的シナリオ |
|---|---|---|---|
| None | Authorization なし | なし | 匿名ツール、401 の確認 |
| Bearer Token | Bearer <手動> | あなた(curl, UI IdP) | Resource Server ロジックのテスト |
| OAuth with cred. | Bearer <client token> | Jam(client_credentials) | サービス/管理ツール |
| Default OAuth | Bearer <user token> | Jam(Authorization Code + PKCE) | ChatGPT と同様のユーザーログイン |
それでは各モードごとに、GiftGenius MCP サーバーをどう流すかを見ていきます。
4. モード None: サーバーが正しく拒否するかを確認
最も単純なモード、すなわち認可なしから始めましょう。
MCP Jam で対象サーバー(例: http://localhost:4000/mcp)を選び、接続設定で認可モードを None にします。
このときの挙動:
- Jam は MCP 接続を確立する;
- ツール呼び出し時に Authorization ヘッダーを付与しない;
- 公開ツール(例: search_gifts)はそのまま呼べる;
- 保護ツール(例: list_user_orders)は 401 Unauthorized を返すべき。
重要なのは、この 401 に正しい WWW-Authenticate を付けることです。OpenAI および MCP Authorization の推奨に近い例(realm と scope を追加):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
resource_metadata="https://giftgenius.example.com/.well-known/oauth-protected-resource",
scope="mcp:tools"
Content-Type: application/json
{"error": "unauthorized"}
Jam はこの応答を見ると、リソースは保護されており、どこからメタデータ(resource_metadata)を取得するか、および期待スコープが何かを理解します。None モードではエラー表示のみですが、Default OAuth モードでは、ここで指定された resource_metadata を自動的に参照して OAuth フローを起動します。
デバッグ観点で、None モードでは次を確認します:
- 公開ツールはトークンなしで動作する;
- 保護ツールは匿名では絶対に実行されない;
- WWW-Authenticate ヘッダーが仕様通り(Bearer と resource_metadata を含む)。
一見単純ですが、多くの問題の発端は、401 に WWW-Authenticate が付いていなかったり、パラメータが不正(例えば、現在は非推奨の resource_metadata_uri を使い、最新の resource_metadata を使っていない)だったりすることです。
5. モード Bearer Token: Resource Server ロジックのクイックテスト
次は、すでに有効なトークン(Jam 外部で取得)を持ち、Resource Server ロジックそのものを確認したい場合のモードです。トークンの受け入れ/拒否、scope と audience の扱い、sub と自サービスのユーザーの紐づけが正しいかを検証します。
MCP Jam を Bearer Token に切り替え、トークン欄に例えば次のように貼り付けます:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
以降、Jam はすべての MCP リクエストにこのヘッダーを付与します:
Authorization: Bearer eyJhbGciOi...
MCP サーバーはリクエストを受け取り、ミドルウェア requireScope("mcp:tools") を通し、JWT をデコードして claims を検証します。検証コードの典型例は次のように簡略化できます:
// auth/verifyToken.ts
import jwt from "jsonwebtoken";
export function verifyToken(header: string) {
const token = header.replace("Bearer ", "");
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY!);
// ここで aud、scope などを検査できます
return payload as { sub: string; scope?: string };
}
これをミドルウェアで利用します:
// requireScope の内部
const payload = verifyToken(header);
if (!payload.scope?.includes(requiredScope)) {
res.status(403).json({ error: "insufficient_scope" });
return;
}
(req as any).user = { id: payload.sub };
next();
Bearer モードでは次の実験が可能です:
- 必要な scope がないトークンを入れて、サーバーが 403/401 を返すことを確認;
- aud が不正なトークンを入れて、サーバーが拒否することを確認;
- 有効期限切れのトークンを入れて、invalid_token エラーを確認。
これは UI ログインや PKCE を伴わない、Resource Server ロジックのローカルな負荷テストのようなものです。ここで検証することは、そのまま Default OAuth で ChatGPT や Jam が取得するトークンにも適用されます。
6. モード OAuth with credentials(Client Credentials): 「アプリとして」のトークン
次は頻度は低いものの、理解に役立つモードです。OAuth with credentials、つまり client_credentials グラントです。Jam には以下を設定します:
- Client ID
- Client Secret
- 必要なスコープ(例: mcp:tools)
Jam はあなたの Auth Server の token_endpoint に対し、おおよそ次のようなリクエストを送ります:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=<ID>&
client_secret=<SECRET>&
scope=mcp:tools
Auth Server はトークンを返します。このとき sub は通常、特定ユーザーではなくクライアント自身(例: sub = "mcp-jam-test-client")を表します。Jam は以後、このトークンを Bearer として使用します。
MCP の世界で何に有用か:
- 特定ユーザーに紐づかないサービス/管理ツール(例: ログ出力、ヘルスチェック、サポート);
- ビジネスロジック上、ユーザートークンと「クライアント」トークンを区別する必要がある場合の検証。
ChatGPT Apps の文脈ではこのモードは通常使いません。ChatGPT はパブリッククライアントでシークレットを保持せず(public client には client_secret がそもそもありません)。しかし Jam では次の違いを把握するのに便利です:
- 「準備済みのトークンを差し込んだだけ」(Bearer モード);
- 「Jam がクライアント資格情報で自らトークンを取りに行った」(OAuth with credentials)。
学習用サーバーでは、例えば特別な MCP ツール admin_list_all_orders を用意し、grant_type=client_credentials のトークンかつ所定ロールでのみ利用可能にする、といった実験も可能です。今日の必須課題ではありませんが、有益な試みです。
7. モード Default OAuth: ChatGPT 同等の Authorization Code + PKCE
いよいよ主役の Default OAuth です。これは ChatGPT があなたの App のアカウント連携で行う動作に最も近いモードです。クライアントは resource_metadata を読み、Auth Server に進み、ログイン画面を開き、authorization code を取得し、Authorization Code + PKCE S256 で access token に交換します。
手順を分解します。理解しやすいよう、次のシーケンス図を参照してください。
sequenceDiagram
participant Jam as MCP Jam (クライアント)
participant RS as MCP Server (リソース)
participant PRM as /.well-known/oauth-protected-resource
participant AS as Auth Server (Keycloak/Auth0)
Jam->>RS: 保護ツールの呼び出し(トークンなし)
RS-->>Jam: 401 + WWW-Authenticate(resource_metadata=PRM)
Jam->>PRM: GET /.well-known/oauth-protected-resource
PRM-->>Jam: resource、authorization_servers、scopes_supported などの JSON
Jam->>AS: GET /authorize?client_id=...&code_challenge=...&scope=...
Note right of AS: ユーザーがログインして同意
AS-->>Jam: authorization_code でリダイレクト
Jam->>AS: POST /token(code + code_verifier)
AS-->>Jam: { access_token, scope, expires_in, ... }
Jam->>RS: Authorization: Bearer <access_token> でツール呼び出し
RS-->>Jam: ツールの成功結果
このモードで確認すべきこと:
- MCP サーバーからの正しい 401/WWW-Authenticate 応答。 resource_metadata がない・URL が誤っている場合、Jam は PRM を読めず、OAuth フローを開始できません。
- 有効な .well-known/oauth-protected-resource ドキュメント。 resource、authorization_servers、scopes_supported などが正しく、Jam がトークン取得先や要求すべきスコープを理解できること。
- Auth Server の正しい設定。
- Authorization Code Flow(PKCE S256)を有効化。
- Client ID が PRM の想定(または DCR による登録)と一致。
- Auth Server 上の Redirect URI が Jam の使用するものと完全一致。
- PKCE S256。 Jam は code_challenge を生成し、Auth Server が S256 をサポートしていることを前提にします。PKCE が無効、または plain のみ対応だとフローは失敗します。
- Scopes と audience。 Auth Server は必要な aud と要求スコープ(mcp:tools など)を持つトークンを発行し、MCP サーバーはそれを検証する必要があります。
Default OAuth に成功すると、次が得られます:
- Jam では MCP サーバーと接続され、保護ツール list_user_orders が、Auth Server にログインしたユーザー本人のデータを正しく返す;
- Auth Server のログには authorize と token 交換の成功が記録される;
- MCP サーバーのログにはトークン検証成功と sub の抽出が記録される。
デバッグには、ツールハンドラーに簡単なロガーを追加し、トークンから取得した userId を確かに見られるようにするのが有効です:
// MCP ツール list_user_orders のハンドラー内部
export async function listUserOrders(args: any, context: any) {
const user = context.user as { id: string };
console.log("[MCP] listUserOrders for user", user.id);
// ここで当該ユーザーの注文を返します
}
8. どこが壊れているか: モード別の診断
ここでは、MCP Jam の症状から、問題が MCP サーバー・Auth Server・メタデータのどこにあるのかを切り分ける方法を解説します。本節はモード別の診断チェックリストです。
None モードの場合:
保護ツールを呼び出したところ、サーバーが次のように返す場合:
- 200 OK で、トークンなしでも実行されてしまう — そのツールの前にトークン検証がありません。 ミドルウェアや scope チェックを追加しましょう。
- 401 だが WWW-Authenticate がない、または resource_metadata が不正 — Jam はどこからメタデータを取るべきか分からず、Default OAuth を開始できません。 上記の例を参考にヘッダーを修正してください。
Bearer Token モードの場合:
- curl や Postman では有効だと確信できるトークンでも、Jam では常に 401/403 になる。 おそらく Resource Server のロジックに問題があります。aud/scope の検証誤り、または JWT 検証に用いる公開鍵の取り違えなどです。
- Jam の Bearer トークンは動くのに、Default OAuth では動かない — 問題は MCP サーバーではなく、Auth Server または PRM にある可能性が高いです。Default OAuth で得たトークンの scope/aud が、手動テストで使ったトークンと異なっているはずです。
OAuth with credentials モードの場合:
- Jam がトークンを取得できない(/token でエラー) — Auth Server 上のクライアント設定を見直してください。secret の誤り、client_credentials 非許可、scope 禁止などが原因です。
- トークンはあるが MCP サーバーが拒否する — サーバーがユーザーの sub(メール/ユーザー ID)を期待しているのに、トークンにはクライアント ID しかないのかもしれません。 あるいは aud/scope が期待と合っていません。
Default OAuth モードの場合:
最も落とし穴が多いシナリオです。よくある問題:
- 不正な Redirect URI。 Auth Server が invalid_redirect_uri を返したり、コードを発行しなかったりします。 Jam の URI を IdP のクライアント設定に漏れなく登録し、余計なスラッシュやタイプミスがないか確認してください。
- PKCE の欠如または非対応。 Auth Server が PKCE を要求しているのに Jam(古い版など)が code_challenge を送らない、あるいは Jam は S256 を送るのに IdP が未対応だと、invalid_request になります。
- スコープの不一致。 PRM では mcp:tools を宣言しているのに、IdP 側のクライアントには openid しか許可されていない、または Jam が IdP の許容以上のスコープを要求している、など。
- audience(aud)が不適切。 トークンに付与された aud が MCP サーバーの期待値(例えば別のリソース URL)と異なるため、サーバーが正当に拒否するケースです。
次の三箇所のログを見られるようにしておくのが非常に重要です:
- MCP Jam — PRM 解析や Auth Server への HTTP リクエスト時のエラー;
- Auth Server — /authorize と /token のログ(拒否理由の手掛かりになります);
- MCP サーバー — トークン拒否の理由(invalid_token、insufficient_scope、wrong_audience)。
9. これが実際の ChatGPT App とどう関係するか
なぜすぐに ChatGPT の Developer Mode に行かず、Jam での検証に時間を使うのでしょうか。Jam はまさに実験用スタンドであり、認可モードを手元で切り替え、フローの内部を丸ごと可視化できるからです。
Jam で Default OAuth を成功させられれば、次の点が実質的に確認できます:
- MCP サーバーの .well-known/oauth-protected-resource が正しい;
- Auth Server(Keycloak/Auth0/…)の設定が適切;
- ロール・スコープ・audience・claims が期待通りにそろっている;
- MCP サーバーがトークンを検証し、ユーザーに紐づけられる。
同じ MCP サーバーに接続した ChatGPT も、同じことを行います。PRM を読み、Auth Server に行き、トークンを取得し、Authorization: Bearer でツールを呼びます。
違いは、ChatGPT では最終結果(「アカウント連携成功」か「何か問題が発生しました」)しか見えないのに対し、Jam では全プロトコルを確認し、どこで問題が起きているかを段階的に突き止められる点です。
10. ミニ実践: GiftGenius MCP サーバーの段階的テスト
ここまでを、あなたのプロジェクトでも実行できる簡単な手順にまとめます。
まず MCP サーバーを起動します(例: pnpm dev:mcp)。次を確認してください:
- http://localhost:4000/mcp(またはあなたの URL)で待ち受けている;
- エンドポイント /.well-known/oauth-protected-resource が正しい JSON を返す;
- Auth Server(Keycloak)が稼働し、Jam/ChatGPT 用の public client が設定されている。
次に:
- None モード。
Jam を認可なしで MCP サーバーに接続。以下を確認:- search_gifts が実行できる;
- list_user_orders が 401 を返し、WWW-Authenticate が正しい。
- Bearer Token モード。
Keycloak から access token を取得(UI または curl)。Jam に貼り付け、list_user_orders を呼び、次を確認:- 有効なトークンでツールが実行され、当該ユーザーの注文が返る;
- mcp:tools のないトークンや異なる aud の場合、サーバーがエラーを返す。
- OAuth with credentials モード。
機密クライアントがあるなら、Jam に client_id と client_secret を設定し、必要スコープを指定。テクニカルツール(例: admin_list_all_orders)を呼び、サービス用トークンでのみ動作することを確認。 - Default OAuth モード。
Default OAuth を有効にして list_user_orders を呼びます。Jam は自動的に:- 401 + WWW-Authenticate を受け取り、
- PRM を読み、
- ブラウザを開いてあなたが Keycloak でログインし、
- Authorization Code + PKCE でトークンを取得し、
- トークン付きで MCP ツールを呼び、レスポンスにあなたの注文が表示される。
この 4 モードが期待通りに動作したなら、単に「Keycloak をなんとなく設定できた」のではなく、認可フロー全体を検証・デバッグできるようになったということです。おめでとうございます。
11. MCP Jam と認可テストでありがちなミス
実務では、次のようなパターンで問題が繰り返し発生します。症状から早期に気づけるよう、典型的な「やってはいけない」シナリオを挙げます。
よくあるミス1: None モードで保護ツールが動くと期待する。
開発者が Jam を None にして list_user_orders を呼び、401 に驚き、ついサーバー側のトークン検証を「念のため」外してしまうことがあります。結果として MCP ツールが匿名で動作し、個人データやコマース系シナリオでは致命的です。None モードは、トークンなしで正しく拒否し、resource_metadata を含む WWW-Authenticate を返すことを確認するためのものです。
よくあるミス2: WWW-Authenticate ヘッダーの付け忘れ・不正。
非常に多いのが、401 に WWW-Authenticate がない、または古いパラメータ resource_metadata_uri を使っているケースです。Jam(ChatGPT も同様)は Protected Resource Metadata の取得先が分からず、Default OAuth を開始できません。最小要件は WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource" です。realm と scope は任意ですが、resource_metadata を忘れないでください。
よくあるミス3: Bearer モードだけをテストし、Default OAuth を無視する。
手でトークンを取得して Jam に貼り、動いたから完了と考えるパターンです。いざ本番で ChatGPT をつなぐと、.well-known が不正、PKCE 非対応、Redirect URI 不一致で、連携が失敗します。Bearer テストは必要ですが十分ではありません。Default OAuth を必ず流し、Auth Server と PRM の重要設定の半分を見落とさないようにしましょう。
よくあるミス4: ユーザー用の場面で client_credentials を使おうとする。
行き詰まって、Jam を OAuth with credentials にして client_credentials でトークンを取得し、それを list_user_orders のようなユーザー向けツールに使ってしまうことがあります。結果としてトークンの sub は client_id であって実ユーザーではないため、ビジネスロジックが不正に(共通データを見せる、該当ユーザーが見つからず落ちるなど)なります。実ユーザーを扱う ChatGPT シナリオでは Authorization Code + PKCE(Default OAuth)が必要で、client_credentials はサービス用途に限ります。
よくあるミス5: PRM・Auth Server・MCP サーバー間での scope と audience の不整合。
.well-known/oauth-protected-resource でリソースを https://giftgenius.example.com、対応スコープを ["mcp:tools"] と宣言しているのに、Auth Server は aud なしでトークンを発行し、MCP サーバーは厳密に aud = "https://giftgenius.example.com" と mcp:tools の存在を期待している——この結果、Default OAuth で取得したトークンを MCP サーバーが拒否し、「魔法探し」に半日かかる、という事態になります。PRM・IdP のクライアント設定・MCP サーバーのミドルウェア検証が、audience と scope で一致しているか常に確認しましょう。
よくあるミス6: 古い MCP Jam の使用。
MCP Authorization の仕様は進化中で、resource_metadata や強化された PKCE フロー、補助デバッガなど新項目が追加されています。古い Jam では最新項目を理解できず、古いパラメータ名で動くことがあり得ます。結果として、最新の仕様通りに設定しているのに Jam が対応できず奇妙なバグが出ます。行き詰まる前に、Jam が最新バージョンか確認しましょう。
GO TO FULL VERSION