1. 將 MCP Jam 作為授權實驗室
MCP Jam 並不是「又一個奇怪的工具」,而是你的實驗平台,能扮演 MCP 客戶端的角色。從本質上看,這是 ChatGPT 與 MCP 伺服器互動行為的模擬器:它能讀取 .well-known/oauth-protected-resource、啟動 OAuth 流程、把 token 掛到請求上,並清楚地顯示是哪一步出了問題。
非常重要的實務重點:如果你能在 MCP Jam 中跑通 Default OAuth 流程,那你大概已經完成與真實 ChatGPT App 整合的 80%。ChatGPT 在綁定帳號(linking)時會做的事,Jam 都會,而且還提供更透明的日誌與按鈕操作。
在上一堂課,我們為教學用的 MCP 伺服器 GiftGenius 設定了基礎授權:選定 token 驗證方式(JWT 或 introspection)、實作 .well-known/oauth-protected-resource,以及保護工具的 middleware。現在我們來看看,在 MCP Jam 的不同授權模式下,這些設定會如何表現。
本課目標—學會:
- 在 Jam 中有意識地切換授權模式(None、Bearer、OAuth with credentials、Default OAuth);
- 理解 Jam 在各模式下到底會送什麼給 MCP 伺服器;
- 診斷是哪一部分壞了:MCP Server、Auth Server,或是中繼資料;
- 確認受保護的工具只在有 token 時可用,而開放的工具則可在無 token 下使用。
2. 我們的教學用 MCP 伺服器:要測什麼
為了不空談,我們先回顧一下上下文。延續我們的教學應用 GiftGenius——這是個 ChatGPT‑App,協助挑選禮物,並向使用者顯示其訂單與願望清單。
在 MCP 伺服器端,我們已有:
- 開放的工具,例如 search_gifts——可匿名呼叫;
- 受保護的工具,例如 list_user_orders——只能在使用者已驗證的情況下執行,且需要 scope mcp:tools。
伺服器具備:
- 發布 .well-known/oauth-protected-resource;
- 驗證 token(JWT 或透過 introspection——你在上一課已選定其一);
- 從 token 中擷取 sub(user id)、scope、aud,並傳遞給工具的處理器。
在 Node.js/TypeScript 中,典型的 token 檢查 middleware 可能長這樣:
// 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;
}
// 此處你們已經驗證 token(簽章、exp、aud、scope...)
// 並把結果放到 req.user
next();
};
}
這個 middleware 將用於 MCP 的受保護工具之前。若沒有 token——我們回傳 401,並附上正確的 WWW-Authenticate 與 resource_metadata,符合 MCP Authorization 規範的要求。關於 token 驗證與輔助函式的詳解你已在前一堂完成,這裡直接視為已就緒。
3. MCP Jam 的授權模式:概覽
MCP Jam 提供多種連線至 MCP 伺服器的授權模式。它們對應典型的 OAuth 型態:從完全沒有 token,到完整的 Authorization Code + PKCE。
簡述如下:
- None (No Auth)——Jam 完全不會加入 Authorization 標頭。這是匿名存取。適用於開放的 MCP 伺服器,以及確認受保護資源能正確回覆 401 與 WWW-Authenticate。
- Bearer Token——Jam 加上 Authorization: Bearer <token>,而這個 token 由你在介面中手動貼上。適合快速檢查:token 已在其他地方取得(curl、Keycloak UI),你想測試 MCP 資源的行為。
- OAuth with credentials (Client Credentials)——Jam 使用你提供的 Client ID 與 Secret,對 Auth Server 以 client_credentials 取得 token。這是「機密型客戶端」模式,更像是伺服器對伺服器、無使用者參與的授權。
- Default OAuth (Authorization Code + PKCE)——面向 ChatGPT 類客戶端的主要模式(無密鑰的 public client)。Jam 會讀取 resource_metadata、找到 Auth Server、以 /authorize 啟動瀏覽器、跑完 PKCE 流程並取得使用者 token。
為了直觀,我們整理成表格。
| Jam 模式 | Jam 發送內容 | 誰取得 token | 典型情境 |
|---|---|---|---|
| None | 無 Authorization | 無 | 匿名工具、檢查 401 |
| Bearer Token | Bearer <手動> | 你(curl、IdP UI) | 測試 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),且預期哪些 scopes。在 None 模式下,它會直接顯示錯誤;但在 Default OAuth 模式下,它會自動依該 resource_metadata 啟動 OAuth 流程。
從除錯角度,在 None 模式你要檢查:
- 開放工具在無 token 時可正常運作;
- 受保護工具不可在匿名情況下執行;
- WWW-Authenticate 是否符合規範(包含 Bearer 與 resource_metadata)。
看似是簡單檢查,但很多問題都起因於回傳 401 時沒有帶 WWW-Authenticate,或其中的參數不正確(例如用過時的 resource_metadata_uri,而非正確的 resource_metadata)。
5. 模式 Bearer Token:快速測試 Resource Server 邏輯
下一步——當你已經有可用的 token(在 Jam 之外取得),想測試的重點是 Resource Server 的邏輯:它是否正確接受/拒絕該 token、是否正確處理 scope 與 audience,並把 sub 與你的服務使用者關聯。
在 MCP Jam 中把模式切換為 Bearer Token,並在 token 欄位貼上例如:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
此後 Jam 會在每個 MCP 請求的標頭中加入:
Authorization: Bearer eyJhbGciOi...
你的 MCP 伺服器收到請求後,會先通過 requireScope("mcp:tools") 這段 middleware,解碼 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 };
}
並在 middleware 中使用它:
// 在 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 的 token,確認伺服器回覆 403/401;
- 貼上錯誤 aud 的 token,確認伺服器會拒絕;
- 貼上過期 token,檢查是否回報 invalid_token。
這是免 UI 登入與 PKCE 的本地「壓力測試」Resource Server 邏輯。你在此驗證的一切,之後可直接套用到由 ChatGPT 或 Jam 在 Default OAuth 模式下取得的 token。
6. 模式 OAuth with credentials(Client Credentials):以「應用身分」取得 token
接著是較不常見、但有助於理解的模式:OAuth with credentials,也就是 client_credentials 授權。在 Jam 中你需要提供:
- Client ID
- Client Secret
- 所需 scopes(例如 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 會簽發 token,其中 sub 通常代表的是客戶端本身(例如 sub = "mcp-jam-test-client"),而非特定使用者。之後 Jam 便像一般 Bearer 一樣使用這個 token。
在 MCP 世界中,這可能的用途:
- 與特定使用者無關的服務/管理工具(例如匯出日誌、health‑check、技術支援);
- 檢查 MCP 伺服器是否能分辨使用者 token 與「客戶端」token,若你的商業邏輯有此需求。
在 ChatGPT Apps 的脈絡下通常不使用此模式,因為 ChatGPT 作為 public client 不會保存密鑰(public client 本就不該有 client_secret)。但在 Jam 中,它能幫助你看清以下差異:
- 「我只是塞一個現成 token」——Bearer 模式;
- 「Jam 依據客戶端憑證自己去換 token」——OAuth with credentials。
在教學伺服器上,你可以實作一個特別的 MCP 工具 admin_list_all_orders,僅允許以 grant_type=client_credentials 所得的 token 且具有對應角色的情況下使用。這不是本課必做項,但很值得一試。
7. 模式 Default OAuth:完整 Authorization Code + PKCE,與 ChatGPT 相同
接著是主角:Default OAuth。這個模式最接近 ChatGPT 在綁定你的 App 帳號時所做的事。客戶端會讀取 resource_metadata、前往 Auth Server、為使用者開啟登入頁面,取得 authorization code,並依 Authorization Code + PKCE S256 換取 access token。
我們來看步驟順序。為了更直觀,請看下方時序圖。
sequenceDiagram
participant Jam as MCP Jam (Client)
participant RS as MCP Server (Resource)
participant PRM as /.well-known/oauth-protected-resource
participant AS as Auth Server (Keycloak/Auth0)
Jam->>RS: 呼叫受保護的工具(無 token)
RS-->>Jam: 401 + WWW-Authenticate(resource_metadata=PRM)
Jam->>PRM: GET /.well-known/oauth-protected-resource
PRM-->>Jam: JSON,包含 resource、authorization_servers、scopes_supported...
Jam->>AS: GET /authorize?client_id=...&code_challenge=...&scope=...
Note right of AS: 使用者登入並給予同意
AS-->>Jam: 帶著 authorization_code 的 redirect
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 能知道去哪裡換 token、要請哪些 scopes。
- Auth Server 設定正確。
- 啟用 Authorization Code Flow 與 PKCE S256。
- Client ID 與 PRM 的期待一致(或透過 DCR——Dynamic Client Registration 註冊)。
- Auth Server 中設定的 Redirect URI 與 Jam 使用的完全一致。
- PKCE S256。 Jam 生成 code_challenge,並預期 Auth Server 支援 S256 方法。若未啟用 PKCE 或僅支援 plain,流程會失敗。
- Scopes 與 audience。 Auth Server 應簽發帶有正確 aud 和請求之 scopes(如 mcp:tools)的 token,而 MCP 伺服器要正確驗證它們。
成功跑完 Default OAuth 後,你會得到:
- 在 Jam——與 MCP 伺服器的連線,其中受保護工具 list_user_orders 會僅針對你在 Auth Server 登入的那位使用者,回傳正確資料;
- 在 Auth Server 日誌——成功的 authorize 與 token 交換;
- 在 MCP 伺服器日誌——成功驗證 token 並取出 sub。
為了除錯,你可以在工具處理器中加入簡單的 logger,以確認確實看到了來自 token 的 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 並在沒有 token 時就執行動作——表示該工具前沒有做 token 檢查。 需要加上 middleware 或 scope 檢查。
- 401,但沒有 WWW-Authenticate,或其中的 resource_metadata 有問題——Jam 將無法知道去哪取得中繼資料,也就無法啟動 Default OAuth。 依上方範例修正標頭。
若在 Bearer Token 模式:
- 即便使用你確定在直接呼叫(curl 或 Postman)時有效的 token,Jam 仍持續拿到 401/403。 很可能是 Resource Server 邏輯有誤:對 aud/scope 的檢查不對,或 JWT 簽章的公鑰用錯。
- 如果 Bearer token 在 Jam 可用,但之後在 Default OAuth 不可用——問題可能不在 MCP 伺服器,而在 Auth Server 或 PRM:Default OAuth 取得的 token 在 scope/aud 上與你手動測試的不同。
若在 OAuth with credentials 模式:
- Jam 無法取得 token(在 /token 步驟出錯)——請檢查 Auth Server 的客戶端設定(secret 錯誤、未允許 client_credentials、或要求的 scope 被禁止)。
- 取得了 token,但 MCP 伺服器拒絕——可能是伺服器預期的是使用者型的 sub(email/使用者 ID),而 token 裡只有客戶端識別。 或者 aud/scope 與預期不一致。
若在 Default OAuth 模式:
這是最容易踩坑的情境。常見問題:
- Redirect URI 錯誤。 Auth Server 回報 invalid_redirect_uri 或直接不發 code。 請確認 Jam 的 URI 已正確登錄在 IdP 的客戶端設定中,沒有多餘斜線或拼寫錯誤。
- 缺少或不支援 PKCE。 若 Auth Server 要求 PKCE,但 Jam(或舊版)未送 code_challenge;或反之——Jam 送 S256,但 IdP 不支援該方法,你會看到 invalid_request。
- Scopes 不一致。 你在 PRM 聲明了 mcp:tools,但 IdP 只允許 openid;或 Jam 請求的 scope 超過 IdP 願意發放的範圍。
- Audience(aud)不對。 Token 的 aud 與 MCP 伺服器的期待不同(例如指向另一個資源的 URL)。伺服器會理所當然地拒絕。
務必學會看以下三處的日誌:
- MCP Jam——解析 PRM 與對 Auth Server 的 HTTP 請求錯誤;
- Auth Server——/authorize 與 /token 的日誌可告訴你它拒絕的原因;
- MCP 伺服器——拒絕 token 的原因(invalid_token、insufficient_scope、wrong_audience)。
9. 這與真實的 ChatGPT App 有何關係
為什麼我們花這麼多時間在 Jam 上,而不是直接衝 ChatGPT 的 Developer Mode?因為 Jam 就是實驗平台:它讓你掌控授權模式,並把整個流程的內幕細節攤在你面前。
當你在 Jam 中成功跑通 Default OAuth,等於已經確認:
- MCP 伺服器的 .well-known/oauth-protected-resource 正確;
- Auth Server(Keycloak/Auth0/…)設定正確;
- 角色、scopes、audience 與 claims 都符合預期;
- MCP 伺服器能驗證 token 並將其綁定到使用者。
ChatGPT 連上同一個 MCP 伺服器時,會做同樣的事:讀取 PRM、前往 Auth Server、取得 token,並開始以 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 會回覆帶有正確 WWW-Authenticate 的 401。
- 模式 Bearer Token。
透過 Keycloak(UI 或 curl)取得 access token。貼到 Jam,呼叫 list_user_orders 並確認:- 在有效 token 下,工具會執行並回傳該使用者的訂單;
- 若 token 缺少 mcp:tools 或 aud 不符——伺服器會回傳錯誤。
- 模式 OAuth with credentials。
若你有機密型客戶端:在 Jam 中填入 client_id 與 client_secret,設定所需 scope,呼叫技術性工具(例如 admin_list_all_orders),並確認僅在此服務型 token 下可用。 - 模式 Default OAuth。
啟用 Default OAuth,呼叫 list_user_orders。Jam 會自動:- 收到 401 + WWW-Authenticate,
- 讀取 PRM,
- 開啟瀏覽器讓你登入 Keycloak,
- 以 Authorization Code + PKCE 取得 token,
- 帶著 token 呼叫 MCP 工具,之後你會在回應中看到自己的訂單。
若四種模式都如預期運作——恭喜!你不僅是「把 Keycloak 勉強弄跑了」,而是實際理解如何檢查與除錯整個授權流程。
11. 使用 MCP Jam 測試授權時的常見錯誤
在實務中,這些問題常以重複出現的錯誤模式呈現。以下是幾個「不該這麼做」的典型情境,方便你一眼辨識。
錯誤 №1:期待受保護工具能在 None 模式運作。
有時開發者把 Jam 設為 None 模式,呼叫 list_user_orders,收到 401 就感到驚訝,然後「以防萬一」把伺服器上的 token 檢查移除。結果 MCP 工具開始在匿名下運作,對個資與交易情境來說完全不可接受。None 模式的目的,是要確認伺服器在沒有 token 時會正確拒絕,並回傳帶有 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。
開發者手動拿到 token,貼進 Jam,看起來一切正常,就以為完成了。等到要接上真實 ChatGPT 時,才發現 .well-known 不正確、PKCE 未支援、Redirect URI 不一致,導致 linking 失敗。測 Bearer 模式是必要但不充分的步驟。一定要跑 Default OAuth,否則你無法驗證 Auth Server 與 PRM 的半數關鍵設定。
錯誤 №4:在需要使用者 token 的地方用 client_credentials。
有時在絕望之下,開發者啟用 Jam 的 OAuth with credentials 模式,用 client_credentials 拿到 token,然後拿它去呼叫使用者工具,如 list_user_orders。結果 token 中的 sub 是 client_id,而非真實使用者,業務邏輯就會變得古怪(例如顯示「共用」資料,或嘗試以該 ID 找不到使用者而失敗)。對 ChatGPT 的使用者情境而言,需要的是 Authorization Code + PKCE(Default OAuth);client_credentials 僅適用於服務型任務。
錯誤 №5:PRM、Auth Server 與 MCP 伺服器間的 scopes 與 audience 不一致。
在 .well-known/oauth-protected-resource 中你宣告資源為 https://giftgenius.example.com,且支援的 scopes 是 ["mcp:tools"]。然而 Auth Server 發給客戶端的 token 沒有 aud,而 MCP 伺服器在驗證時又嚴格要求 aud = "https://giftgenius.example.com" 並包含 mcp:tools。結果透過 Default OAuth 取得的 token 被 MCP 伺服器拒絕,你則為此花半天找「玄學」。務必確保 PRM、IdP 的客戶端設定,以及 MCP 伺服器的 middleware 檢查,三者在 audience 與 scope 上達成一致。
錯誤 №6:使用舊版 MCP Jam。
MCP Authorization 規範仍在積極演進,會加入新欄位(如 resource_metadata、更完整的 PKCE 流程、輔助除錯工具)。如果你使用舊版 Jam,它可能無法識別新欄位,或仍沿用過時的參數名稱。這會導致光怪陸離的 bug:你一切都依最新 RFC 設好,但 Jam 根本不知道該怎麼處理。在陷入絕望之前,先確認 Jam 已更新至最新版本。
GO TO FULL VERSION