记一次 MCP Streamable HTTP 和 OAuth 认证改造

2026年05月22日7 次阅读0 人喜欢
MCPOAuth 2.0Claude Code CLIOpenAINginx踩坑记录Next.js
所属合集

这两天折腾了一下博客的 MCP 接口。

一开始我以为只是 Claude Code CLI 和 Codex CLI 的兼容问题,结果查下来才发现,问题其实分了好几层:MCP Streamable HTTP、OAuth discovery、Bearer token、Nginx 反代,哪个地方少一点细节都会炸。

说实话,这种问题挺烦的,因为它不是一个报错就能定位的。客户端只会告诉你连不上,真正的原因要一层一层拆。

一开始的问题

我博客里本来就有一个 MCP 接口,入口是:

text 复制代码
/api/mcp

我想兼容两种使用方式:

  • Codex CLI 这种直接带 Bearer token 的模式
  • Claude Code CLI 这种走 OAuth discovery 的模式

想法很简单:长期 token 给自动化工具用,OAuth 给 Claude Code CLI 这种需要发现认证端点的客户端用。

但实际接上去以后,认证不通,Streamable HTTP 也不太像标准实现。

最开始的实现更像一个普通的 stateless JSON-RPC POST 接口,只是名字叫 MCP。POST 能调,工具也能返回,但对于 MCP Streamable HTTP 来说,还缺不少协议层面的东西。

比如未认证时应该返回 401,而且要带 WWW-Authenticate

http 复制代码
WWW-Authenticate: Bearer resource_metadata="https://www.nnnnzs.cn/.well-known/oauth-protected-resource"

客户端看到这个以后,才知道去哪里拿 protected resource metadata,再继续 OAuth discovery。

如果这个头不标准,后面就不用看了,客户端直接懵。

先把 MCP Streamable HTTP 拉正

我先改的是 /api/mcp 本身。

之前接口虽然能处理 JSON-RPC,但没有完全按 Streamable HTTP 来。

后来改成基于 WebStandardStreamableHTTPServerTransport,把几个关键行为补上:

  • 带 token 的 POST /api/mcp 正常处理 JSON-RPC
  • 未认证请求返回 401WWW-Authenticate
  • 已认证的 GET /api/mcp 返回 405
  • notification 请求返回 202
  • 响应保持 JSON 模式,兼容 Codex CLI 这种调用方式

验证的时候,我用 initializetools/listresources/list 和 notification 都跑了一遍。

结果是:

text 复制代码
initialize -> 200
工具列表 -> 6 个 tools
资源列表 -> 正常返回
notification -> 202

这一步以后,Bearer token 模式基本就通了。

OAuth discovery 的坑

然后问题转到 OAuth metadata。

MCP 相关的 OAuth discovery 至少要暴露两个东西:

text 复制代码
/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server

protected resource metadata 里要告诉客户端:这个 MCP resource 是谁,authorization server 在哪里。

authorization server metadata 里要告诉客户端:authorize、token、revoke、introspect、register 等端点在哪里。

我一开始把这些都放在 .well-known 下,看起来没问题。

结果线上一测,发现 .well-known 直接 404。

真凶不是 Next.js,而是 Nginx。

我的 Nginx 里有这一段:

nginx 复制代码
location ~ \.well-known {
  allow all;
}

它本来是为了 ACME 证书验证留的,但副作用是 .well-known 请求被 Nginx 当静态文件处理了,没有转发给 Next.js。

所以 Next.js 里的 .well-known route 根本没机会执行。

这个问题挺典型:代码看着没问题,线上就是不生效,最后发现是反代规则把路截走了。

Nginx 也要配合

后面我把 Nginx 改成两类规则分开:

一类是证书验证用的 ACME 静态路径。

另一类是 OAuth discovery,要转发给 Next.js。

大概是这个意思:

nginx 复制代码
location ^~ /.well-known/oauth-protected-resource {
  proxy_pass http://127.0.0.1:3000;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
}

location ^~ /.well-known/oauth-authorization-server {
  proxy_pass http://127.0.0.1:3000;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-Proto $scheme;
}

这里还有一个小坑。

如果不传 X-Forwarded-HostX-Forwarded-Proto,应用里生成 metadata 的时候可能拿到内部地址,比如:

text 复制代码
https://0.0.0.0:3000

这肯定不行。

OAuth metadata 里的地址必须是客户端能访问的公网地址,也就是:

text 复制代码
https://www.nnnnzs.cn

所以代码里也补了一层公共 origin 推断逻辑,优先读反代传进来的 host 和 proto,再兜底环境变量。

path-specific metadata 又补了一刀

本来我以为到这里就结束了。

结果再测的时候发现,Codex 这类客户端可能会访问 path-specific 的 protected resource metadata:

text 复制代码
/.well-known/oauth-protected-resource/api/mcp

这个地址当时还是 404。

原因也很简单:我只做了固定路径:

text 复制代码
/api/oauth-protected-resource

没有做 catch-all。

最后改成了 Next.js 的可选 catch-all route:

text 复制代码
src/app/api/oauth-protected-resource/[[...resourcePath]]/route.ts

这样不管访问:

text 复制代码
/.well-known/oauth-protected-resource

还是访问:

text 复制代码
/.well-known/oauth-protected-resource/api/mcp

都能返回同一份正确 metadata,并且里面的 resource 指向真正的 MCP 地址。

最终线上返回是这样的:

json 复制代码
{
  "resource": "https://www.nnnnzs.cn/api/mcp",
  "authorization_servers": ["https://www.nnnnzs.cn"],
  "scopes_supported": ["read", "write", "admin"]
}

这才算把 discovery 链路补完整。

最后验证

最后我在线上完整测了一遍。

未认证请求:

text 复制代码
GET /api/mcp -> 401
WWW-Authenticate -> 指向正确的 protected resource metadata

OAuth metadata:

text 复制代码
/.well-known/oauth-protected-resource -> 200
/.well-known/oauth-protected-resource/api/mcp -> 200
/.well-known/oauth-authorization-server -> 200

带 token 的 MCP 请求:

text 复制代码
initialize -> 200
resources/list -> 200
tools/list -> 200
tools/call list_articles -> 200
notifications/initialized -> 202

到这里,Codex CLI 的 Bearer token 模式没问题,Claude Code CLI 需要的 OAuth discovery 入口也都齐了。

这次的结论

这次最大感受是,MCP 接口不是“能 POST 一个 JSON-RPC”就完事了。

如果要兼容 Codex CLI、Claude Code CLI 这种真实客户端,协议边界要补齐:

  • MCP Streamable HTTP 要按标准返回状态码和响应格式
  • OAuth discovery 要把 .well-known 做完整
  • Bearer token 和 OAuth 可以共存,但资源端点和认证端点职责要分清楚
  • Nginx 不能把 .well-known 提前截走
  • 反代必须传正确的公网 host 和 proto

这类问题没有特别玄学,就是细节多。

一个一个测下来,其实也还好。

加载评论中...