☰
Spring Boot接入MCP:从零实现AI可调用的工具服务实战
2026/10/13 5:04:55 网站建设 项目流程

最早我把某个内部订单服务接进AI应用时,方式是给每家模型厂商单独写一套工具调用适配层:一份OpenAPI规范、一个Function Calling的参数协议转换器、再写一堆测试用例防止模型把参数传错。后来模型一换,适配层全要重写,而且每个模型对"工具描述"的敏感度还不一样,同样的参数文档在这家正常、在那家就频繁传错。这个问题的根子在于:业务服务和AI应用之间缺少一个双方共同遵守的协议层。

MCP(Model Context Protocol)解决的就是这件事。它相当于给Spring Boot服务装上一个标准插座,任何支持MCP的AI应用(桌面客户端、智能体框架、自定义Agent)都能直接插上来调用你的工具、读取你的资源。这篇文章我从头梳理了MCP的核心机制,再用Spring Boot从零实现一个可运行的MCP Server,覆盖工具定义、客户端调试、生产环境边界和实际踩坑,适合已经能写Spring Boot接口、但对MCP只停留在"听说过"阶段的开发者。

1. 为什么Spring Boot开发者绕不开MCP这一层

很多人的第一反应是:我的服务已经有REST API了,AI应用直接调不就行了,为什么要再套一层MCP?这个问题问得很对,如果只是让AI调用一个"查订单"接口,REST确实够用。但一旦涉及多个接口、多次调用、动态补全参数,问题就变了。

1.1 从"写死调用链"到"让模型自己决定调什么"

传统REST API是给人看的,接口约定靠文档、靠人肉阅读、靠前端代码固定好。AI应用不一样,模型不会像程序员那样读你的Swagger文档,它只能通过"工具描述"来理解你的服务能干什么、参数长什么样、什么情况下该调它。这就是Function Calling时代的做法:你手动把每个接口翻译成一个带JSON Schema的工具描述,告诉模型"什么时候用它、传什么参数"。

但Function Calling没有统一标准。OpenAI有一套参数格式,其他模型厂商各自有各自的结构;同一套工具定义换个平台就得重写;工具调用的上下文(比如用户问了订单又追问物流)得你自己拼装历史消息。说白了,每次对接都是定制开发,积攒下来的适配代码越来越像一座无法维护的巴别塔。

1.2 MCP与Function Calling的本质差异

MCP做的事情是"协议化":它定义了AI客户端与工具服务端之间的标准对话方式,包括如何初始化、如何列出工具、如何调用工具、如何订阅资源变化。Spring Boot服务只需要实现MCP协议端点,任何MCP客户端都能识别,不用再为每家模型单独适配。

这两者的差距我用一个表格说明:

对比维度Function Calling直连MCP接入
工具定义格式各平台自定义统一JSON Schema + 协议原语
客户端适配每个模型写一套一套协议到处通用
工具发现手动维护清单客户端自动拉取tools/list
上下文关联自己拼消息历史协议内建会话与上下文
资源与提示词不覆盖resources/prompts原生支持
服务暴露形态散落的HTTP端点统一端点,即插即用

1.3 Spring Boot选MCP的三个现实理由

第一个理由是生态话语权。MCP从诞生起就是为工具服务设计的,主流AI客户端和智能体框架都在原生支持它,Spring Boot服务接进去之后等于一次接入、处处可用。

第二个理由是存量代码改造代价小。Spring Boot的MCP集成模块支持把普通Service方法直接暴露成Tool,不需要为每个接口单独建适配层,老业务代码不用大改。我见过有项目把几十个查询方法批量注册成了MCP工具,整个改造只花了一天。 第三个理由是AI应用链路里"服务端"的定位越来越重。现在不少企业内部Agent已经不再只调公有API,而是直接操作内部业务系统。与其让Agent逐个对接你的REST接口,不如让它通过MCP统一通道进来,权限、日志、审计都能收敛在一个入口。

2. MCP协议骨架:借助"插座"思维理解四件事

MCP协议本身不复杂,但它引入了一套自己的术语体系,第一眼容易懵。我用一个类比把它拆开:MCP像一个统一充电协议,客户端是充电头,服务端是插座,双方都遵循同一个标准就能直接配合。

2.1 一次会话怎么建立:握手、能力协商与初始化

任何MCP客户端与服务端建立连接,第一步都是握手。客户端发送initialize请求,带上自己支持的协议版本和客户端能力;服务端返回自己支持的协议版本和服务端能力;双方确认版本兼容后,客户端再发一个initialized通知,真正进入工作状态。

这个机制很像SSH连接时的算法协商,目的都是避免"我以为你会、你以为我会"导致的黑话。Spring Boot的MCP集成把这个过程封装好了,你不需要手写握手逻辑,但必须理解它存在——后面排查"客户端连接上但工具列表拉不到"的问题时,八成就是卡在这一步。

2.2 三大原语:Tools、Resources、Prompts

MCP协议里最核心的三类能力,名字很直白:Tools是工具调用,Resources是资源读取,Prompts是预置提示词模板。

Tools是最常用的,它对应"AI应用能对业务系统做什么"。每个工具都有一个名称、一段描述、一个JSON Schema参数定义,客户端通过tools/list拿到全部工具清单,再通过tools/call触发调用。Spring Boot里一个加注解的Service方法就能变成一个Tool。

Resources解决的是"AI应用能读什么数据"。它以URI形式暴露数据,比如order://123、file:///docs/spec.md,比Tools更静态,适合给模型补充上下文。Prompts则类似模板,服务端定义好提示词片段,客户端按需拉取组合使用。理解这三个原语的区别,后面看SDK代码会清晰很多。

2.3 传输方式:stdio与Streamable HTTP怎么选

MCP支持两种主流传输通道。stdio模式是客户端把服务端当成子进程启动,通过标准输入输出交换JsonRPC消息,适合本地命令行工具、桌面客户端,好处是不需要端口、不需要网络配置。Streamable HTTP模式则是服务端暴露一个HTTP端点,客户端通过HTTP + SSE通道通信,适合Spring Boot这种跑在服务器上的应用。

维度stdioStreamable HTTP
部署形态客户端拉起子进程独立Web服务
网络要求本机进程通信可跨机器
认证鉴权不适用可在HTTP层加
典型场景本地工具、桌面端Web应用、微服务
Spring Boot适配度低,需jar包启动高,天然契合

Spring Boot服务接入MCP,绝大多数场景选Streamable HTTP,因为你的服务本来就是常驻Web服务,暴露一个/mcp端点顺理成章。

2.4 说人话的类比:MCP像统一充电协议

再回到插座的类比。以前每个设备一个充电头,对应"每个AI应用一个接入方式"。MCP把充电口统一了,Spring Boot服务只需要做成标准接口,任何支持MCP的AI应用都能直接插上来。协议内部的初始化、工具发现、调用约定,就是"插口里的金属触点",触点位置统一了,设备才能通用。

3. 实战:把一个订单查询服务变成MCP Tool

理论说再多,不如跑通一个最小示例。这一节我用一个订单查询场景演示完整链路:Spring Boot暴露一个"查订单状态"工具,客户端拿到工具列表,成功调用并返回结构化结果。

3.1 最小依赖与项目结构

基于常见的Spring Boot工程,只需额外引入MCP的Spring WebMVC集成模块与Web启动器。我用的示例版本以当前Maven中央仓库稳定版为准,SDK版本迭代很快,建议你在搭建时去仓库确认最新坐标。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-sdk-spring-webmvc</artifactId> <version>0.10.0</version> </dependency>

引入依赖后,Spring Boot应用启动时会自动提供一个MCP端点,默认路径在/mcp附近,具体以当前SDK的自动配置为准。如果你手上有多个MCP Server需要区分,在配置文件中显式声明服务名和版本:

spring.application.name=order-mcp-server # 以下为常见配置示例,字段名随SDK版本可能调整 spring.ai.mcp.server.name=order-tools spring.ai.mcp.server.version=1.0.0

如果你对自动配置的默认行为不放心,想完全掌控端点路径,可以通过配置调整后再验证效果。在动手前我建议先确认两件事:你的Spring Boot版本是否被当前MCP SDK支持,以及项目里是否已经存在其他Web拦截器可能挡掉/mcp请求。

3.2 定义一个Tool:从普通Service方法到工具暴露

假设我有一个订单服务,核心方法是根据订单号查询状态。传统做法是写一个Controller暴露REST接口,现在只需要在Service方法上加上@Tool注解:

import io.modelcontextprotocol.sdk.spring.annotation.Tool; @Component public class OrderQueryService { @Tool(description = "根据订单编号查询订单当前状态与物流信息,输入参数为订单号字符串,返回JSON") public String queryOrder(String orderId) { // 这里可以调用数据库、远程接口、缓存等已有逻辑 OrderInfo order = doQueryFromDatabase(orderId); return """ {"orderId":"%s","status":"%s","carrier":"%s","trackingNo":"%s"} """.formatted(order.orderId(), order.status(), order.carrier(), order.trackingNo()); } }

非常重要的一点:description一定要写明白"这个工具做什么、需要什么参数、返回什么",模型就是靠这句话决定何时调用你的工具。description写得模糊,模型会在不该调用的时候调用、或者在改调用的时候不调用。

如果你不想用注解,也可以编程式注册Tool。这种方式适合工具来自第三方系统、没法直接加注解的场景。两种方式在MCP协议层面没有任何区别,客户端看到的都是同一个tools/list清单。

### 3.3 参数设计:为什么推荐基本类型而不是自定义对象 我在实际项目中见过一个很容易踩的坑:@Tool方法直接接收一个自定义DTO对象,比如QueryOrderRequest(String orderId, String userId),结果客户端那边工具参数Schema生成得很奇怪,嵌套结构让模型经常传错字段。 原因是MCP的工具参数Schema走的是JSON Schema标准,模型对扁平化的基本类型字段理解准确度远高于嵌套对象。所以我的建议是:对外暴露的工具方法尽量用基本类型参数(String、Integer、Boolean),或者用一个小而扁平的DTO,并确保字段名见名知义。如果某个业务对象字段很多,宁可拆成两个工具,也不要让模型去猜一个8字段对象的含义。 ### 3.4 本地启动与用官方Inspector验证 服务写好之后,第一步不是接AI客户端,而是用官方提供的Inspector工具验证链路。Inspector可以以npx方式启动,然后在浏览器里填写Service端点地址。 ```bash npx @modelcontextprotocol/inspector

打开Inspector界面后,填上Spring Boot服务的MCP端点URL,点Connect。如果能正常看到工具列表,并且手动调用queryOrder能拿到预期JSON,说明服务端没问题。用Inspector调一次,会展示完整的JSON-RPC请求与响应,这对理解协议流程特别有帮助,也能顺便确认参数名、返回结构。

我在这个阶段常用的验证清单有三项:工具列表数量是否正确、点开工具后参数Schema是否符合预期、手动调用返回内容是否为合法JSON。三项全过,再进行客户端接入。

4. 客户端接入与调试:从Inspector到真实AI应用

服务端跑通只是第一步,真正的目标是把工具接入AI应用。这一节讲两种典型接入方式:本地进程式的stdio接入,和远程HTTP式的Streamable HTTP接入。

4.1 连接配置:stdio与HTTP两种形态下客户端怎么写

支持MCP的AI桌面客户端,通常允许你在配置里新增一个MCP Server。如果服务端是Streamable HTTP模式,只需填端点URL,比如http://localhost:8080/mcp。如果服务端是stdio模式,你需要提供一个启动命令,让客户端把服务端作为子进程拉起来。

下面的示例模拟了stdio场景下的启动方式,把Spring Boot应用打包后由客户端拉起:

java -jar order-mcp-server.jar --server.port=0 --spring.profiles.active=stdio

这里有两个细节容易被忽略。第一,stdio场景下不要让服务端占用固定端口,——它只在标准输入输出上通信,端口反而可能引发冲突。第二,如果你在Windows上调试stdio模式,注意进程退出异常问题,客户端把子进程杀了之后Spring Boot这边可能残留日志,不代表服务端有Bug。

4.2 工具描述如何影响模型调用:实际对比

我在调试时发现一个有趣现象:同一套Spring Boot服务,工具描述写法不同,模型调用成功率差异巨大。

最开始我把描述写成"查询订单",模型经常在用户问"我的快递到哪了"时不调用工具,因为"快递"和"订单"之间的语义关联不够强。把描述改成"根据订单号查询订单当前状态、物流进度与快递公司,用于回答任何关于订单进展、物流位置、配送状态的问题"之后,调用率明显提升。模型本质上是在做文本匹配,你给它的语义线索越多,它越容易做对决策。

参数名也有类似影响。参数叫orderId还是id,模型的理解成本不一样。我给工具参数命名时坚持用完整业务词汇,宁可长一点,也不要缩写。

4.3 多工具协作:连续调用与结果组装

生产场景里很少只调一个工具。举例来说,用户问"订单A到哪了?运费多少钱?",AI客户端应该先调用queryOrder拿到物流信息,再调用calculateFreight计算运费。MCP协议本身支持这种连续调用:客户端可以自己决定调用顺序,把前一个工具返回的信息作为后一个工具的上下文。

Spring Boot这边只需要把每个查询逻辑暴露成独立工具,不需要关心模型怎么编排。但有一点值得注意:工具返回结果的结构化程度。如果queryOrder返回一大段自然语言文本,模型就很难从中提取"运费计算所需字段"。我在定义工具返回值时强制用JSON字符串,并保证字段命名稳定,这样后续工具能精准拿到输入参数。

5. 上生产前的边界:权限、防注入、稳定性

MCP让AI应用能直接操作业务服务,这既是便利也是风险。生产环境不能像Demo一样裸奔,下面的边界问题是我在落地时逐个踩过之后总结出来的。

5.1 MCP没有内建安全模型,认证鉴权要自己加

MCP协议目前没有定义完整的认证授权规范,只建议在传输层做安全控制。这意味着:如果你的MCP端点暴露在公网,任何人都可能连接并列出你的工具,尝试调用。Spring Boot服务内如果要加认证,常见做法是在MCP端点前面套一层网关,或者用Spring Security拦截器校验请求头中的Bearer Token。

推荐路径是:MCP端点只走内部网络/内网网关,网关层做身份认证,再往下游传递可信用户标识。不要把数据库密码、云厂商密钥这些敏感信息放进工具参数或返回内容里,工具返回给模型的内容要遵循最小暴露原则。

5.2 输入校验与提示注入防护

AI应用调用你的工具时,参数是模型基于用户输入生成的。这意味着用户的原始文本可能通过模型间接进入你的工具参数。如果你有一个"执行系统命令"或"查询任意SQL"的工具,必须做严格的参数白名单校验。

我处理这类风险的原则是三层:工具层只接收业务语义参数(比如订单号而非SQL片段);服务内部把参数映射到预编译的查询语句或受控代码路径;日志中不对原始参数做全文打印,避免敏感信息外泄。另外提醒一句,不要在工具返回值里拼接未经转义的用户文本,这可能导致下游模型被"提示注入"影响行为。

5.3 超时、并发与日志:让工具调用可观测

MCP工具调用是同步请求,如果工具内部逻辑执行很久,AI客户端可能先超时。我给耗时操作设置了统一的超时阈值,超过阈值的走异步转人工处理,而不是让客户端一直挂起。并发方面也要注意:AI应用触发工具调用时往往集中爆发,数据库连接池和线程池要按峰值预留。

日志是排查问题的关键。每个工具调用都建议记录:谁在什么时间调了哪个工具、传了什么参数、返回了什么结果、耗时多少。这样一旦出现"模型行为异常",可以顺着日志定位是模型决策问题还是服务端问题。

6. 踩坑记录:五个真实问题及排查链路

协议对接这种事,书面文档永远绕不开现实偏差。这一节记录我实际遇到的五个问题,每个都给出排查路径,而不是直接给答案,因为下次你遇到的报错大概率长得不一样。

6.1 工具不生效:工具名被规范化导致调用400

现象:Inspector里明明能看到工具,工具列表返回正常,但一调用就返回错误。

排查过程:先看Inspector里的实际请求与响应,发现客户端发出的tools/call中工具名和tools/list里的名字不一致——我把方法命名为queryOrderInfo,但SDK自动把方法名转成了query_order_info,客户端按queryOrderInfo调用就找不到工具。问题出在工具名的命名规则上:MCP工具名约定使用snake_case,Spring Boot的自动映射可能做了转换,但我没有意识到。

解决方式:在@Tool注解上显式指定name,或者统一用snake_case命名方法。之后验证时先看tools/list返回的准确名称,再决定客户端怎么调用。

6.2 中文参数乱码与JSON Schema类型错配

现象:工具描述里有中文,传给模型后参数值出现乱码;或者前端传入"123"字符串,但Schema要求integer类型,模型传成了字符串导致校验失败。

排查过程:中文乱码先检查HTTP通信编码,确保Spring Boot配置了UTF-8字符集,MCP端点返回Content-Type时带charset=utf-8。类型错配则要打开工具Schema看字段类型声明:如果数据库里订单号是数字但业务上也不需要计算,建议声明为string,避免模型把前导零去掉。

解决后我养成了一个习惯:每个工具的参数类型和返回值类型,都用最简单的类型表达,不给模型留出自由发挥空间。

6.3 SSE流式连接被网关切断

现象:本地连接一切正常,部署到服务器后AI客户端可以连接但过一会儿就断开,重新连接后才恢复。

排查过程:看服务端日志,发现连接不是被Spring Boot关闭的,而是前置网关的空闲超时把SSE通道切了。考虑到MCP长连接需要持续保持,网关层对MCP端点关了代理缓冲,并把读超时调到足够大。

这里想提醒的是:如果你用Nginx这类反向代理,默认配置对SSE并不友好。需要针对性调整与代理超时相关的参数。同理,云负载均衡器也有关闲置连接断开策略,要单独配置MCP端点的白名单。

6.4 多个工具返回结构不一致导致客户端解析失败

现象:接了两个工具,一个返回JSON字符串,另一个返回纯文本,结果客户端Agent解析第二个工具结果时说JSON解析失败。

排查过程:翻看两个工具的实现,发现前者是精心构造的JSON返回,后者是直接拼接的业务文案。检查MCP协议对工具返回的结构要求后发现,统一返回结构是最稳妥的做法。于是把返回类型全部改成合法JSON,并保证外层是稳定的对象结构,内层字段才放业务数据。

修复之后模型对工具结果的依赖判断稳定多了,这也印证了"给模型的输入输出越规整,模型行为越可靠"。

6.5 版本兼容:协议版本、SDK版本与客户端版本三者的对应关系

现象:升级Spring Boot MCP SDK后,原来能用的客户端连接时报协议版本不支持。

排查过程:在错误信息里看到协议版本协商失败,原因是SDK默认使用较新的MCP协议版本,而客户端支持的是旧版。MCP协议目前仍在演进,不同版本之间接口细节有差异。这种情况下要么升级客户端,要么固定SDK版本,保持与现有客户端匹配。

这事给我的经验是:MCP相关依赖版本不要随手升最新,先看客户端支持什么协议版本,再决定SDK版本。协议升级属于破坏性变更,生产环境谨慎为大。

7. 一点后续建议

整个项目从调研到跑通,我最深的一点体会是:MCP的价值不在某个具体实现,而在于它把"AI应用该以什么方式使用工具"这件事标准化了。Spring Boot服务接入MCP之后,你不再需要关心对面是哪家的模型、是桌面应用还是Agent框架,它们都按同样的规则来list、来call。

最后分享一个小技巧:工具描述里把"成功/失败/业务状态"三种情况都写清楚,比如注明"订单不存在时返回status=NOT_FOUND",模型处理边界情况会聪明很多。这个细节我试了几次,对调用准确率的提升比改任何代码配置都明显。MCP的坑不少,但每踩一个,对协议的理解就深一层,值得折腾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询