
一次跨栈 MCP 接入的开发复盘:从方案澄清到端到端验证
复盘把 PDF 工具箱接入 Agent 的跨栈 MCP 开发过程:从方案澄清、OAuth 2.1 与 PKCE、Supabase 授权事实、Go MCP Gateway 鉴权,到真实 Agent 端到端验证,并总结回调校验、文件输入、幂等等架构坑。
展开文章目录
公司要求:开发基于PDF工具箱的MCP服务。把 PDF 工具箱接入 Agent,看上去像是“给现有接口加一个 MCP 入口”。实际做下来,它更像一次跨越 MCP 协议、OAuth 授权、用户体系、异步任务和多个部署服务 的系统集成。
这次工作的首要产出并不是代码,而是把边界讲清楚:谁负责协议,谁保存授权事实,谁执行业务,客户端带来的文件如何被处理,以及出了问题该从哪里排查。方案清楚后,后续开发和联调才有了可验证的依据。
本文记录这次接入的方案确定过程、最终架构、开发中暴露的问题,以及完整验证为什么必须从浏览器授权一直走到真实工具调用。
1. 背景:为什么这不是一次普通的接口改造
现有系统已经有 Next.js 网站、Supabase Auth、用户 API Key、积分和 PDF 异步任务,也有 Go 侧的转换服务。MCP 接入后,调用方从网页变成了豆包、Claude、ChatGPT 等 Agent 客户端,入口协议和身份链路都发生了变化。
目标不是把旧 API Key 直接暴露给 MCP 客户端,而是同时满足下面几件事:
- Agent 客户端能按 MCP 的 Streamable HTTP 方式发现并调用工具。
- 用户在浏览器中完成标准 OAuth Authorization Code + PKCE 授权。
- 用户、积分、任务、审计和并发控制继续复用现有业务体系。
- Go 网关可作为多个 MCP 服务的统一入口,PDF 只是第一个服务模块。
- 手动配置 MCP 的客户端仍可使用长期 API Key,避免频繁手工更新短期 OAuth access token。
- 真实转换任务、文件下载和错误日志都能追溯到用户与调用凭证。
因此,问题不在于仅仅实现这个MCP功能,而是合理的架构前提下实施,以便之后的扩展和维护。
2. 先与 AI 把方案讨论清楚,而不是先写代码
这次最有价值的工作,是把原本模糊的需求拆成一组可验证的架构决定。AI 在这里适合做资料对照、提出反例、检查跨服务耦合和生成验证清单;最终的协议边界、业务授权和上线策略仍需由人确认。
2.1 方案讨论要回答的核心问题
在开始开发前,至少应明确以下问题。
| 问题 | 最终结论 | 原因 |
|---|---|---|
| MCP 服务放在哪里 | Go MCP Gateway 独立承载 | Go 更适合承接长连接、协议适配、下游调用和统一日志;未来可挂载多个 MCP 服务。 |
| OAuth 服务放在哪里 | Next.js 统一提供授权、Token、注册与发现端点 | 可复用网站登录态、用户同意页和品牌域名入口。 |
| 授权事实由谁保存 | Supabase 数据库 | 授权码、Grant、Refresh Token、撤销状态属于持久化业务事实,不能依赖某个 Next 进程。 |
| Gateway 如何验证凭证 | 受限 Supabase RPC | Gateway 只知道“凭证是否有效及其身份上下文”,不直接读取授权表,也不反向依赖 Next。 |
| 文件如何传入工具 | 客户端提供 Gateway 可访问的 HTTPS 临时 URL | 远程 Streamable HTTP MCP 不应假设能从 Agent UI 直接获得 multipart 文件流;附件映射需要成为明确契约。 |
| 是否复用旧 API Key | 复用用户和计费归属,不复用 OAuth 协议语义 | OAuth access token、手动 API Key、网页内部 Key 的生命周期和暴露边界不同。 |
这张表看似简单,却决定了大量实现细节。例如,一旦把授权校验放在 Next 的 HTTP 接口上,Go 网关就会在每次工具调用时依赖前端服务;一旦没有先定义文件输入契约,工具 Schema 写得再漂亮也无法保证 Agent 上传的附件能被真正下载。
2.2 从“需求描述”变成“可验收的方案”
方案确定时,不能只说“支持 OAuth”和“支持 PDF 转 Word”。每项能力都要落到请求、数据和失败行为:
- 协议层:MCP 地址、Protected Resource Metadata、Authorization Server Metadata、DCR、PKCE、回调 URI 精确匹配和 Token 刷新。
- 身份层:OAuth access token、refresh token、手动 API Key、内部服务凭证分别可做什么,是否可撤销,如何计费归属。
- 业务层:转换、拆分、合并、水印、查询任务的输入、输出、异步状态和幂等范围。
- 运行层:域名、反向代理、日志目录、跨服务超时、错误记录和部署配置。
- 验收层:从发现地址到真实任务结果的完整链路,不能只测某一个 HTTP 接口。
这也是下一阶段应继续做的事:先把每一个新 MCP 服务按这五层补齐方案,再进入编码,而不是复制 PDF 模块后边做边猜。
3. 最终架构:协议、授权和业务各司其职
3.1 系统全景图
架构的关键不在于服务数量,而在于责任不重叠:
- Next.js 是面向用户和 OAuth 客户端的协议入口,负责登录态、同意页、授权码、Token、撤销和发现元数据。
- Supabase 是身份、授权、任务和计费的事实来源。它保存可撤销、可审计的数据,不承担 MCP 协议转发。
- Go MCP Gateway 是面向 Agent 的资源服务器,负责 Streamable HTTP、鉴权、中间件、工具列表、参数适配和日志。
- PDF 模块与 converter 只专注 PDF 业务,不需要理解浏览器授权页面或客户端注册细节。
3.2 OAuth 授权与工具调用是两条不同链路
把两条链路分开理解,有助于定位问题。
前一条链路的成功不代表后一条链路必然成功。许多联调问题都发生在两者交界处:客户端拿到了 Token,但请求的资源地址、Authorization 头格式、Gateway 元数据或文件 URL 并不符合预期。
3.3 PKCE:把用户同意绑定到同一个 Agent 实例
PKCE(Proof Key for Code Exchange)是这条链路里很巧妙的一层设计。MCP Client 属于公开客户端,不能安全保存传统 OAuth client_secret;而浏览器回调里的授权码又可能被日志、浏览器扩展或恶意软件截获。PKCE 用一份只保留在客户端本地的随机原文,解决了这个问题。
具体过程分为四步:
- Agent 在发起授权前生成随机的
code_verifier原文,只保存在自己的内存或安全存储中。 - Agent 计算
BASE64URL(SHA-256(code_verifier)),把得到的code_challenge连同code_challenge_method=S256发给授权端点。服务端只保存 challenge,而不保存 verifier 原文。 - 用户完成登录和“同意授权”后,授权服务器向登记的回调地址返回一次性授权码。
- Agent 用授权码兑换 Token 时,必须交回最初的
code_verifier原文。服务端重新计算哈希并与先前保存的 challenge 比较;一致才允许兑换。
这意味着,即使有人只截获了浏览器回调中的 code,也无法兑换 Token,因为他没有最初由 Agent 生成的 code_verifier。严格说,PKCE 证明的是“当前兑换者持有发起该授权请求时生成的 verifier”,它不证明某个品牌或某台设备的身份;但对于公开 Agent 客户端,它有效地把用户的授权同意、返回的授权码和同一个客户端实例绑定在一起。正因如此,验证时不能只检查回调有 code,还必须检查 state 并用原始 code_verifier 真正兑换 Token。
4. 数据与凭证模型:复用业务,不混淆语义
4.1 不同凭证必须有明确边界
| 凭证 | 使用场景 | 生命周期 | Gateway 接受方式 |
|---|---|---|---|
| MCP OAuth access token | 支持 OAuth 的远程 MCP Client | 短期,可刷新 | Authorization: Bearer <token> |
| MCP refresh token | 客户端后台续期 | 仅调用 Token 端点,不可调工具 | 不进入 Gateway |
| 用户手动 API Key | 手工配置、脚本、只支持自定义 Header 的客户端 | 相对长期,可在账户中撤销 | 原始 Authorization: <key> 或 Bearer 形式 |
| 网页内部 Key / 服务凭证 | 网站或内部服务调用 | 不对外暴露 | Gateway 明确拒绝 |
用户 API Key 与 OAuth access token 可以共享哈希、撤销、api_key_id 计费归属等基础能力,但它们不是同一种协议凭证。把所有 Key 都当成 Bearer Token,或者把 OAuth Token 当成长效 API Key,都会让撤销、续期、审计和客户端体验变得不可靠。
4.2 introspect_mcp_credential 的设计
Gateway 不直接读取 oauth_grants、oauth_refresh_tokens 等表,而是调用仅授权给 service_role 的 RPC:
introspect_mcp_credential(token, resource)
→ active / user_id / client_id / scope / audience / expires_at / api_key_id
这个 RPC 的作用是把内部表结构收敛成网关真正需要的最小身份上下文。它带来三个好处:
- Go 服务不依赖 Next 进程,不会因前端服务重启或路由变化影响鉴权。
- 授权表的结构可以演进,Gateway 无需跟着复制 SQL 或表查询约定。
- 数据库可以只给
service_role执行权限,匿名和普通登录角色没有调用入口。
性能上还应做到凭证类型先分流:
md_oauth_前缀仅查询 OAuth access token;查不到时才查询一次旧 access token 的短暂宽限期。- 手动 API Key 直接按其哈希查询
api_keys,不会再扫描 OAuth Grant。
这里 OAuth 的第二次查询不是多余开销,而是 Token 轮换后的兼容机制:网络重试或客户端并发请求可能仍携带刚被替换的旧 Token。没有这一小段宽限期,用户会看到偶发的未授权错误;宽限期没有严格过期和撤销校验,又会扩大泄露 Token 的风险。
5. 开发中的架构坑:问题往往不在“功能没写完”
5.1 单文件 Go 服务:能跑,不等于可演进
初版 AI 曾把路由、工具定义、鉴权、converter 调用和辅助方法堆在一个 Go 文件中。对于临时 demo 可以接受,但它与既有 Go Zero 服务的目录和运行方式不一致,也无法支持“一个网关下多个 MCP 服务”的目标。
后续调整为标准分层结构:
mcp_gateway/
├── etc/ # 服务与日志配置
├── internal/config/ # 配置定义
├── internal/svc/ # ServiceContext
├── internal/middleware/ # MCP 鉴权
├── internal/handler/ # 路由、元数据、工具排序
├── internal/logic/pdf/ # PDF 核心逻辑与辅助逻辑
└── logs/ # access / error / slow / severe / stat
其中 internal/logic/pdf 又拆分了工具定义、转换调用、文件下载和主业务逻辑。这样的拆分不是为了目录好看,而是为了让新增图片、音频或其他 MCP 服务时,不必触碰 PDF 的实现细节。
经验:只要需求里已经写了“未来可能有多个服务”,第一版就应该按可扩展的服务边界落目录,避免 demo 结构成为长期架构债务。
5.2 Gateway 反向请求 Next 鉴权:职责方向错了
初版AI 请求 Next 的 introspect 接口确认 Token。这个设计就搞错了方向,架构混乱:每次 MCP 工具调用都依赖网站前端服务。
这会引入不必要的耦合:
- Next 部署、重启、缓存或反向代理异常会让 MCP 鉴权失败。
- Go 需要了解 Next 的内部 Header、接口契约和服务地址。
- 数据库里已经存在授权状态,却多了一层转发。
后改为 Gateway 直连 Supabase 受限 RPC 后,职责方向恢复正常:Next 负责写入授权事实,Gateway 负责读取授权结论,Supabase 是双方共同依赖的事实来源。
经验:后端服务需要鉴权结论时,应优先依赖权威数据源或专用身份服务;不要因为“前端已有一个接口”就让后端绕过数据库事实去请求前端。
5.3 远程 MCP 的文件输入不是浏览器表单上传
Agent 界面允许用户上传文件,不意味着远程 MCP Server 能直接收到文件。对于目前MCP,反复确认多次,发现在JSON-RPC调用中只支持url,没办法支持文件。这个限制对于开发我们这文件处理MCP是不友好的。实施上是一个功能的削弱。
好在后面使用豆包联调过程中发现,豆包自带文件转url 功能,可以方便传url 到MCP。但这里还是留下了一个潜在的坑。 其他agent如果这方面支持不够的话,需要再开发一个上传文件转url 的skill来作为补充。
经验:设计工具 Schema 前,先确认目标 Agent 对附件的实际传递形式。不要因为本地工具可以用 multipart/form-data,就假设远程 MCP 客户端一定会转发 multipart 文件流。
5.4 工具 Schema 与客户端实现存在兼容性边界
图片转 PDF 的 image_urls 曾因 Schema 中对数组使用可空联合类型而触发客户端 JSON 参数解析失败。最终应使用客户端兼容的明确数组 Schema,并在工具说明中给出输入格式。
另一个细节是,业务服务的文件数量上限来自配置,Gateway 不应再写死一套“1 到 10 个”的重复限制,否则配置调整后 MCP 与业务接口会产生不一致。
经验:MCP 工具 Schema 需要在真实 Agent 中验证。能被 SDK 接受的 Schema,不一定能被所有客户端的工具调用生成器正确构造。
5.5 Migration 也要作为可执行代码审查
开发中曾出现同一函数重复定义、SQL 别名使用保留字导致 db reset 失败等问题。由于本次工作尚未推送远端,最终做法是合并到最后一个未发布 migration,而不是不断新增互相依赖的修复 migration。
数据库改动需要检查:
- 本地
db reset或目标环境迁移是否能顺序执行。 - RPC 是否只授权给需要的角色。
- 新增约束和索引是否匹配业务幂等边界。
- 生成的 TypeScript 数据库类型是否同步。
经验:Migration 不是说明文档,而是生产环境将直接执行的程序。它和 Go、Next 代码一样需要可重复执行和最小权限审查。
5.6 幂等不应误伤用户的并发工作
PDF 转 Word、PDF 转 Excel、拆分和合并都是异步任务。幂等键应限定在“同一用户、同一凭证、同一操作、同一幂等键”的范围,而不是简单地禁止用户同时存在任何 MCP 任务。
这样用户可以同时转换两个 Word 文件,或同时执行 Word 与 Excel 转换;同一请求因网络重试而重复提交时,才返回已有任务。
经验:并发控制回答的是“哪些请求不该同时执行”,幂等回答的是“哪一次重试等价于原请求”。两者不能混为一个全局任务锁。
5.7 localhost 回调被过严认证规则拒绝:连接失败不在 MCP 工具
首次联调失败的根因不是 Agent 无法访问本机,而是 OAuth/MCP 接入初期对 redirect_uri 的校验过严:只接受 HTTPS 公网回调,拒绝了 http://localhost:<port>/... 这类本机回调地址。agent一过来注册就失败了,整个授权和 MCP 连接流程因此中断。
后续对豆包、Codex、Cloud 等实际客户端的请求进行比对后发现,它们都会在本机启动回调监听,并把 localhost 作为 OAuth 回调地址。这是原生客户端常用的 OAuth 模式:浏览器仍访问公网授权服务器,但用户同意后,浏览器将一次性 code 重定向给本机运行的 Agent。
定位过程依赖日志,而不是猜测客户端行为。
修复后的规则不是给每个 Agent 维护一份域名白名单,而是按 URI 的安全语义校验:允许任意 HTTPS 回调;同时允许带显式端口的 HTTP loopback 回调,只限 localhost、127.0.0.1 或 ::1,拒绝普通 HTTP 域名、任意局域网 IP、账号密码字段和片段。客户端注册后,授权请求与 Token 兑换仍必须与登记的 redirect_uri 完全一致。
需要区分两个地址:MCP Gateway 和 OAuth 服务本身仍应使用 Agent 可访问的 HTTPS 公网域名;只有授权完成时返回给本机 Agent 的 redirect_uri 可以是 localhost。
经验:OAuth 回调地址的规则必须按客户端类型设计。对浏览器网站只允许 HTTPS 是合理的;对桌面 Agent 和原生客户端,拒绝 loopback HTTP 会直接破坏标准授权流程。日志中授权请求停在哪一层,是区分回调校验问题、MCP 鉴权问题和工具业务问题的最快方法。
6. 真实对接验证:先用 Python 走 OAuth,再交给真实 Agent
这次验证不把“接口返回 200”当作完成标准,而是分成两个连续的真实过程:Python 验证 OAuth 协议闭环,再由 豆包、Codex 等真实 Agent 连接并调用 MCP。前者验证协议实现,后者验证客户端兼容性与业务能力;两个过程都完成,才说明服务真正可接入。
6.1 Python 全流程:由用户配合完成浏览器授权
项目中的 scripts/test_mcp_oauth.py 是一个面向真实环境的手工联调程序。它不是伪造 Token 的测试工具,而是按客户端实际行为运行:脚本启动本地回调服务,用户在浏览器中登录并点击同意,回调抵达后脚本再完成授权码兑换。
执行过程如下:
- 脚本读取 Authorization Server Metadata 和 Protected Resource Metadata,不把 Token、注册和 resource 地址写死在测试逻辑中。
- 脚本通过 DCR 注册一个测试客户端,并登记 HTTPS 回调地址。
- 脚本生成
state、code_verifier和code_challenge,打印并打开带 PKCE 参数的授权 URL。 - 用户在浏览器完成登录和授权同意;公开回调域名将
/oauth/callback转发给脚本监听的本机端口。 - 脚本收到回调后先验证
state,拒绝缺少授权码或状态不匹配的回调。 - 脚本把
code、原始code_verifier、redirect_uri、client_id和resource一并提交到 Token 端点。 - 脚本打印 Token 交换结果,随后可使用 refresh token 验证刷新与轮换行为。
示例命令:
python scripts/test_mcp_oauth.py
这里用户参与不能省略:登录、查看同意页、点击同意和浏览器重定向,本身就是 OAuth 产品体验的一部分。脚本自动完成的是客户端应完成的协议动作,不替代用户对授权范围的确认。
Python 流程应明确检查以下结果:
- 元数据中的
issuer、授权端点、Token 端点、注册端点和 resource 相互一致。 - DCR 返回有效
client_id,回调 URI 与登记值完全一致。 - 用户同意后浏览器立即跳转到回调页,脚本收到
code与原始state。 - 使用正确
code_verifier能兑换 Token;篡改 verifier、state 或 redirect URI 时必须失败。 - refresh token 能换取新 access token;旧 access token 仅在预期宽限期内可用,之后必须失效。
6.2 真实 Agent 验证:连接服务并调用 PDF 能力
Python 验证通过后,才进入 Agent 侧。这里使用豆包、Codex 等目标 MCP Client 添加 Gateway URL,按各客户端提供的交互完成授权。这个阶段验证的是客户端是否真的能理解元数据、构造 OAuth 请求、保存 Token、发送 MCP 消息并生成正确的工具参数。
按以下顺序执行:
- 在 Agent 中添加远程 MCP 服务地址,触发授权。
- 在浏览器完成登录与同意,回到 Agent 后确认连接状态成功。
- 检查
initialize、initialized和tools/list,确认工具名称、描述、排序和参数 Schema 正常显示。 - 上传真实 PDF 或图片,让 Agent 把附件转换为 Gateway 可访问的 HTTPS 临时 URL,再调用 PDF 转 Word、PDF 转 Excel、拆分、合并或图片转 PDF。
- 调用任务查询工具,确认异步任务状态、结果文件、用户隔离、积分归属和幂等行为正确。
- 使用过期 Token、已撤销 Key、无效文件 URL 和非法参数复测失败路径,确认 Agent 获得可理解的错误,Gateway 的
error.log同时有完整记录。
其中 PDF 转 Word 和 PDF 转 Excel 是重点业务验证项,尤其要使用包含表格、图片和公式的真实 PDF,检查输出文档中的公式是否按预期经过专用转换能力处理。图片转 PDF 还要验证多个 image_urls 能被 Agent 生成合法 JSON 数组,而不是在工具参数序列化阶段失败。
最终验收应同时保留两类证据:Python 终端中的发现、注册、回调和 Token 交换记录,以及真实 Agent 的连接成功、工具调用、任务结果和 Gateway 日志。前者证明 OAuth 实现正确,后者证明产品在目标使用环境中真正可用。
7. 结语
这次 MCP 接入最重要的成果不是多出十个 PDF 工具,而是建立了一条清晰的链路:客户端遵循标准协议,Next 承接用户授权,Supabase 保存授权事实,Go Gateway 承接 MCP 调用,converter 专注处理业务。
AI 写代码现在越来越强了,当前主要的工作模式是:
- 和 AI 讨论实施方案,修改、完善,最终确认方案。
- 实施结束后检查实施结果。由于代码要用于生产,此次还是进行了通读代码的检查,估计以后慢慢就不需要了?
- 逐功能的联调验证,这个现在是工作的大头。
记录一下当前工作模式,说不定过几个月就变了,哈哈!
最后,欢迎有需要的朋友使用我们的 MCP 服务。说实话,PDF 转 Word 还是相当不错的,尤其是公式的处理。