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——只能为已认证用户工作,并要求 scope 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,这正是 MCP Authorization 规范所要求的。关于令牌校验与辅助函数实现的详细讲解你已在上一讲完成,这里默认它们已就绪。
3. MCP Jam 中的授权模式:总览
在 MCP Jam 中,连接 MCP 服务器时可以选择多种授权模式。它们对应典型的 OAuth 模式:从完全没有令牌到完整的 Authorization Code + PKCE。
简要列表:
- None(No Auth)——Jam 完全不会添加 Authorization 头。这是匿名访问。适用于开放的 MCP 服务器,以及验证受保护资源是否会正确返回 401 和 WWW-Authenticate。
- Bearer Token——Jam 会添加 Authorization:Bearer <令牌>,该令牌由你在界面中手动粘贴。适合快速验证:令牌已在别处获得(curl、Keycloak UI),而你只想测试 MCP 资源的行为。
- OAuth with credentials(Client Credentials)——Jam 使用提供的 Client ID 与 Secret,向 Auth Server 按 client_credentials 获取令牌。该模式是“机密客户端”,更像是无用户参与的服务端到服务端授权。
- Default OAuth(Authorization Code + PKCE)——面向类 ChatGPT 客户端的主要模式(无密钥的 public client)。Jam 会读取 resource_metadata,定位 Auth Server,打开带有 /authorize 的浏览器页面,跑完 PKCE 流程并获得用户令牌。
为了直观起见,我们用表格汇总一下。
| Jam 中的模式 | Jam 发送什么 | 谁获取令牌 | 典型场景 |
|---|---|---|---|
| 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。下面是一个带有 realm 与 scope 额外字段的示例响应,接近 OpenAI 与 MCP Authorization 规范中的推荐形式:
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 模式下,Jam 只会把错误展示给你;而在 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 并校验声明。典型的校验代码可简化如下:
// 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
- 所需 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 返回的令牌中,sub 通常表示客户端自身(例如 sub = "mcp-jam-test-client"),而不是具体用户。随后 Jam 会把这个令牌当作普通 Bearer 使用。
在 MCP 领域这有什么用:
- 与具体用户无关的服务/管理类工具(例如导出日志、health-check、技术支持);
- 验证 MCP 服务器是否能区分“用户令牌”和“客户端令牌”(如果你的业务逻辑需要)。
在 ChatGPT Apps 情境下,这个模式通常不会用,因为 ChatGPT 作为 public client 不保存密钥(public client 按定义不应有 client_secret)。但在 Jam 中,它能帮助你看清两者差异:
- “我只是塞了一个现成令牌” (Bearer 模式);
- “Jam 自己用客户端凭据去拿令牌” (OAuth with credentials 模式)。
在教学服务器上,你可以实现一个专用的 MCP 工具 admin_list_all_orders,只允许带有 grant_type=client_credentials 且具备相应角色的令牌访问。这不是本讲的必做项,但很值得一试。
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(客户端)
participant RS as MCP 服务器(资源)
participant PRM as /.well-known/oauth-protected-resource
participant AS as 认证服务器(Keycloak/Auth0)
Jam->>RS: 调用受保护的 tool(无令牌)
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 的重定向
Jam->>AS: POST /token(code + code_verifier)
AS-->>Jam: { access_token, scope, expires_in, ... }
Jam->>RS: 使用 Authorization: Bearer <access_token> 调用 tool
RS-->>Jam: 工具成功返回结果
在该模式下你需要重点检查:
- MCP 服务器返回的 401/WWW-Authenticate 是否正确。 如果服务器未返回 resource_metadata,或 URL 不正确,Jam 将无法读取 PRM,也就无法启动 OAuth 流程。
- 文档 .well-known/oauth-protected-resource 是否有效。 其中应包含正确的 resource、authorization_servers、scopes_supported 等,以便 Jam 知道去哪里获取令牌,以及申请哪些 scopes。
- Auth Server 配置是否正确。
- 已开启带 PKCE S256 的 Authorization Code 流程;
- 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)的令牌;MCP 服务器需要据此进行校验。
成功跑通 Default OAuth 后你将得到:
- 在 Jam 中,与 MCP 服务器的连接可以正常使用受保护工具 list_user_orders,并仅返回你在 Auth Server 登录的那个用户的正确数据;
- 在 Auth Server 的日志中,能看到授权与令牌交换成功;
- 在 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 公钥。
- 如果 Bearer 令牌在 Jam 中可用,但在 Default OAuth 中不可用——问题不在 MCP 服务器,而在 Auth Server 或 PRM:通过 Default OAuth 获得的令牌在 scope/aud 上与手动测试的令牌不同。
如果在 OAuth with credentials 模式:
- Jam 无法获得令牌(在 /token 步骤报错)——请检查 Auth Server 中客户端配置:secret 是否正确,是否允许 client_credentials,scope 是否被允许。
- 已获得令牌,但 MCP 服务器拒绝——可能你的服务器期望令牌中是用户 sub(邮箱/用户 ID),但该令牌只有客户端标识。 或者 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)不一致。 令牌的 aud 与 MCP 服务器所期望的不同(例如指向了另一个资源的 URL),服务器会据此拒绝。
务必要学会查看三处日志:
- MCP Jam——解析 PRM 与向 Auth Server 发起 HTTP 请求时的错误;
- Auth Server——/authorize 与 /token 的日志能指示它拒绝的原因;
- MCP 服务器——拒绝令牌的原因(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 服务器能够校验令牌并将其关联到用户。
连接到同一 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(UI 或 curl)获取 access token。在 Jam 中粘贴令牌,调用 list_user_orders 并确认:- 在有效令牌下,工具能执行并返回该用户的订单;
- 在缺少 mcp:tools 或 aud 不匹配的令牌下——服务器返回错误。
- 模式 OAuth with credentials。
如果你有机密客户端:在 Jam 中填写 client_id 与 client_secret,设置所需 scope,调用技术类工具(例如 admin_list_all_orders),并确认它只在该服务令牌下可用。 - 模式 Default OAuth。
启用 Default OAuth,调用 list_user_orders。Jam 会自动:- 获得 401 + WWW-Authenticate,
- 读取 PRM,
- 打开浏览器,让你在 Keycloak 中登录,
- 通过 Authorization Code + PKCE 获取令牌,
- 携带令牌调用 MCP 工具,随后你会在响应中看到自己的订单。
如果四种模式都按预期工作——恭喜!你不仅“配置好了 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 而非真实用户,业务逻辑就会异常(例如显示“全局”数据,或者在查找该 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 给客户端签发的令牌中没有 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,它可能不认识这些新字段,或仍在使用过时的参数名。这会导致诡异的 bug:你按最新规范配置好了一切,但 Jam 不知道该怎么处理。在陷入绝望前,先确保 Jam 已更新至最新版本。
GO TO FULL VERSION