ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

StreamableHTTP 的 /mcp 握手通了,客户端 tools/call 还是调不动?TaoToken 只管模型通道这一段

StreamableHTTP 的 /mcp 握手通了,客户端 tools/call 还是调不动?TaoToken 只管模型通道这一段 1. 先别急着改代码这个坑八成不在 MCP 服务里如果你用 Spring AI 的mcp-server-webmvcstarter 把CalculatorTool、SystemInfoResource、CodeReviewPrompt暴露到/mcpcurl走initialize能拿到Mcp-Session-Idtools/list也能列出calculate但一挂进 Codex 当客户端tools/call就是发不出去——先别怀疑Tool注解写错了也别急着翻MethodToolCallbackProvider的源码。我踩过的坑是MCP 端点地址和模型通道地址填混了。这两个地址长得像作用却完全不同。MCP 地址是http://localhost:8080/mcp它负责把你的CalculatorTool暴露给客户端模型通道地址是https://taotoken.net/api它负责让客户端里的模型能正常对话、决定要不要调工具。很多人把模型通道的 Base URL 顺手填成了http://localhost:8080/mcp结果客户端把tools/call当成聊天请求发给了本地 MCP 服务握手通了才怪。这篇就按排障视角走一遍先确认 MCP 服务本身没问题再把客户端里两个地址拆开最后用一次真实的tools/call验证请求确实走到了CalculatorTool。适合正在用 Java StreamableHTTP 搭 MCP 服务、并且已经卡在客户端调用这一步的人。2. 前置MCP 服务归 MCP模型通道归 TaoToken先把职责划清楚后面排查才不会乱。MCP 服务这一侧你只需要保证http://localhost:8080/mcp能正常响应 JSON-RPC。它不认识什么模型、什么 Key只认initialize、tools/list、tools/call这些方法。Spring AI 的 starter 已经把 transport 层、JSON-RPC 解析、会话管理全自动做完了你写的CalculatorTool只要被Tool标注就会被注册成 MCP tool。模型通道这一侧是客户端里真正“动脑子”的部分。Codex 这类客户端需要一个大模型来决定“用户这句话要不要调calculate”这个模型请求得走一个独立的 Base URL。这里用 TaoToken 作为模型通道Base URL 写https://taotoken.net/apiKey 到https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end注册后创建。注意MCP 地址和模型通道地址是两条独立的链路。MCP 地址指向你本地的8080模型通道地址指向taotoken.net。两者填反握手能通tools/call必挂。如果你还没建 Key进控制台创建即可https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite。创建完在 API Keys 页面复制https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite。接入细节可以对照文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。3. 可复制配置把两个地址彻底拆开3.1 先确认 MCP 服务端配置没跑偏application.yml里这段是 StreamableHTTP 的开关enabled: true才会走/mcp单端点模式否则 starter 会退回旧的 SSE transportspring: ai: mcp: server: type: SYNC streamable-http: enabled: true endpoint: /mcp启动后先用curl把三步走完确认服务端自身是好的。第一步initialize重点看响应头里的Mcp-Session-Idcurl -i -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-03-26,capabilities:{},clientInfo:{name:test,version:1.0}}}第二步tools/list把上一步拿到的 session id 填进去应该能看到calculatecurl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Mcp-Session-Id: 上一步响应头里的值 \ -d {jsonrpc:2.0,id:2,method:tools/list}第三步直接tools/call这一步能返回42说明 MCP 服务端完全没问题curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Mcp-Session-Id: 同一个 session id \ -d {jsonrpc:2.0,id:3,method:tools/call,params:{name:calculate,arguments:{operation:multiply,a:6,b:7}}}3.2 客户端里两个地址分开填这是整篇最关键的一步。在 Codex 这类客户端的配置里MCP server 的地址填本地{ mcpServers: { calculator: { url: http://localhost:8080/mcp } } }模型通道单独配置Base URL 指向 TaoTokenKey 用你刚创建的那串{ model: { baseUrl: https://taotoken.net/api, apiKey: sk-你的TaoToken密钥, model: claude-sonnet-4-5 } }两处分开之后客户端的行为就对了它先通过http://localhost:8080/mcp完成initialize和tools/list知道有个calculate工具当用户说“6 乘 7 等于几”时模型请求走https://taotoken.net/api模型返回一个tools/call意图客户端再把tools/call发回http://localhost:8080/mcp。请求这才真的走得到CalculatorTool。3.3 用环境变量兜底避免手滑如果你不想在配置文件里写死 Key用环境变量更稳export TAOTOKEN_API_KEYsk-你的TaoToken密钥 export TAOTOKEN_BASE_URLhttps://taotoken.net/api export MCP_SERVER_URLhttp://localhost:8080/mcp然后在客户端配置里引用这些变量。这样即使换机器也不会把localhost和taotoken.net填反。4. 验证请求确认 tools/call 真的到了 CalculatorTool配置改完重启客户端做一次端到端验证。最直接的办法是在CalculatorTool的calculate方法里加一行日志Tool(description 四则运算计算器支持 add/subtract/multiply/divide) public double calculate(String operation, double a, double b) { System.out.println([MCP] tools/call 到达 CalculatorTool: operation a b); return switch (operation) { case add - a b; case subtract - a - b; case multiply - a * b; case divide - a / b; default - throw new IllegalArgumentException(不支持的运算: operation); }; }然后在客户端里输入“帮我算一下 6 乘 7”。预期结果有两层第一层客户端控制台或日志里能看到模型通道请求成功说明https://taotoken.net/api这条链路是通的。第二层你的 Spring Boot 应用控制台打印出[MCP] tools/call 到达 CalculatorTool: multiply 6.0 7.0说明tools/call确实发到了http://localhost:8080/mcp并且被路由到了CalculatorTool。如果第一层通、第二层没打印那基本就是 MCP 地址填错了或者客户端把tools/call发到了模型通道地址。反过来如果第二层打印了但客户端没显示结果那问题在响应回传跟地址无关。想单独验证模型通道是否正常可以到模型对话页面发一条普通消息https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite。如果那边能正常回复说明 Key 和 Base URL 没问题问题就锁定在 MCP 地址这一侧。5. 本篇常见错排查5.1 把 MCP 地址填成了模型通道地址最常见的错。客户端里 MCP server 的url写成了https://taotoken.net/api结果initialize请求发给了模型通道返回的是一段模型回复而不是Mcp-Session-Id。表现就是“握手看起来通了但tools/call发不出去”。改回http://localhost:8080/mcp即可。5.2 把模型通道地址填成了 MCP 地址反过来也常见。Base URL 写成http://localhost:8080/mcp客户端把聊天请求发给本地 MCP 服务MCP 服务只认 JSON-RPC收到普通聊天请求直接报错。表现是模型完全不回复或者回复一段 JSON-RPC 错误。Base URL 必须是https://taotoken.net/api。5.3 session id 没带上tools/call必须带Mcp-Session-Id请求头这个值来自initialize的响应头。有些客户端会自动管理有些需要手动配置。如果你用curl测试时忘了带服务端会返回会话不存在的错误。检查客户端是否在initialize之后正确保存并复用了 session id。5.4 endpoint 路径写错application.yml里endpoint: /mcp客户端地址就必须是http://localhost:8080/mcp。如果写成http://localhost:8080/mcp/多了斜杠或者http://localhost:8080少了路径都可能 404。Spring MVC 对路径匹配比较严格建议完全按配置来。5.5 端口被占用或服务没起来8080是常见端口容易被其他服务占用。启动时看日志有没有Tomcat started on port 8080。如果端口冲突改server.port并同步改客户端里的 MCP 地址。这个错的表现是initialize直接连接失败跟地址填混的表现不一样容易区分。5.6 模型通道 Key 无效如果模型通道返回 401客户端可能根本走不到tools/call这一步。先确认 Key 是在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite创建的并且没有多余空格。Key 无效时模型不会返回工具调用意图自然也就没有tools/call。6. 地址分开之后链路才真的通回到最初那个现象initialize通了tools/call调不动。根因不是 Spring AI 的 starter 有问题也不是CalculatorTool注册失败而是客户端里两个地址混在了一起。MCP 地址负责工具暴露模型通道地址负责模型推理两者必须分开。你现在可以这样收尾MCP 地址保持http://localhost:8080/mcp模型通道 Base URL 写https://taotoken.net/apiKey 到https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end注册后创建。如果你后面要长期跑编码类 Agent可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。接入过程中遇到报错先翻文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite大部分地址和鉴权问题那里都有对照。最后留一个实用习惯每次改完客户端配置先在CalculatorTool里看那行日志有没有打印。日志打印了说明tools/call真的到了没打印就回去检查两个地址是不是又填混了。这比盯着客户端界面猜要快得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进