最近,我一直在为 OpenViking 贡献 MCP(Model Context Protocol)相关的代码。OpenViking 的愿景不仅仅是做一个 Coding 助手的记忆插件,它想成为一个独立、全面、统一的记忆平台。
为了实现这个目标,我为它写了完整的 MCP Server,暴露了 search、read、store、forget 等工具。因为项目还没完整支持 OAuth 认证,图省事起见,我直接沿用了其 REST API 的 Authorization: Bearer <api-key> Header 鉴权方案。
一开始,这完全不是问题。Cursor、Trae、Claude Code、Manus……市面上主流的 Coding 平台对 Header 鉴权都非常友好,配置文件里加一行就搞定了。但在 OpenViking 的蓝图里,只拿下 Coding 平台是不够的,我们更希望能无缝接入受众更广的 ChatGPT 和 Claude 的网页端及桌面端。
然后,我就结结实实地撞墙了。
令人沮丧的“高墙”
调研了一圈后,我发现情况非常尴尬:
-
Claude(网页版 / Desktop 的 Connector):完全不支持 Header 认证,只留了走 OAuth 这一条路,连个填 API Key 的输入框都没给你留。

-
ChatGPT:支持添加自定义 MCP,也有 Header 选项。但如果你用“开发人员模式”强行接入,它的前端会极其显眼地提示用户“此工具未经验证”;更要命的是,它还会直接关闭内置的记忆功能。想优雅地使用?那你得发布到它的 Apps 平台,而 Apps 平台不仅需要审核,且只支持 OAuth。

起初,我对 OAuth 并没有太深的概念,天真地以为它不过是“换个地方塞 Header Key”。等深入了解后才发现,OAuth 2.1 的认证流程和简单的 API Key 完全是两个维度的东西。
这就意味着:如果你的 MCP Server 只接受 API Key,大厂的官方客户端你根本连不上。
走不通的捷径
为了不改动后端,我脑子里闪过好几个“走捷径”的念头,但都被现实无情打脸:
- 念头一:张冠李戴,把 API Key 填进 OAuth 字段里?
Claude 的 UI 里有 Client ID 和 Client Secret 两个输入框。我的直觉是:把 API Key 塞进 Client Secret 里,让它在请求时透传到Authorizationheader 上不就行了?
👉 结果:想得美。 OAuth 里的 Client Secret 是用来标识“客户端应用”身份的,只在/token换取令牌时用一次。Claude 绝不可能把这玩意儿当成 Bearer Token 挂在每个后续的 API 请求头上。 - 念头二:写个最薄的 OAuth 层,原样返回 Key?
既然必须走 OAuth,那我写个极简的/authorize接口,用户提交 Key,我直接把 Key 当作 Token 在 URL 里返回给客户端,行不行?
👉 结果:被协议封杀。 OAuth 2.1 彻底废弃了 Implicit Flow(隐式授权,即 Token 随 URL 返回),强制要求走 Authorization Code + PKCE 流程。你必须老老实实地:给 Code -> 用 Code 换 Token -> 用 Token 请求。跳过任何一步,客户端都会拒绝合作。
最终解法:搭一座恰到好处的桥
既然捷径走不通,后端又不想动,那就只能在前面加一层代理了。思路很清晰:
- 面向 MCP 客户端(Claude/ChatGPT):实现一个极其标准的 OAuth 2.1 流程(PKCE、动态客户端注册、Token 端点一个不少),在客户端看来安全性毫无问题。
- 面向上游 MCP Server:拿到 Token 后,在代理层解密出用户的 API Key,重新组装成标准的 Bearer Token,透传给源服务器。
用户的 API Key 怎么来?在 OAuth 的授权流程中,我会弹出一个 Web 授权页面,让用户手动粘贴 API Key,然后把它加密塞进 OAuth Token 里。
这条路不仅走得通,而且可以做成一个通用工具! 任何用 API Key 鉴权的 MCP Server,只要套上这层代理,都能瞬间“伪装”成支持 OAuth 的服务。
于是,就有了这个项目:MCP-Key2OAuth。
架构设计:像做短链接一样创建代理
为了让它足够好用,我采用了类似“短链接服务”的核心模型——Slug。 你只需要在网页上把的目标 MCP 服务器地址填进去:

系统会返回给你一个专属的代理端点(比如 .../a3x9k2m7p1q4/mcp)。把这个端点粘贴到 Claude 客户端里,接下来的一切都是全自动的:客户端会自动发现 OAuth 端点,弹出页面让用户输入 API Key,然后顺滑地建立连接。
整体链路如下:
MCP Client ──(走 OAuth 2.1)──> Key2OAuth Worker ──(转 Bearer Token)──> Your MCP Server
│
弹出一个 Web 页面
让用户输入 API Key
极简的技术选型与“动态路由”魔法
项目搭在 Cloudflare Workers 上(Serverless 边缘节点,冷启动极快)。核心依赖了 Cloudflare 开源的 @cloudflare/workers-oauth-provider,它帮我扛下了 PKCE 验证、DCR(动态注册)、CORS 等 80% 的脏活累活;再配合 Hono 处理路由和页面渲染。
开发过程中最大的挑战是路由问题。
每个 Slug 都有自己的端点路径(/{slug}/mcp),但 OAuth 库要求在初始化时就必须传入固定的 apiRoute 来做鉴权拦截。Slug 是用户随时动态创建的,代码启动时我上哪知道有哪些路由?
最后我用了一个看起来很“浪费”但极其有效的解法——每个请求到来时,动态实例化一个 OAuthProvider。
// 伪代码演示
const slug = extractSlugFromPath(url.pathname);
let apiRoute = '/__internal_no_match__';
if (slug) {
const config = await env.SLUG_KV.get(`slug:${slug}`);
if (config) apiRoute = `/${slug}/mcp`;
}
// 动态创建实例
const provider = new OAuthProvider({
apiRoute,
// ... 其他配置
});
return provider.fetch(request, env, ctx);
实际上这并不浪费,因为 OAuthProvider 的构造函数是纯函数,没有 I/O 操作,所有持久化状态都在 Cloudflare KV 里,实例本身是完全无状态的。
通过这种方式,从客户端发起请求、自动注册、跳转授权页,到最后完成 Token 交换,大部分流程都交给了库自动处理。我只需要写那 20% 的“粘合代码”(授权页渲染和最后一步的代理透传逻辑)。
踩坑实录:纸上得来终觉浅
整个流程跑通前,我踩了几个极其搞心态的坑:
-
UserId 里的冒号引发的血案
OAuth 库生成的 Token 格式是userId:grantId:secret,按冒号切分成 3 段。我一开始顺手把 UserId 设置成了${slug}:${keyHash}。好家伙,Token 变成 4 段了,Token Exchange 时直接抛错Invalid authorization code format。改成下划线连接才搞定。这种坑,写单元测试根本测不出来,只有完整跑完真实链路才会暴露。 -
想省掉授权页?又被源码打脸
我曾再次幻想:能不能让用户把 API Key 填在 Claude UI 的 Client Secret 字段里,我在/token端点把它偷出来,这样连授权页都不用弹了?
翻了库的源码后我死心了:库在验证client_secret时,做完 SHA-256 Hash 比对后,直接就把原文丢弃了!回调函数里根本拿不到原文。而且动态注册时,secret 是库生成的随机字符串,用户压根没机会塞自己的 Key。 -
Wrangler CLI 的玄学
写一键部署脚本时,我想用wrangler kv namespace create --json解析返回的 namespace ID。结果文档里写着有的--jsonflag,实际上根本不存在。最后只能靠粗暴的grep '"id": "xxx"'来提取。
安全模型:诚实比“吹牛”更重要
一开始在规划时,我想实现一个后端完全不存储 API Key 的“零信任”中转工具。但随着对协议的了解和开发的深入,我发现这几乎不可能。最终的方案更类似于:
- API Key 确实没明文存,但被 AES-GCM 加密后存入了 KV 里的 OAuth Grant 中。
- Worker 的部署者(也就是我),手里握着加密密钥,在技术层面是有能力解密的。
因此,在页面上我挂出了一条极为醒目的免责声明:
“This is a shared public deployment. The operator can technically decrypt API keys in OAuth tokens. For sensitive keys, self-deploy your own instance in under 5 minutes (Cloudflare Workers free tier).”
诚实的信任模型远比虚幻的安全感更有价值。
当然,基础的防护我都做足了:防 SSRF(拦截内网 IP 请求)、防枚举(12位密码学随机 Slug ID)、即时撤销(代理不缓存,源站一撤销 Token 立马失效)。同时,我在 GitHub 提供了极为简单的一键部署到 Cloudflare 按钮,有顾虑的开发者完全可以在 5 分钟内建一个自己的专属代理。
总结与未来计划
整个项目算下来只有 520 行 TypeScript,8 个源文件,部署在一个 Cloudflare Worker 上。
- 🚀 Demo 体验: mcp.767911.xyz
- 🐙 GitHub 开源: t0saki/MCP-Key2OAuth (欢迎 Star、体验并部署)
调研下来,市面上虽然有 mcp-front 这类工具,但它们走的是 IdP(身份提供者)路线,更适合企业内部搞 SSO 登录。而我这个项目,生态位非常精准:帮个人和小团队,在不改动一行后端代码的前提下,让仅支持 API Key 的 MCP 服务快速上线,兼容各种严苛的客户端。
这算是个很棒的开始,目前它已经完美解决了 Claude、Cursor 等客户端的接入问题。
至于 ChatGPT?它的“开发人员模式”依然让人头疼。但有了 MCP-Key2OAuth 这个架构打底,接下来我们可以顺理成章地做一个 OpenViking 专有的代理产品。当代理不再是“通用工具”,而是有明确应用场景(比如“为 ChatGPT 提供长期记忆”)的产品时,我们就有极大的几率通过 ChatGPT Apps 平台的审核。
有时候,面对不兼容的两个世界,最好的解决方案不是重构现有系统,而是造一座足够轻巧、恰到好处的桥。
Screenshots

