用 Spring AI 将内部接口接入 MCP
教程通过完整 Java 项目,将内部业务接口封装成 Claude、Cursor 等客户端可调用的 MCP 工具。服务端项目无需大模型 API 密钥即可运行,旨在减少不同 AI 客户端重复集成内部系统的工作。
AI 深度解读
让 Claude 查一笔内部订单,难点往往不在模型能力,而在它接触不到订单系统。原文用 Spring AI 搭建 MCP Server,把查询、搜索和退款操作变成 Claude、Cursor 可以发现并调用的工具,试图减少每增加一个 AI 客户端就重写一套集成的成本。
值得读的是它把协议交互、Java 注解和工具描述串到了一起。但提供的原文在退款方法和 curl 验证命令处都有截断,因此它更适合作为实现思路参考,不能直接当成完整可运行项目。
MCP 减少的是重复集成,模型并不会因此自动懂业务
作者面对的是一个常见问题:订单、库存、日志分属不同系统,产品经理用 Claude Desktop,工程师用 Cursor,内部客服又有自己的 Agent。每条接入链路都要处理工具描述、参数和错误,系统与客户端越多,重复工作越多。
MCP 希望把这种关系从 M×N 变成 M+N:客户端遵守统一协议,业务系统通过统一接口发布能力,各自完成一侧适配。
这里有三个角色。Host 是用户操作的应用,例如 Claude Desktop、Cursor;Client 是 Host 内部连接某个 Server 的连接器;Server 则是开发者编写的能力提供方。一个 Host 可以启动多个 Client,分别连接多个 Server。
Server 提供的能力也有区别:
- Tools 是模型可以选择调用的操作,例如查询订单。
- Resources 是客户端可以读取并放入上下文的资料,例如排障手册。
- Prompts 是用户通过界面选择的提示模板,例如退款审核清单。
模型并不直接处理 MCP。Host 负责把工具列表转换成模型理解的 Function Calling 格式,再把调用结果送回模型。
因此,工具接上了,不代表模型就会用对。作者建议把「查询订单」写成「按订单号查询详情,订单号以 SO 开头」,让描述同时交代用途、参数格式和操作边界。
工具描述是模型选择操作的依据,但不是业务规则的执行器。
先走通协议,再排查 Claude 和 Cursor
原文把一次调用拆成初始化、列举工具和执行工具。消息使用 JSON-RPC 2.0,初始化示例如下:
{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"cursor","version":"1.0"}}}
重点是 protocolVersion。它表达客户端使用的协议版本;clientInfo.version 中的 1.0 是另一项客户端信息,两者不能混为一谈。
初始化之后,Client 发送 notifications/initialized 表示就绪。它没有 id,属于通知,不应等待对应的 JSON-RPC 响应。随后通过 tools/list 获取工具名称、描述和 inputSchema,再用 tools/call 提交工具名与参数。执行结果放在 content 数组里,供模型继续处理。
传输方式有两种。stdio 由 Host 启动子进程,通过标准输入输出交换消息,适合本地工具。它最容易踩的坑是日志污染:stdout 必须只输出协议数据,额外的 System.out.println 或 INFO 日志都可能破坏解析。
Streamable HTTP 更适合独立部署、供多个客户端访问。原文指出,2025-03-26 规范修订引入这种方式,替代早期收发分离的 HTTP+SSE 设计。示例把入口统一为 /mcp,响应可以是 JSON,也可以采用服务器发送事件(SSE)格式。
作者展示的有状态流程会从响应头取得 Mcp-Session-Id,后续请求携带它关联会话。不过,原文对断线恢复的描述比较笼统,没有提供相关服务端配置,不能据此认定所有部署都能自动续接。
排障顺序很实用:先用 mvn spring-boot:run 启动,再用 curl 验证协议,最后连接 AI 客户端。遗憾的是,提供的 curl 内容在第二步请求头处截断,无法照抄完成验证。
Spring AI 省掉协议代码,参数和写操作仍要自己管
示例采用 Java 21、Spring Boot 4.1.2、Spring AI 2.0.2,通过 spring-ai-bom 管理依赖,并引入 spring-ai-starter-mcp-server-webmvc。这些是原文列出的版本组合,提供的材料没有附带构建结果。
服务监听 8080,应用名为 mcp-order-server,MCP Server 名为 order-admin、版本为 1.0.0;配置使用 protocol: streamable 和 type: sync。tool-response-mime-type 将 get_order 标记为 application/json。
Spring AI 扫描 @McpTool 方法,根据签名和 @McpToolParam 生成 JSON Schema,注册工具,再在收到调用时执行方法、序列化结果。下面这段展示了如何把订单查询变成工具:
@McpTool(
name = "get_order",
description = "按订单号查询订单详情,返回状态、金额、下单时间。订单号格式为 SO 加日期加序号。这是只读操作。",
annotations = @McpTool.McpAnnotations(readOnlyHint = true, openWorldHint = false))
public Order getOrder(
@McpToolParam(description = "订单号,例如 SO20261027001", required = true) String orderNo) {
Order order = orders.get(orderNo.trim().toUpperCase(Locale.ROOT));
if (order == null) {
throw new IllegalArgumentException("订单不存在: " + orderNo);
}
return order;
}
要看的是参数声明这一行:@McpToolParam 只有 description 和 required,参数名依赖编译后的字节码,因此需要 -parameters。作者说明 Spring Boot 父 POM 默认启用它;迁移旧项目或使用公司统一编译配置时,应检查生成的 Schema 是否出现 arg0、arg1。
另外两个工具是 search_orders 和 create_refund_ticket。搜索支持客户姓名、状态与页码,页码从 1 开始,每页 10 条;状态包括 PAID、SHIPPED、REFUNDING、CLOSED。分页的目的很直接:避免把大量订单塞进模型上下文。
退款工具带有 readOnlyHint = false、destructiveHint = true、idempotentHint = false,并要求调用前确认订单号和原因。可见代码会把状态改成 REFUNDING,拒绝 CLOSED 订单,但方法尾部缺失,也没有展示持久化工单流程。
Resource 用 @McpResource 暴露 ops://runbook/{service},返回 text/markdown 排障手册。Prompt 用 @McpPrompt 注册 refund-review,通过 @McpArg 接收订单号和原因;@McpComplete 提供示例订单号的前缀补全。这让业务操作、参考资料和人工发起的工作流各有入口。
我的判断
我认同它最有用的两点:复用协议适配,以及先验证服务、再连接客户端。前者适合确实存在多个 AI 入口的团队,后者能缩小排障范围。只有一个内部 Agent、少量固定接口时,直接使用 Function Calling 也可以,原文没有提供两种方案的成本对照。
工具命名、明确错误和分页都是合理的工程建议,但作者没有给出工具选择准确率、失败重试率或 token 消耗的对照数据。「描述改好后命中率立刻变化」应当视为经验判断。
更需要打问号的是确认机制。原文把 destructiveHint 与客户端确认弹窗联系得很紧,却没有提供客户端版本、配置或验证记录。提示和注解可以表达意图,不能替代服务端授权、状态校验与幂等控制。
示例数据也保存在 ConcurrentHashMap 中。它展示了调用路径,尚未覆盖真实订单系统的持久化与操作审计。服务端不需要 LLM API key,则是因为它只发布能力;模型如何接入仍由客户端负责。
能拿来做什么
- 如果团队同时用 Claude Desktop、Cursor 和内部 Agent 查业务数据,可以先用一个只读
get_order验证复用价值,检查三个入口是否能理解同一份工具描述与参数。 - 接入现有 Java 服务时,先检查
-parameters和tools/list的输出,再调模型提示,避免把参数名丢失误判为模型能力问题。 - 做订单搜索时,沿用分页、明确状态值和显式错误的设计,让模型获得可解释、可继续处理的结果。
- 增加退款等写操作前,先补齐服务端权限、状态检查和幂等处理,再验证客户端如何呈现确认;不要把
instructions中的确认要求当成操作保障。