1. 为什么要把 Spring Boot 服务塞进 AI 的“工具箱”
第一次听到 MCP 这个词,很多人会下意识觉得又是哪个厂商造的新概念。但如果你手上正好有一堆跑了好几年的 Spring Boot 服务,里面封装着订单查询、库存校验、报表生成、权限判断这些实打实的业务能力,那你大概率会冒出一个念头:能不能让 AI 直接调用这些接口,而不是每次都要人肉复制粘贴参数、再把结果贴回对话框?
这个念头就是 MCP 要解决的核心问题。MCP 全称 Model Context Protocol,直译过来是“模型上下文协议”。你可以把它理解成 AI 应用和外部能力之间的一份“插座标准”。以前每个 AI 客户端想接你的服务,都得单独写一套适配代码;现在只要你的服务按 MCP 的规范暴露能力,任何支持这个协议的 AI 应用都能即插即用。
我拿一个真实场景来说明它的价值。假设你有一个 Spring Boot 写的内部工单系统,提供“按工单号查详情”“按状态统计数量”“批量导出周报”三个接口。没有 MCP 之前,运营同学想让 AI 帮忙分析工单,只能自己登录系统、导出 Excel、再上传给 AI。有了 MCP 之后,AI 应用可以直接调用这三个接口,用户一句“帮我看看本周未关闭的工单里有没有超期的”,AI 自己就去查了。
这篇文章适合三类人看:一是手里有 Spring Boot 项目、想让 AI 用起来的后端开发;二是正在做 AI 应用、需要接外部业务系统的工程师;三是对 MCP 好奇但还没动手试过的技术负责人。我会从协议原理讲到代码落地,把“为什么这么设计”和“实际怎么跑通”都讲透,中间踩过的坑也会一并交代。
需要先说明一点:MCP 本身还在快速演进,不同 AI 客户端对它的支持程度不一样。我下面讲的是基于当前主流实现方式的通用做法,具体到某个客户端可能会有细微差异,但核心逻辑是通的。
2. MCP 到底在解决什么:从“人肉搬运”到“协议对接”
2.1 没有 MCP 之前,AI 接外部服务有多别扭
在 MCP 出现之前,让 AI 调用外部服务主要有几种土办法。第一种是“提示词里塞接口文档”,把 API 的 URL、参数格式、返回示例全写进系统提示词,让模型自己生成 HTTP 请求。这种做法的问题非常明显:接口一多提示词就爆炸,参数稍微复杂模型就编错,而且没有任何类型校验,错了也不知道错在哪。
第二种是“函数调用”,也就是 Function Calling。这个方案比纯提示词进步很多,模型会按你定义的 JSON Schema 输出结构化参数。但它有个致命短板:每个 AI 平台定义的函数格式都不一样。你在 A 平台写好的函数定义,搬到 B 平台就得重写一遍。如果你的服务要被多个 AI 应用使用,维护成本会成倍上升。
第三种是“自己写中间层”,针对每个 AI 客户端做适配。这本质上是在重复造轮子,而且每接一个新客户端就要改一次代码。我见过一个团队为了接三个不同的 AI 工具,硬是维护了三套适配逻辑,后来接口一改,三套全挂。
MCP 的思路是把“能力描述”和“能力调用”标准化。你的 Spring Boot 服务只需要按 MCP 规范暴露一次,所有支持该协议的客户端都能发现并调用这些能力。这就像 USB 接口统一了外设连接方式,你不需要为每个电脑单独做一个鼠标接口。
2.2 MCP 的三个核心角色:Host、Client、Server
理解 MCP 的关键是搞清楚它里面的三个角色,我用一个生活化的类比来解释。
Host 是“宿主”,也就是用户直接交互的 AI 应用,比如某个桌面 AI 助手、某个 IDE 里的编程助手。它相当于一个“总调度中心”,负责和用户对话、决定什么时候需要调用外部能力。
Client 是“客户端”,它由 Host 创建和管理,专门负责和某一个 Server 建立连接、发送请求、接收结果。你可以把它理解成 Host 派出去的“联络员”,一个 Host 可以同时管理多个 Client,每个 Client 对接一个 Server。
Server 是“服务端”,也就是我们这篇文章的主角——把 Spring Boot 服务包装成的 MCP 服务。它对外声明“我能做哪些事”,并在被调用时执行真正的业务逻辑。Server 相当于一个“专业窗口”,只负责自己那一摊事,不关心是谁来调用。
这三个角色的关系是:Host 根据用户需求,通过 Client 向 Server 发起调用,Server 执行完把结果原路返回。整个过程中,Host 不需要知道 Server 是用 Java 写的还是 Python 写的,只需要知道它声明了哪些能力。
2.3 为什么是 Spring Boot 服务最适合做 MCP Server
有人可能会问,MCP Server 用什么语言写不行,为什么偏偏要拿 Spring Boot 服务来做?这里有几个很实际的原因。
第一,绝大多数企业的核心业务逻辑都在 Spring Boot 里。订单、库存、审批、报表这些能力不是凭空冒出来的,它们已经在 Java 服务里跑了很久,经过了充分测试。把这些现成能力暴露成 MCP,比用别的语言重写一遍要靠谱得多。
第二,Spring Boot 的生态成熟,做 HTTP 服务、参数校验、异常处理、日志记录都有现成方案。MCP Server 本质上也是一个服务,需要处理并发、需要做鉴权、需要记日志,这些 Spring Boot 都帮你解决好了。
第三,Java 的强类型特性在定义工具参数时是优势。MCP 要求 Server 声明每个工具的参数结构,Java 的类定义天然适合做这件事,配合注解可以自动生成参数描述,减少手写出错。
第四,团队维护成本低。如果团队本来就是 Java 技术栈,用 Spring Boot 做 MCP Server 不需要引入新的语言和构建体系,现有开发人员直接就能上手。
3. 动手之前:把 MCP 的核心概念翻译成 Java 能听懂的话
3.1 Tools、Resources、Prompts 三件套分别对应什么
MCP 协议里 Server 可以暴露三类东西:Tools、Resources、Prompts。这三个词听起来抽象,我用 Spring Boot 的视角给你翻译一下。
Tools 是“可执行的操作”,对应到 Spring Boot 里就是那些会改变状态或执行计算的接口,比如“创建工单”“计算折扣”“发送通知”。AI 调用 Tool 时,Server 会真正执行逻辑并返回结果。这是最常用的一类,也是我们重点要实现的。
Resources 是“可读取的数据”,对应到 Spring Boot 里就是那些只读的查询接口,比如“获取配置”“读取日志”“查询字典”。Resources 和 Tools 的区别在于,Resources 是幂等的、只读的,AI 可以放心地反复读取而不用担心副作用。
Prompts 是“预定义的提示模板”,对应到 Spring Boot 里可以理解成“封装好的操作流程”。比如“生成周报”这个 Prompt 可能内部会依次调用查询工单、统计数量、格式化输出三个 Tool。Prompts 让 AI 可以一键执行复杂流程,而不需要用户一步步描述。
对于大多数 Spring Boot 服务来说,我建议先从 Tools 入手,把最核心的几个业务能力暴露出来。Resources 适合暴露那些查询类接口,Prompts 可以等前两者跑通后再考虑。
3.2 传输方式怎么选:stdio 还是 HTTP
MCP 支持多种传输方式,最常见的是 stdio 和 HTTP 两种。这个选择直接影响你的部署方式,必须一开始就想清楚。
stdio 方式是 Server 作为一个子进程运行,通过标准输入输出和 Client 通信。它的优点是简单、无需网络配置、天然隔离;缺点是 Server 必须和 Client 在同一台机器上,而且一个 Server 进程通常只服务一个 Client。这种方式适合本地工具类的场景,比如 IDE 里的编程助手调用本地脚本。
HTTP 方式(包括 SSE 和 Streamable HTTP)是 Server 作为独立的网络服务运行,Client 通过网络调用。它的优点是 Server 可以独立部署、多个 Client 共享、方便做鉴权和限流;缺点是需要处理网络问题、需要配置端口和证书。对于 Spring Boot 服务来说,HTTP 方式几乎是必然选择,因为你的服务本来就是网络服务,没必要退化成子进程。
我实测下来的建议是:如果你的 Spring Boot 服务已经是一个独立部署的 Web 服务,直接用 HTTP 传输方式,把它当成一个普通的 REST 服务来对待。stdio 方式更适合那些一次性的、本地的脚本工具,不太适合企业级服务。
3.3 工具描述写得好不好,直接决定 AI 会不会用
这是最容易被忽视、但实际影响最大的一点。MCP Server 暴露的每个 Tool 都需要一段描述,告诉 AI 这个工具是干什么的、什么时候该用、参数怎么填。这段描述写得好不好,直接决定 AI 能不能正确调用。
我见过太多人把工具描述写成“查询工单”,然后抱怨 AI 老是调错。问题就出在描述太模糊。好的工具描述应该包含三个要素:这个工具做什么、什么场景下用、参数有什么约束。
举个例子,同样是查询工单,差的描述是“查询工单信息”,好的描述是“根据工单号查询单个工单的详细信息,包括状态、负责人、创建时间。适用于用户提供了明确工单号的场景。工单号格式为字母加数字,长度 10 位”。后者明确告诉 AI 了使用场景和参数格式,调用准确率会高很多。
参数描述同样重要。每个参数都要说明它的含义、格式、是否必填、取值范围。如果参数有枚举值,一定要列出来。这些信息会直接影响 AI 生成参数的质量。
4. 从零搭建:Spring Boot 接入 MCP 的完整实操
4.1 依赖选型与项目骨架
目前 Java 生态里做 MCP Server 主要有两种路径:一是用官方或社区提供的 MCP Java SDK,二是自己按协议手写 JSON-RPC 处理。我建议优先用 SDK,因为协议细节比较多,手写容易漏掉边界情况。
在pom.xml里引入 MCP 相关的依赖,通常包括核心协议库和 Spring Boot 的集成模块。具体坐标会随版本变化,你需要去对应的仓库确认最新版本。引入依赖后,项目骨架大致是这样的:一个配置类负责注册 MCP Server 和工具,若干个工具类分别封装具体的业务能力,一个启动类把服务跑起来。
这里有个容易踩的坑:MCP SDK 的版本和 Spring Boot 的版本要匹配。我遇到过 SDK 依赖了某个较新的 Jackson 版本,而项目里锁定了旧版本,结果启动时报序列化相关的错。解决办法是在dependencyManagement里统一 Jackson 版本,或者用mvn dependency:tree排查冲突。
项目结构我建议按职责分层:config包放 MCP 配置,tools包放各个工具实现,service包放原有的业务逻辑。工具类只负责参数转换和结果封装,真正的业务逻辑还是调用原有的 Service,这样不会污染现有代码。
4.2 把第一个业务接口包装成 Tool
假设我们有一个现成的TicketService,里面有个getTicketById方法。要把它包装成 MCP Tool,核心工作是三件事:声明工具元信息、定义参数结构、实现调用逻辑。
声明工具元信息时,需要给工具起一个清晰的名字(建议用英文小写下划线,比如get_ticket_detail),写一段前面说的那种详细描述,并指定参数结构。参数结构可以用一个 Java 类来定义,字段上加注解说明含义和约束。
实现调用逻辑时,工具方法接收参数对象,调用TicketService,把返回结果转换成 MCP 能识别的格式。这里要注意异常处理:如果业务抛异常,要捕获后返回一个结构化的错误信息,而不是让异常直接冒泡。AI 拿到清晰的错误信息才能决定下一步怎么做。
我建议第一个工具选最简单的查询接口来试,不要一上来就搞复杂的写操作。查询接口没有副作用,调错了也不会造成数据问题,适合用来验证整条链路是否通畅。
4.3 参数校验与类型映射的细节
MCP 的参数是通过 JSON 传递的,而 Java 是强类型的,中间的类型映射有很多细节要注意。
数字类型要特别小心。JSON 里的数字可能是整数也可能是小数,如果你的 Java 字段是int,但 AI 传了个3.0,反序列化就可能出问题。我的做法是:能用Integer就不用int,能用BigDecimal处理金额就不用double。对于明确是整数的参数,在描述里写清楚“必须是整数”。
日期时间类型也是重灾区。AI 生成的日期格式五花八门,有2024-01-01的,有2024/01/01的,还有带时区的。稳妥的做法是在参数描述里明确规定格式,比如“日期格式为 yyyy-MM-dd”,然后在工具方法里做一次格式校验和转换,不合法就返回明确的错误提示。
枚举类型要列出所有可选值。如果某个参数只能是“待处理、处理中、已完成”三个值之一,一定要在描述里写全,否则 AI 可能编出一个“进行中”这种不存在的值。
必填和选填要区分清楚。必填参数缺失时,要返回“缺少必填参数 X”这样的明确提示,让 AI 知道该补什么。选填参数要有合理的默认值,并在描述里说明默认行为。
4.4 本地跑通与调试的实用手法
代码写完后,怎么验证它真的能用?我总结了一套调试流程。
第一步是单元测试。不依赖任何 AI 客户端,直接调用工具方法,传入各种正常和异常参数,看返回结果是否符合预期。这一步能排除掉大部分逻辑错误。
第二步是用 MCP 官方的调试工具或支持 MCP 的客户端手动连接。连接成功后,先看工具列表能不能正确拉取,工具描述和参数结构显示是否正常。然后手动触发一次调用,观察请求和响应的完整内容。
第三步是看日志。MCP 的通信是基于 JSON-RPC 的,把请求和响应的原始 JSON 打出来,能快速定位是参数问题还是返回格式问题。我习惯在开发阶段把日志级别调到 DEBUG,把每次调用的入参和出参都记下来。
这里有个实用技巧:准备一组“标准测试用例”,包含正常调用、参数缺失、参数格式错误、业务异常等场景。每次改完代码都跑一遍这组用例,能快速发现回归问题。
5. 让 AI 真正会用:工具描述与调用体验的打磨
5.1 工具粒度:太粗和太细都不行
工具粒度是个需要反复权衡的问题。粒度太粗,一个工具干太多事,AI 很难准确填参数;粒度太细,工具数量爆炸,AI 选择困难。
我的经验法则是:一个工具对应一个明确的业务动作。比如“查询工单”和“关闭工单”应该是两个工具,而不是一个“工单操作”工具带个 action 参数。因为这两个动作的参数结构、使用场景、风险等级都不同,混在一起会让 AI 困惑。
但也不要细到把“查询工单状态”和“查询工单详情”拆成两个工具。如果它们返回的数据高度重叠,合并成一个工具、用参数控制返回字段的详细程度会更合理。
判断粒度是否合适,有个简单的测试:把工具列表给一个不熟悉系统的人看,如果他能在不看文档的情况下说出每个工具什么时候用,那粒度就差不多了。
5.2 返回值设计:给 AI 看的,不是给人看的
工具返回值的设计原则和普通 API 很不一样。普通 API 的返回值是给前端程序解析的,而 MCP 工具的返回值是给 AI 理解并转述给用户的。这意味着返回值要“自解释”。
我建议返回值包含三部分:状态标识、核心数据、人类可读的摘要。状态标识让 AI 知道调用成功还是失败;核心数据是结构化的业务数据,方便 AI 做后续处理;人类可读的摘要可以直接展示给用户,减少 AI 的转述负担。
举个例子,查询工单的返回可以是这样:状态是 success,核心数据里包含工单号、状态、负责人、创建时间等字段,摘要是一句“工单 TK12345 当前状态为处理中,负责人是张三,创建于 2024 年 1 月 1 日”。这样 AI 既拿到了结构化数据,又有一句现成的话可以回复用户。
错误返回同样要清晰。不要只返回“操作失败”,而要说明失败原因和可能的解决办法,比如“工单号 TK99999 不存在,请确认工单号是否正确”。
5.3 多工具协作时的上下文传递
当 AI 需要连续调用多个工具完成一个任务时,上下文传递就成了关键。比如用户说“把上周未关闭的工单都导出成报表”,AI 可能需要先调用“查询工单列表”,再调用“生成报表”,最后调用“保存文件”。
这里的问题是:第一个工具的返回结果,怎么传给第二个工具?MCP 本身不负责维护这种跨工具的上下文,需要靠 AI 自己从上一个结果里提取信息填到下一个调用里。所以工具返回的数据结构要尽量扁平、字段命名要清晰,方便 AI 提取。
我的做法是:对于经常需要串联的工具,在返回结果里额外放一些“便于后续调用”的字段。比如查询工单列表时,除了返回工单详情,还返回一个工单号数组,这样 AI 调用批量操作工具时可以直接用这个数组。
另外,工具描述里可以提示常见的调用顺序。比如在“生成报表”工具的描述里写“通常需要先调用查询工单列表获取数据”,帮助 AI 建立正确的调用链路。
6. 上线前必须想清楚的安全与稳定性问题
6.1 鉴权:别让 MCP 成为后门
把 Spring Boot 服务暴露成 MCP 之后,它就成了一个对 AI 开放的能力入口。如果不做鉴权,任何能连上这个服务的 AI 客户端都能调用你的业务接口,这是非常危险的。
鉴权的第一层是传输层鉴权。如果走 HTTP 传输,至少要加 Token 校验,每个 Client 连接时带上约定的 Token,Server 验证通过才允许调用。Token 要定期轮换,不要硬编码在代码里。
第二层是工具级鉴权。不是所有工具都对所有调用方开放。比如“删除工单”这种高危操作,应该只对特定来源开放。可以在工具方法里检查调用方身份,不满足条件就拒绝执行。
第三层是参数级校验。即使调用方有权限,也要校验参数是否合法。比如查询工单时,要校验调用方是否有权限查看该工单,不能因为对方知道工单号就放行。
我见过一个反面案例:某团队把内部管理接口直接暴露成 MCP,没做任何鉴权,结果被扫描到之后,有人通过 AI 客户端批量拉取了敏感数据。这个教训很深刻,鉴权这一步绝对不能省。
6.2 限流与超时:防止 AI 把服务打挂
AI 调用工具的频率可能远超人类操作。一个复杂的任务,AI 可能连续调用几十次工具。如果不做限流,很容易把后端服务打挂。
限流要分两个维度:一是单次会话的调用频率,二是全局的并发数。单次会话限流防止某个 AI 任务疯狂调用,全局限流防止整体过载。具体阈值要根据你的服务承载能力来定,可以先设一个保守值,观察一段时间再调整。
超时同样重要。AI 调用工具时,如果后端处理时间过长,Client 可能会一直等待。要给每个工具设置合理的超时时间,超时后返回明确的错误,让 AI 知道这次调用失败了,可以决定重试还是放弃。
另外要注意工具执行的幂等性。AI 可能会因为超时重试而重复调用同一个工具,如果工具不是幂等的,就会产生重复数据。对于写操作,要么设计成幂等,要么在返回结果里带上唯一标识,让 AI 能判断是否重复。
6.3 日志与审计:出了问题能查
MCP 调用必须有完整的日志记录。每次调用要记录:谁调的(调用方标识)、调了什么(工具名)、传了什么参数、返回了什么结果、耗时多久、是否成功。
这些日志有两个用途:一是排查问题,当 AI 行为异常时,可以通过日志还原它到底调了什么;二是审计,对于涉及敏感数据的调用,需要留痕以备追溯。
日志里要注意脱敏。参数和返回值里可能包含手机号、身份证号、金额等敏感信息,记录前要做脱敏处理。我一般会把敏感字段替换成掩码,只保留格式信息。
日志的存储和查询也要考虑。如果调用量大,日志会很快堆积,需要设置合理的保留周期和归档策略。查询接口要能按调用方、工具名、时间范围等维度快速检索。
7. 踩过的坑与实战经验
7.1 工具描述里的“隐形陷阱”
我踩过最深的坑是工具描述里的歧义。有一次我写了个“查询用户信息”的工具,描述里只说了“根据用户 ID 查询”,结果 AI 经常把用户名当成用户 ID 传进来。后来我在描述里明确写了“参数必须是数字 ID,不是用户名”,问题才解决。
还有一个坑是工具之间的描述太相似。我有两个工具,一个叫“查询订单”,一个叫“查询订单详情”,描述也差不多,AI 经常选错。后来我把它们合并成一个工具,用参数控制返回详细程度,反而更清晰。
另一个经验是:描述里要写“不适用场景”。比如某个工具只适用于查询已完成的订单,那就在描述里写明“不适用于查询进行中的订单”,避免 AI 在错误场景下调用。
7.2 参数默认值引发的误调用
给参数设默认值本来是为了方便,但有时候会适得其反。我有个工具的参数“时间范围”默认值是“最近 7 天”,结果 AI 在用户明确说“查上个月”的时候,还是用了默认值,因为它觉得不传参数更简单。
后来我改成了:对于关键参数,不设默认值,强制 AI 明确传入。如果 AI 没传,就返回错误提示“请指定时间范围”。这样虽然多了一次交互,但避免了错误结果。
对于确实需要默认值的参数,我会在描述里强调“如果不确定,请先询问用户,不要使用默认值”。这句话能显著减少 AI 的“自作主张”。
7.3 返回数据过大导致的截断问题
有一次我做了个“导出全量数据”的工具,返回了几万条记录。结果 AI 客户端直接把返回截断了,只拿到前一部分,后续处理全乱了。
这个坑的教训是:MCP 工具的返回值要控制大小。如果数据量可能很大,要么分页返回,要么只返回摘要和获取详情的入口。我后来的做法是:列表类工具默认只返回前 20 条,并在返回里说明“共 X 条,已返回前 20 条,如需更多请使用分页参数”。
对于确实需要返回大数据的场景,可以返回一个临时文件路径或下载链接,让 AI 告诉用户去下载,而不是把数据全塞进返回值里。
7.4 版本升级时的兼容性处理
MCP 协议和 SDK 都在快速迭代,升级时很容易出现兼容性问题。我遇到过一次 SDK 升级后,工具注册方式变了,原来的代码直接编译不过。
应对这个问题的办法是:把 MCP 相关的代码隔离在一个独立的模块里,业务逻辑不要直接依赖 MCP 的类。这样升级时只需要改这个模块,业务代码不受影响。同时要锁定依赖版本,不要用动态版本号,避免某天构建时突然拉到一个不兼容的新版本。
升级前一定要在测试环境完整跑一遍所有工具,确认行为没有变化。我习惯维护一份“工具行为基线”,记录每个工具在标准输入下的输出,升级后对比输出是否一致。
8. 这套方案还能怎么扩展
把 Spring Boot 服务变成 MCP Server 只是第一步。跑通之后,你会发现很多可以继续深挖的方向。
一个方向是工具的动态注册。现在工具是写死在代码里的,每次加新工具都要改代码、重新部署。如果能把工具定义放到配置中心或数据库里,就可以在不重启服务的情况下动态增减工具。这对于工具频繁变化的场景很有价值。
另一个方向是多服务聚合。一个 AI 应用可能需要同时调用多个 Spring Boot 服务的能力,如果每个服务都单独做一个 MCP Server,Client 要管理多个连接。可以考虑做一个“聚合网关”,把多个后端服务的工具统一暴露在一个 MCP Server 里,对 Client 来说只有一个连接。
还有一个方向是调用链追踪。当 AI 连续调用多个工具时,把整条调用链串起来,记录每个环节的输入输出和耗时。这对于分析 AI 行为、优化工具设计都很有帮助。可以在每次调用时生成一个 trace ID,贯穿整条链路。
我个人在实际操作中的体会是:MCP 的价值不在于技术本身有多复杂,而在于它把“AI 调用外部能力”这件事标准化了。标准化带来的最大好处是复用——你为 A 应用做的 MCP Server,B 应用可以直接用,不需要任何改造。这种复用性会随着你接入的 AI 应用越来越多而越来越明显。所以如果你手上有一堆 Spring Boot 服务,早点把它们 MCP 化,后面会省很多事。