收到带 WWW-Authenticate 挑战的 401 之后,客户端已经拥有触达 authorization server 所需的一切。每一步都是对同一个 host 的普通 GET 或 POST。把 <host> 替换成该 blocklet 的 host。
| 步骤 | 请求 | 得到 |
|---|---|---|
| 1 | 不带凭证 tools/call 一个写工具 | 401 + 携带元数据 URL 的 WWW-Authenticate |
| 2 | GET /.well-known/oauth-protected-resource | 哪个 authorization server 保护这个资源 |
| 3 | GET /.well-known/oauth-authorization-server | 端点、grant 类型、PKCE 方法 |
| 4 | POST /.well-known/service/oauth/register | 属于这个客户端的 client_id |
| 5 | 授权,然后换取凭证 | Authorization: Bearer blocklet-… |
第 2 步 —— 受保护资源
curl -s https://<host>/.well-known/oauth-protected-resource{
"resource": "https://<host>/mcp",
"authorization_servers": ["https://<host>"]
}authorization server 就是同一个 host。一个 blocklet 只保护自己的资源、只签发自己的凭证,因此要访问多个 blocklet 的客户端需要对每一个分别注册和授权。
第 3 步 —— authorization server 元数据
curl -s https://<host>/.well-known/oauth-authorization-server{
"issuer": "https://<host>",
"authorization_endpoint": "https://<host>/.well-known/service/oauth/authorize",
"registration_endpoint": "https://<host>/.well-known/service/oauth/register",
"device_authorization_endpoint": "https://<host>/.well-known/service/oauth/device_authorization",
"token_endpoint": "https://<host>/.well-known/service/oauth/token",
"response_types_supported": ["code"],
"grant_types_supported": [
"urn:ietf:params:oauth:grant-type:device_code",
"authorization_code",
"refresh_token"
],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["mcp"]
}token_endpoint_auth_methods_supported 是 none,所以客户端用 PKCE verifier 而不是 client secret 来认证 token 请求。scopes_supported 只有一项 —— 没有更窄或更宽的 scope 可选。
第 4 步 —— 注册客户端
客户端通过自我注册取得 client_id。系统不预先分发标识符。
curl -s -X POST https://<host>/.well-known/service/oauth/register \
-H 'Content-Type: application/json' \
-d '{
"client_name": "my agent",
"redirect_uris": ["http://127.0.0.1:3118/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'{
"client_id": "b665f46e-8fb5-4bf9-bd32-53df5b35f7f5",
"redirect_uris": ["http://127.0.0.1:3118/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}响应回显整份注册内容,而不只是标识符。客户端会校验它,并拒绝漏掉自己所发字段的回复。
注册一次,把 client_id 存下来。 注册按来源地址限流;从未在一次完成的授权中被使用的注册,七天后被清除。
回调地址
跑在用户机器上的客户端监听的端口在运行时选定,所以请注册本次会话实际使用的 redirect_uris,不要指望某个固定值已被预先许可。loopback 地址、https URL 和 private-use scheme 被接受;其余一律拒绝 —— 见错误。
第 5 步 —— 取得凭证
两种 grant 通向同一份凭证。通常你的工具会替你选好,见接入你的工具。
| 你的客户端 | grant |
|---|---|
| 能打开浏览器并监听本地端口(多数 MCP 客户端) | 带 PKCE 的 authorization_code,先 authorization_endpoint 再 token_endpoint |
| 不能:CLI、CI 任务、无头 agent | device grant,见无浏览器时取得凭证 |
两者都终结于同一个 token_endpoint,返回同样形状的凭证。
这份凭证是什么
token 端点返回一个 bearer token,它同时也是一把 access key:
{
"access_token": "blocklet-zEJYCdC9awCqxEPLhqreFbwXjqU6Y2BHvKVQAx6kMZfRh",
"token_type": "Bearer",
"scope": "mcp"
}这个响应带出三件事,每件都重要:
blocklet-前缀就是运行时识别它的方式。 OAuth token 和 access key 不是两套机制,是同一份凭证、同一个前缀。- 它携带同意界面上那个人所选的 role(
owner、admin、member或guest),作用域限于这个 blocklet 实例。见访问分档。 - device grant 不返回
expires_in,也不返回refresh_token。 authorization server 把refresh_token列为支持的 grant,但 device 流程不发放它。
使用它
作为 bearer token 发给 /mcp:
curl -s -X POST https://<host>/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer blocklet-…' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'401 消失了:此前被拒的同一个 tools/call 现在能到达工具。
这份凭证打不开什么
到达工具,不等于操作成功。一次已认证的写入仍可能是这样 —— 传输层 200,内部被拒:
{
"result": {
"content": [{
"type": "text",
"text": "AFS_FORBIDDEN: Forbidden at /instance/notes.txt: network clients cannot write base paths; use a resolver overlay or internal code"
}],
"isError": true
},
"jsonrpc": "2.0",
"id": 3
}owner 身份的凭证得到同样的拒绝。从网络写入不是凭证能打开的东西 —— 该 blocklet 必须已经把那条路径向网络客户端开放,通过 resolver overlay 或运行在它内部的代码。
如果你的 agent 必须写入,那是 blocklet 拥有者的决定。围绕它做设计之前,先读访问分档。