1. 为什么要在 Spring AI 里认真对待 MCP 的 JSON-RPC 层
如果你正在用 Spring AI 1.x 做 Java 侧的 AI 应用,大概率已经写过ChatClient调模型、用@Tool注解暴露本地方法。但一旦工具不在本进程里——比如一个独立的检索服务、一个公司内部的订单查询服务、一个跑在另一台机器上的 Python 脚本——本地@Tool就不够用了。这时候需要的是模型上下文协议(MCP),它把「AI 应用怎么调用外部工具」这件事标准化成了一套基于 JSON-RPC 2.0 的通信约定。
MCP 能做什么?简单说,它让 AI 应用(Host)通过客户端(Client)连接到一个个独立的服务器(Server),服务器把资源、提示模板、工具以标准原语暴露出来。适合谁?适合需要在 Java 项目里接入外部工具服务、又不想为每个工具写一套私有适配层的开发者。你可以把它理解成 AI 世界的 USB-C:以前每个外部系统都要单独做一根线,现在统一成一个接口。
我试过在 Spring AI 1.x 里接一个自建的 MCP 服务,最开始卡住的不是业务逻辑,而是 JSON-RPC 的初始化握手和端点声明——文档里一笔带过,实际配置时字段写错一个就静默失败。这篇就把这套配置骨架拆开,从依赖到端点声明,再到一次真实的工具调用链路验证,全部落到可复制的代码。
2. TaoToken 前置:把模型侧和工具侧解耦
在讲 MCP 配置之前,先说清楚模型侧怎么接。MCP 解决的是「工具怎么暴露和调用」,但工具调用最终还是要模型来决定「调哪个工具、传什么参数」。所以你需要一个能稳定响应工具调用(tool calls)的模型端点。
我这边习惯用 TaoToken 作为模型接入层,原因是它同时提供 OpenAI 兼容接口和 Anthropic 兼容接口,Spring AI 1.x 里两种ChatModel实现都能直接对接,不用为了换模型改 MCP 那层的代码。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意这个地址后面不加任何查询参数。
具体到配置,你需要在 TaoToken 控制台创建一个 API Key,然后把它写进 Spring 的配置文件。控制台入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建好之后,模型侧的配置大概是这样:
spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: claude-sonnet-4-5 temperature: 0.2这里base-url指向 TaoToken 的 API 根路径,Spring AI 的 OpenAI starter 会自动拼接/v1/chat/completions之类的路径。如果你用的是 Anthropic 兼容模式,换成对应的 starter 和base-url即可,MCP 那层的代码完全不用动——这正是把模型侧和工具侧解耦的价值。
注意:API Key 不要硬编码在
application.yml里提交到仓库,用环境变量或者配置中心注入。MCP 服务器如果也要认证,同样走环境变量。
3. 可复制配置:MCP 客户端与服务端的 JSON-RPC 骨架
3.1 依赖引入
Spring AI 1.x 对 MCP 的支持拆成了客户端和服务端两个 starter。假设你用的是 Maven,先在pom.xml里加:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> </dependency>版本跟随你的 Spring AI BOM 走,不要单独指定,否则容易出现McpClient和McpServer的 API 不匹配。如果你只做客户端(连接别人写好的 MCP 服务),只引 client starter 就够了;服务端 starter 是给「自己暴露工具给别人用」的场景。
3.2 服务端:声明 JSON-RPC 端点与工具
服务端的核心是把一个 Spring Bean 里的方法注册成 MCP 工具。Spring AI 提供了@Tool注解,配合ToolCallbackProvider暴露出去。下面是一个最小可运行的服务端配置:
@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider orderToolProvider(OrderService orderService) { return MethodToolCallbackProvider.builder() .toolObjects(orderService) .build(); } } @Service public class OrderService { @Tool(description = "根据订单号查询订单状态,返回状态码和更新时间") public OrderStatus queryOrder(@ToolParam(description = "订单号,格式 ORD- 开头") String orderId) { // 真实场景这里查数据库或调内部 API return new OrderStatus(orderId, "PAID", Instant.now()); } }然后在application.yml里声明 MCP 服务端的传输方式和端点:
spring: ai: mcp: server: name: order-mcp-server version: 1.0.0 protocol: STREAMABLE streamable-http: mcp-endpoint: /mcp port: 8081这里protocol: STREAMABLE对应新版推荐的 Streamable HTTP 传输,mcp-endpoint就是 JSON-RPC 消息的入口路径。客户端会往http://localhost:8081/mcp发 POST 请求,请求体是标准的 JSON-RPC 2.0 格式。
3.3 客户端:连接外部 MCP 服务
客户端侧要声明「连哪个服务器、用什么传输」。Spring AI 1.x 支持在配置文件里直接声明多个 MCP 服务器连接:
spring: ai: mcp: client: enabled: true name: spring-ai-mcp-client version: 1.0.0 type: SYNC request-timeout: 30s streamable-http: connections: order-server: url: http://localhost:8081 endpoint: /mcptype: SYNC表示同步客户端,适合请求-响应式的工具调用;如果你要做流式或长任务,可以换成ASYNC。connections下面可以挂多个服务器,每个 key 是连接名,url和endpoint拼起来就是完整的 JSON-RPC 端点。
配置完之后,Spring AI 会自动创建McpSyncClient并注册到ToolCallbackProvider里,模型在对话时就能「看到」这些外部工具。你不需要手写 JSON-RPC 的initialize、tools/list、tools/call这些方法——框架帮你做了,但理解它们有助于排障。
3.4 JSON-RPC 消息长什么样
为了后面排障方便,这里贴一条真实的tools/call请求体,你可以用 curl 直接打服务端验证:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "queryOrder", "arguments": { "orderId": "ORD-20250101-001" } } }对应的成功响应:
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\"orderId\":\"ORD-20250101-001\",\"status\":\"PAID\"}" } ] } }注意id在同一会话内不能重复,也不能为null——这是 MCP 在 JSON-RPC 2.0 之上的硬性增强,写自定义客户端时容易踩。
4. 验证请求:跑通一次工具调用链路
配置写完,怎么确认真的通了?分两步:先绕过模型直接验证 MCP 服务端,再走完整链路让模型决定调用。
4.1 直接打 JSON-RPC 端点
服务端起在 8081 后,先用 curl 发一个initialize请求:
curl -X POST http://localhost:8081/mcp \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0"} } }'如果返回里带result.capabilities.tools,说明服务端能力协商成功。接着发tools/list:
curl -X POST http://localhost:8081/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'你应该能看到queryOrder出现在工具列表里,带inputSchema。这一步过了,说明 JSON-RPC 层没问题,问题只可能在客户端配置或模型侧。
4.2 走完整链路
写一个 Spring Boot 测试类,注入ChatClient,让它根据自然语言决定调用工具:
@SpringBootTest class McpToolCallTest { @Autowired private ChatClient chatClient; @Test void shouldCallExternalOrderTool() { String reply = chatClient.prompt() .user("帮我查一下订单 ORD-20250101-001 的状态") .call() .content(); System.out.println(reply); assertThat(reply).contains("PAID"); } }跑起来后,观察日志里有没有tools/call的 JSON-RPC 往返。成功的话,模型会先返回一个 tool call,Spring AI 把它转成 MCP 请求发给 8081,拿到结果后再喂回模型生成最终回复。整个链路里,模型侧走的是 TaoToken 的 API,工具侧走的是本地 MCP 服务,两边通过 Spring AI 的ToolCallbackProvider缝合。
如果你只想先验证模型能不能正确识别工具,可以打开模型对话页面手动试:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite ,把工具描述贴进去,看它是否按预期生成调用参数。
5. 本篇常见错排查
5.1 客户端连不上,日志只有一句 timeout
先确认url和endpoint有没有拼错。url: http://localhost:8081加endpoint: /mcp拼出来是http://localhost:8081/mcp,如果你在url里已经写了/mcp,就会变成/mcp/mcp。另外 Streamable HTTP 的initialize是 POST,但后续监听流是 GET,如果服务端只放行了 POST,握手会卡住。
5.2 工具列表为空
大概率是ToolCallbackProvider没被扫描到。检查@Tool方法所在的类是不是 Spring Bean,MethodToolCallbackProvider.builder().toolObjects(...)传进去的对象必须是被 Spring 管理的实例。另外@Tool的方法参数要加@ToolParam描述,否则生成的inputSchema可能缺字段,模型看不到参数说明就不会调。
5.3 模型不调用工具
先看模型侧返回里有没有tool_calls。如果模型压根没生成工具调用,可能是工具描述太模糊,或者模型本身对工具调用支持不好。换一个工具调用能力强的模型,或者在 prompt 里明确「你必须使用 queryOrder 工具查询」。TaoToken 的模型列表里可以挑支持 function calling 的型号,具体在文档页有说明:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
5.4 JSON-RPC id 重复导致会话中断
如果你自己写了客户端重试逻辑,注意每次请求的id要递增,不能复用。MCP 规范明确要求同一会话内 id 不得重复,复用了服务端可能直接断连。用 Spring AI 的McpSyncClient不用管这个,框架内部维护了计数器。
5.5 服务端返回 406 或 415
Streamable HTTP 对Content-Type和Accept有要求。请求必须是application/json,响应期望application/json或text/event-stream。如果你前面挂了网关,检查网关有没有改写这两个头。
6. 接下来怎么走
MCP 的配置骨架搭起来之后,真正花时间的是工具粒度的设计和权限边界。我的经验是:一个 MCP 服务只暴露一组高内聚的工具,别把订单、库存、用户全塞一个服务里,否则模型在tools/list里看到几十个工具,选择准确率会下降。另外服务端的@Tool方法尽量幂等,因为模型重试是常态。
如果你要长期跑编码类 Agent,把 MCP 客户端接进 IDE 或 CLI 工作流,可以考虑用 Coding Plan 统一管理模型额度和调用:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。Claude Code 这类工具接 Anthropic 兼容端点的配置在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 有说明,MCP 那层配置和本文一致,换的只是模型接入地址。
最后留一个实操建议:先把本文的服务端 curl 验证跑通,再动客户端配置。JSON-RPC 层通了,后面都是 Spring 的装配问题,排障范围会小很多。