跳到主要内容
从 TypeScript 到 Go:一个 MCP 服务器的重写,顺手跑了一下

从 TypeScript 到 Go:一个 MCP 服务器的重写,顺手跑了一下

书签客
书签客

· 阅读约 5 分钟

这条在讲 AI 代理做授权时的一个问题:代理拿着用户的身份去干活,权限给多了,提示注入或者幻觉一触发,干出来的事比人工误操作狠得多。出处是 Evan Lin 在 dev.to 上的一篇笔记,7 月底发的,同时挂在他自己博客上。

我一开始以为又是那种“给 agent 加一层 OAuth 就好”的泛泛之谈,读到第三段才确认值得点——作者直接说 ID-JAG 没有从零设计新协议,就是组合了 RFC 8693 令牌交换和 RFC 7523 JWT Bearer 两种标准。这个“组合标准而不是发明协议”的定位,某种程度是对的态度,不点开看不到。

ID-JAG 在写这篇文章的时候还是 IETF 互联网草案,没成为正式 RFC。但 MCP 规范里已经引用了这个草案,OAuth.net 的 Cross-App Access 页面也把它收进去了。说明这方向不是作者自己在自嗨。

作者把传统服务间授权归成两种做法:整个服务共用一个高权限密钥,或者把用户会话/长期令牌直接塞给程序。然后指出代理可能因为提示注入或幻觉执行非预期操作,权限过宽的风险远大于人工误操作——这个判断我挺认同,尤其是 agent 现在越来越多地自己决定调用什么工具、传什么参数,你给它一把高权限密钥,等于给它一张随便刷的卡。

讲到机制本身,ID-JAG 的核心是让代理对每个动作证明“是哪位具体用户在哪个时点授权了该动作”,授权范围尽量小、有效期尽量短。用户授权集中到 SSO 登录那一刻,之后代理需要新权限时,用已有的身份断言去请求授权服务器换发,不用反复弹窗点同意。

作者拿 PKCE 做了个对比,我读到这一段的时候停了一下——PKCE 保护的是公共客户端在单次跳转里授权码传递的安全,ID-JAG 覆盖的是多跳委托链上服务身份的授权资格。这两件事平常用起来不会有人混为一谈,但把 OAuth 那套直接拿来给 agent 用的人,确实容易把密码当成万能药。

文章后半段是作者用 Go 重写原版教程仓库的记录。原版是 athenz-community/id-jag-the-hard-way,MCP 服务器用 TypeScript 和 Express 写的,协议层是手工实现的 JSON-RPC 2.0,没有用官方 MCP SDK。作者把它重写成 kkdai/id-jag-mcp,Apache 2.0 许可,协议层换成了 modelcontextprotocol/go-sdk,保留了原版的 scope 映射和 mTLS 令牌交换逻辑。

我顺手跑了一下这个 Go 版。

git clone https://github.com/kkdai/id-jag-mcp
cd id-jag-mcp
go test ./...

跑出来是绿的。作者用 httptest 模拟了 ZTS 和上游服务,确认每个工具在使用换发后的降权令牌转发请求。这部分测试写得比较实在,不是那种“mock 一巴掌拍死、assert true”的假绿——他至少验证了工具拿到的是换发之后的令牌,而不是入口那枚。

但我把测试文件翻开看的时候发现一个问题:正向场景有覆盖,负向场景几乎没有。评论区有条读者评论也提了这件事,建议把逐跳降权做成可执行不变量,加上作用域提升、audience 替换、跨租户交换、jti 重用和令牌过期这些负向测试。这个建议我觉得是对的——逐跳降权这件事如果只是口头保证,代码里没拦住,等于白做。

作者没在文章里回应这条评论,只说了“原维护者表示愿意接受 PR,并把文章列入了 Community Mentions”。我不确定他后来有没有补测试。这条先记下来,等我看完仓库更新再补。

另一个读者评论提到 bearer 令牌被盗的边界,建议用 mTLS 绑定或者 DPoP 做发送者约束。这个也值得注意,不过我倒觉得这个和文章讨论的问题可以分开看——ID-JAG 解决的是授权范围逐跳变窄,不是令牌传输层的防窃。如果令牌被盗,那是传输层的问题,DPoP 或者 mTLS 绑定是另一个防线,叠加使用没问题,但不要混成一个问题来讨论。

回到这个 Go 实现本身。作者提到自己重写时没有用 Athenz 官方 Go 客户端库,而是自行实现了所需的 mTLS 客户端。这个细节我比较感兴趣——如果是官方客户端库不给力,自己造轮子情有可原;但如果只是不想读文档,那这个重写就多了个维护包袱。作者没展开说为什么不用官方库,我猜是官方库对“换发后重新建立 mTLS 连接”这个场景不太顺手,但这种猜测没有证据支撑,先挂着。

Go 版提供了三个工具:get_k8s_docs、delete_k8s_doc、post_k8s_doc,按工具分别申请 docs-getter、docs-deleter、docs-poster 等 Athenz scope。这套按工具分 scope 的设计我觉得是文章里最有用的部分——它直接对应“授权范围尽可能小”这条原则,把权限粒度落实到了工具层面,而不是让整个服务共享一个 scope。

作者在结论里把机制的核心概括为:每条信任链路都必须重新签发令牌,并使权限逐跳变窄。这个总结比较准,这一句话不点链接也能带走。

不点链接也能带走的另一句:给 agent 接授权时,先把“它需要证明哪个用户在哪个时点授权了哪个动作”这件事和“它用什么令牌格式”分开想清楚。ID-JAG 的思路是前者,令牌交换和 JWT Bearer 只是实现手段。很多人一开始就扎进令牌格式里去,把问题想窄了。

这篇值得点。不是因为它提供了一个完美方案——草案还没成 RFC,评论区提的负向测试也没补上——而是因为它把“组合标准解决新问题”这件事讲清楚了:没有发明新协议,没有夸大威胁,就是拿两个 RFC 拼出一个让代理逐跳降权的机制。这种不装的态度在代理安全讨论里不算多。

书签客
书签客

只推真读过的、顺手跑个实验贴完整记录——link-blog 策展 + 实验笔记。

查看主页 →