2026/10/15 6:06:37

Spring Boot 服务接入 MCP:让 AI 应用直接调用你的接口

Spring Boot 服务接入 MCP:让 AI 应用直接调用你的接口 1. 为什么要把 Spring Boot 服务塞进 AI 的“工具箱”第一次听到“把 Spring Boot 服务变成 AI 应用的工具”这个说法时我脑子里冒出来的第一个疑问是AI 应用自己不是能调 HTTP 接口吗为什么还要多此一举搞个协议来包装后来真正动手把一个内部订单查询服务接进 AI 工作流之后我才明白这里面的差别有多大。传统的做法是你在 AI 应用的代码里硬编码一个 HTTP 请求把 URL、参数、鉴权头全写死。这种方式在只有一个服务、一个接口的时候没问题但一旦你手上有十几个 Spring Boot 微服务每个服务又有几十个接口AI 应用那边就会变成一坨巨大的胶水代码。更麻烦的是接口改了参数、加了字段AI 应用那边得跟着改两边耦合得死死的。而 MCPModel Context Protocol模型上下文协议要解决的就是这个问题它把“工具”这件事标准化了让 AI 应用不需要知道你的服务长什么样只需要知道“有哪些工具可用、每个工具要什么参数”就行。MCP 本质上是一套约定规定了 AI 应用客户端和工具提供方服务端之间怎么描述能力、怎么调用、怎么返回结果。你可以把它理解成 AI 世界的“USB 接口标准”——以前每个设备都有自己的插头现在统一成 USB-C插上就能用。Spring Boot 服务通过 MCP 暴露出去之后任何支持 MCP 的 AI 应用都能直接发现并调用你的接口不需要为每个 AI 平台单独写适配层。这篇文章适合两类人看一类是手里有一堆 Spring Boot 服务、想让 AI 能直接调用它们的后端开发者另一类是想搞清楚 MCP 到底怎么落地、不想只看概念文档的 AI 应用开发者。我会从协议的核心机制讲起然后一步步把一个真实的 Spring Boot 服务改造成 MCP 服务端最后聊聊实测中踩到的坑和性能调优的经验。整个过程不需要你提前精通 MCP但需要对 Spring Boot 和基本的 HTTP 交互有概念。2. MCP 协议到底规定了什么三个核心角色和一条调用链2.1 客户端、服务端、宿主别被这三个词绕晕MCP 的架构里有三个角色刚接触的时候很容易搞混。我用一个生活化的场景来解释假设你是一个老板宿主应用你有一个助理MCP 客户端助理手里有一本通讯录MCP 服务端通讯录里记着各种专家的联系方式工具。宿主Host就是最终用户用的那个 AI 应用比如某个对话助手、某个 IDE 插件。它负责和用户交互决定什么时候需要调用工具。客户端Client宿主内部的一个组件负责和服务端建立连接、发送请求、接收结果。一个宿主可以同时连多个服务端。服务端Server就是我们的 Spring Boot 服务改造后的形态它对外声明“我有哪些工具、每个工具需要什么参数”然后等待客户端来调用。关键点在于宿主和客户端通常是同一个进程里的两部分而服务端是独立运行的。这种设计让工具提供方和 AI 应用方可以完全解耦——你改你的 Spring Boot 服务我改我的 AI 应用只要 MCP 协议不变两边就不用互相等。2.2 工具描述AI 怎么知道你有什么能力MCP 服务端启动后第一件事是向客户端声明自己有哪些工具。这个声明不是随便写个接口列表就行而是要用结构化的方式描述每个工具的名称一个简短的标识符比如query_order_status。描述一段自然语言说明告诉 AI 这个工具是干什么的。这段描述的质量直接决定了 AI 能不能正确选用这个工具。输入参数用 JSON Schema 描述每个参数的类型、是否必填、取值范围。比如order_id是字符串、必填include_history是布尔值、可选。输出结构工具返回的数据长什么样方便 AI 理解结果。我实测下来工具描述写得好不好对 AI 调用准确率的影响能差出三成以上。后面我会专门讲怎么写描述。2.3 调用链从用户一句话到 Spring Boot 接口执行完整的调用链是这样的用户在宿主应用里说“帮我查一下订单 A123 的状态”宿主把这句话和所有可用工具的描述一起发给大模型大模型判断需要调用query_order_status工具参数是order_id: A123。宿主通过 MCP 客户端把这个调用请求发给 MCP 服务端服务端解析请求、调用对应的 Spring Boot 接口、拿到结果再按 MCP 格式返回给客户端最后客户端把结果交给大模型生成自然语言回复。这条链里MCP 负责的是“客户端到服务端”这一段。服务端内部怎么调 Spring Boot 接口是开发者自己决定的。这就给了我们很大的灵活性——你可以直接调 Service 层的方法也可以走 HTTP 调自己的 Controller甚至调别的微服务。2.4 传输方式stdio 和 HTTP 该怎么选MCP 支持两种主要的传输方式传输方式适用场景优点缺点stdio本地工具、IDE 插件简单、无需网络配置、进程隔离只能本地、一个服务端对应一个客户端HTTP/SSE远程服务、多客户端支持远程、可多客户端共享需要处理网络、鉴权、连接管理如果你的 Spring Boot 服务是部署在服务器上、要给多个 AI 应用用的那肯定选 HTTP 方式。如果只是本地开发时给 IDE 里的 AI 助手用stdio 更省事。我下面主要讲 HTTP 方式因为这才是“把服务变成所有 AI 应用的工具”的典型场景。3. 动手改造把一个订单查询服务变成 MCP 服务端3.1 环境准备依赖选型和版本坑先说我用的技术栈Spring Boot 3.2.x、Java 17、MCP Java SDK。MCP 的 Java SDK 目前还在快速迭代版本兼容性是个大坑。我试过用 Spring Boot 3.1 配最新 SDK结果因为 Jakarta EE 版本冲突启动不了。稳妥的做法是Spring Boot 用 3.2 以上Java 用 17 或 21MCP SDK 用官方仓库里标注为 stable 的版本Maven 依赖大概长这样dependency groupIdio.modelcontextprotocol/groupId artifactIdmcp-server-spring-boot-starter/artifactId version0.10.0/version /dependency注意这个 starter 的版本号变化很快建议直接去官方仓库看最新稳定版不要照抄网上的旧版本号。除了 MCP SDK你还需要一个 JSON Schema 生成库来辅助描述工具参数。我用的victools/jsonschema-generator它能直接从 Java 类生成 Schema省得手写。3.2 定义工具从 Service 方法到 MCP Tool 的映射假设我们有一个现成的OrderService里面有个方法public OrderStatus queryOrderStatus(String orderId, boolean includeHistory) { // 查数据库、组装结果 }要把它变成 MCP 工具需要做三件事第一定义一个工具描述类用注解标注工具名和描述McpTool(name query_order_status, description 根据订单号查询订单当前状态可选返回历史流转记录) public class QueryOrderStatusTool { // ... }第二定义输入参数的结构。这里有个关键决策是用一个扁平的参数对象还是嵌套结构我的经验是参数尽量扁平因为大模型对嵌套结构的理解准确率会下降。比如order_id和include_history平级放不要包一层request对象。第三在工具的执行方法里调用 ServiceMcpToolCall public McpToolResult execute(QueryOrderStatusInput input) { OrderStatus status orderService.queryOrderStatus( input.getOrderId(), input.isIncludeHistory()); return McpToolResult.success(status); }这里有个容易忽略的点异常处理。如果 Service 抛异常MCP 服务端不能直接把堆栈扔回去而是要返回一个结构化的错误结果让 AI 能理解“这个调用失败了原因是订单不存在”。我一般会定义一个错误码枚举把业务异常映射成 MCP 能识别的错误格式。3.3 参数校验别让 AI 的幻觉参数打到数据库AI 生成的参数不一定靠谱可能传个空字符串、传个超长的 ID、传个不存在的枚举值。如果直接把这些参数透传到数据库查询轻则查不到数据重则引发性能问题。所以参数校验必须做而且要做得比普通接口更严格。我的做法是在工具执行方法入口加一层校验if (input.getOrderId() null || !input.getOrderId().matches(^[A-Z]\\d{6,12}$)) { return McpToolResult.error(INVALID_ORDER_ID, 订单号格式不正确应为大写字母开头后跟6到12位数字); }注意错误信息要写得让 AI 能理解并自我纠正。比如告诉它“订单号格式不正确”它下次就会尝试生成符合格式的参数。如果只说“参数错误”AI 就懵了。3.4 启动和注册让客户端能发现你的工具Spring Boot 启动时MCP starter 会自动扫描所有McpTool标注的类把它们注册到服务端的工具列表里。你需要配置的是传输层mcp: server: transport: http port: 8081 path: /mcp启动后客户端访问http://your-host:8081/mcp就能拿到工具列表。我建议在开发阶段加一个简单的健康检查接口返回当前注册的工具数量和名称方便确认服务端是否正常。4. 工具描述怎么写AI 才不容易选错4.1 描述不是文档写给模型看的不是给人看的很多人写工具描述时习惯性地写成技术文档的风格比如“该方法用于查询订单状态入参为订单号和是否包含历史记录”。这种描述对人来说很清楚但对模型来说信息量不够。模型需要知道的是什么场景下该用这个工具什么场景下不该用。我对比过两种写法。第一种是“查询订单状态”第二种是“当用户询问某个订单当前处于什么状态、是否已发货、是否已完成时使用此工具。不适用于查询订单金额明细或修改订单”。实测下来第二种写法的调用准确率明显更高因为模型能根据场景描述做匹配而不是靠猜。4.2 参数描述要给出示例和边界每个参数的描述里我建议包含三样东西类型说明、示例值、边界条件。比如order_id订单号格式为大写字母开头加6到12位数字例如A123456。不要传用户姓名或手机号。include_history是否返回订单的历史流转记录默认为false。如果用户问“这个订单经历了哪些环节”设为true。最后那句“不要传用户姓名或手机号”看起来有点多余但实测能有效减少模型把错误信息塞进这个参数的情况。4.3 工具粒度的取舍一个万能工具还是多个专用工具这是设计时最纠结的问题。比如订单服务你可以设计一个query_order工具通过一个query_type参数区分查状态、查金额、查物流也可以设计三个独立工具。我的经验是宁可多个专用工具不要一个万能工具。原因有两个。第一万能工具的参数组合空间太大模型容易选错参数。第二专用工具的描述可以写得更精准模型匹配场景时更准。代价是工具数量变多但 MCP 客户端通常能处理几十个工具问题不大。不过也不能太碎。如果一个工具只返回一个字段那也没必要单独建。我的一般原则是一个工具对应一个完整的用户意图。4.4 返回结果的结构化让 AI 能直接读懂工具返回的结果最好也是结构化的 JSON而不是一段拼接好的字符串。比如返回{ order_id: A123456, status: SHIPPED, status_label: 已发货, last_update: 2025-01-15T10:30:00Z }而不是订单A123456当前状态为已发货最后更新于...。结构化数据让模型能自己决定怎么组织语言回复用户灵活性更高。如果返回的是字符串模型只能照搬遇到需要二次加工的场景就无能为力了。5. 实测中踩到的五个坑和排查过程5.1 坑一工具注册了但客户端看不到第一次启动服务端后客户端死活拿不到工具列表。排查过程是这样的先确认服务端日志里有没有“registered tool”之类的输出发现有然后用 curl 直接访问 MCP 端点返回的是空列表。问题出在包扫描路径上——MCP starter 默认只扫描启动类所在包及其子包而我的工具类在另一个模块里。解决办法是在配置里显式指定扫描路径mcp: server: tool-scan-packages: com.example.order.tools,com.example.user.tools这个坑的教训是MCP starter 的自动配置不是万能的跨模块时一定要检查扫描路径。5.2 坑二中文描述导致 Schema 解析失败我一开始用中文写工具描述结果客户端解析时报 JSON 解析错误。查了半天发现是编码问题——服务端返回的 Schema 里中文字符没有正确转义。解决办法是在 HTTP 响应头里明确指定Content-Type: application/json; charsetutf-8并且确保序列化框架用 UTF-8 编码。这个问题在不同 MCP 客户端上表现不一样有的客户端能自动处理有的不行所以最好在服务端就处理好。5.3 坑三大模型反复调用同一个工具测试时发现模型有时候会连续调用同一个工具好几次参数还一样。原因是工具返回的结果里没有明确告诉模型“这是最终结果”。解决办法是在返回结果里加一个is_final: true字段或者在工具描述里说明“此工具返回的是完整结果无需重复调用”。后者更通用一些。5.4 坑四超时设置不合理导致调用中断订单查询涉及数据库偶尔会慢。默认的超时时间太短导致模型还没拿到结果就断了。我在服务端配置了 30 秒超时同时在工具执行方法里加了异步处理——如果查询超过 5 秒先返回一个“处理中”的状态让模型告诉用户稍等。不过这个方案会增加复杂度如果你们的接口响应稳定在 2 秒内直接同步返回就行。5.5 坑五并发调用时的线程安全问题MCP 服务端默认是多线程处理请求的如果你的工具类里用了成员变量存状态就会出现线程安全问题。我一开始在工具类里缓存了一个OrderService的实例结果并发测试时数据错乱。后来改成无状态设计所有状态都通过方法参数传递问题解决。这个坑比较隐蔽单线程测试时发现不了一定要做并发测试。6. 性能调优让工具调用快且稳6.1 连接池和超时别让数据库成为瓶颈MCP 服务端本身很轻量性能瓶颈通常在它调用的后端服务上。我的做法是给每个工具的执行方法配独立的线程池避免一个慢查询拖垮所有工具。线程池大小根据后端接口的 QPS 能力来定一般设成后端能承受的并发数的 80%。超时设置分两层MCP 传输层超时设 30 秒工具执行层超时设 10 秒。这样即使工具执行卡住传输层也不会一直挂着。6.2 结果缓存哪些工具适合缓存查询类工具适合加缓存但要注意缓存的粒度。比如query_order_status可以按订单号缓存 30 秒因为订单状态不会频繁变。但query_order_history就不适合缓存因为历史记录可能随时追加。缓存的 key 要包含所有影响结果的参数别漏了include_history这种布尔参数。我见过有人缓存时只用了订单号做 key结果带历史和不带历史的结果混在一起出了 bug。6.3 批量调用一次问多个订单怎么处理用户可能会问“帮我查一下 A123、B456、C789 这三个订单的状态”。如果模型逐个调用工具会有三次网络往返。更好的做法是提供一个批量查询工具接受订单号列表。但要注意限制列表长度防止模型生成一个包含几百个订单号的请求。我一般限制最多 20 个。6.4 监控和日志出问题时怎么快速定位MCP 服务端的日志要记录四样东西请求 ID、工具名、参数摘要、执行耗时。请求 ID 用于串联客户端和服务端的日志。参数摘要不要记完整参数避免敏感信息泄露记个哈希或者前几个字符就行。我还会加一个简单的指标接口返回每个工具的调用次数、平均耗时、错误率。这样能快速发现哪个工具出了问题。7. 从单个服务到工具生态后续可以怎么扩展把第一个 Spring Boot 服务改造成 MCP 服务端之后你会发现这套模式可以复制到其他服务上。但服务多了之后管理就成了问题。我的做法是建一个统一的 MCP 网关所有工具都注册到网关上客户端只需要连网关一个地址。网关负责路由请求到对应的后端服务同时做统一的鉴权、限流、日志。另一个扩展方向是动态工具发现。现在工具列表是启动时固定的如果后端服务增减了工具需要重启 MCP 服务端。可以做一个注册中心让各个 Spring Boot 服务启动时把自己的工具注册上去MCP 服务端定期拉取最新列表。不过这个方案会增加复杂度服务数量不多的话没必要上。还有一个值得尝试的方向是给工具加版本号。当工具的参数或行为发生变化时旧版本的客户端可能不兼容。在工具描述里加一个version字段客户端可以根据版本号决定是否使用某个工具。这个在工具生态变大之后会很有用。我个人在实际操作中的体会是MCP 的价值不在于技术有多复杂而在于它把“AI 调用外部能力”这件事标准化了。以前每接一个 AI 平台就要写一套适配代码现在只要服务端支持 MCP所有平台都能用。虽然目前 SDK 还在迭代、文档也不够完善但方向是对的。如果你手里有现成的 Spring Boot 服务花半天时间接一个工具试试就能感受到这种标准化的威力。