从去年底开始MCPModel Context Protocol这几个字母在 AI 工程圈里的出现频率越来越高。尤其是 Anthropic 开源协议规范之后几乎所有主流 AI 框架都在往这个方向靠。Spring AI 也不例外在 1.0 版本里直接提供了对 MCP 的完整支持而且封装成了非常顺手的 Boot Starter 风格。我这次在项目里实际落地了一个 MCP 服务器端把基于 Spring Boot 的框架整条链路摸了一遍这篇笔记就详细展开讲讲我是怎么做的以及中间踩过的坑。1. MCP 到底解决了什么问题从工具孤岛到统一协议先说清楚 MCP 是干嘛的否则后面代码看起来会有点莫名其妙。过去我们要给大模型接外部工具基本是各自为战调用天气接口写一个 function calling 定义调用数据库查询再写一套 JSON Schema调用内部系统 API 又要单独写一套工具描述。每个模型厂商一套规范甚至同一个模型不同版本之间还有差异维护成本非常高。MCP 想做的事情本质上就是给AI 模型调用外部工具这件事定一个通用的标准化协议类似数据库世界里的 JDBC、前端世界里的 HTTP。MCP 的架构可以简单理解成三端MCP Host比如 Claude Desktop、Spring AI 中的 AiClient、MCP Client负责与服务器通信的客户端组件、MCP Server实际执行工具逻辑的服务端。Server 通过暴露 tools、resources、prompts 三类能力给 Host 调用。我这次做的就是一个 MCP Server它自己本身就是个 Spring Boot 应用通过标准协议把内部的能力暴露出去让任何支持 MCP 的客户端Spring AI、Claude Desktop 等都能直接调用。从实际价值角度讲MCP Server 最吸引我的一点是一次开发处处接入。服务器端写好的工具不需要为每个客户端定制适配层只要客户端支持 MCP 协议天然就能发现并调用这些工具。对于团队内部有多个 AI 应用要复用同一批业务能力的情况这个收益是立竿见影的。另外它天然支持 JSON-RPC 2.0 和两种传输模式stdio标准输入输出和 Streamable HTTP这给部署方式也留下了足够的灵活度。2. Spring AI 的 MCP Boot Starter 设计思路与环境准备Spring AI 做 MCP 支持的方式很符合 Spring 家族一贯的思路——用自动配置把你的代码和协议细节拆开。你只需要关注业务工具本身的实现剩下的握手、会话管理、JSON-RPC 封装、工具注册框架都替你处理掉了。官方提供了两个 Starter一个是spring-ai-starter-mcp-server用于构建 MCP 服务器另一个是spring-ai-starter-mcp-client用于构建 MCP 客户端。这篇主要讲服务器端。2.1 为什么选 Boot Starter 而不是从零实现协议确实有直接从零实现 MCP 协议的可能性官方规范文档也不长按 JSON-RPC 的格式自己写一套消息处理也不是不行。但我不建议这么做原因有三条。第一协议细节中的边界情况非常多比如会话初始化握手顺序、工具调用结果的错误码约定、流式输出的消息分帧自己处理容易漏。第二Spring AI 已经在持续跟进协议演进像 MCP 规范从早期版本到 2025 年的更新框架都及时适配了你不需要自己追规范变更。第三Boot Starter 天然融合了 Spring Boot 的生态能力配置、拦截器、监控都能复用现成的东西。基于这些考虑直接站在 Spring AI 肩膀上是最稳的路线。2.2 推荐的环境版本组合我这次用的组合是经过实测稳定的列出来给大家参考组件版本说明JDK17Spring Framework 6.x 的硬性要求Spring Boot3.3.x 或 3.4.x与 Spring AI 1.0 兼容Spring AI1.0.0 GA建议用 GA 版方便稳定MCP Java SDK由 Spring AI 传递引入无需手动指定值得注意的是 Spring AI 1.0 版本里MCP 模块的坐标和 API 相比 0.x 版本有调整。如果你之前看过老版本的教程一定要检查版本号。用 Maven 的话核心依赖配置大概长这样dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId version1.0.0/version /dependency需要注意 Spring AI 的依赖仓库不在中央仓库的默认源里需要额外配置 Spring 官方仓库。这一点非常容易忽略但少了它 Maven 会直接报依赖解析失败。3. 构建一个 MCP 服务器从 Tool 定义到自动注册Spring AI MCP Server 的核心使用模式就是写 Tool 类。每个 Tool 就是一个带有Tool注解的方法框架启动时会自动扫描这些方法把它们注册成 MCP 协议里的工具并生成对应的 JSON Schema 描述。客户端通过大模型决定调用哪个工具时协议层会自动把参数传进来执行。3.1 创建项目与基础配置我用 Spring Initializr 生成了一个空项目只保留了 Web 和 Actuator 依赖然后手动加了 Spring AI 相关的依赖。不过这里有个细节要注意如果 MCP Server 准备用 Streamable HTTP 模式对外提供服务Web 依赖是必需的如果只是本机用 stdio 模式Web 依赖其实可以省掉。我在实际项目里因为要部署到服务器上供多个客户端访问所以选择了 HTTP 模式同时也保留了 stdio 方式做本地测试。最基础的配置文件长这样spring.application.namemcp-server-demo server.port8080 spring.ai.mcp.server.enabledtrue spring.ai.mcp.server.namemcp-server-demo spring.ai.mcp.server.version1.0.0 # 传输模式http 或 stdio spring.ai.mcp.server.transporthttp第一次跑起来的时候Spring Boot 启动日志里会打印一条类似Registered MCP tool: getNowWeather这样的信息。看到这个就说明你的工具已经被自动注册进 MCP 协议层了。这背后 Spring AI 的McpToolAutoConfiguration会自动扫描容器中的 Tool 方法为每个方法生成一个McpSchema.Tool定义并注册到工具注册表中。3.2 写第一个真正的业务工具我拿一个实际的小功能来演示写一个根据城市名获取天气数据的工具。虽然是 Demo但整个流程和企业内部的真实业务工具完全一致。Component public class WeatherToolService { private static final MapString, String WEATHER_DATA Map.of( beijing, 晴最高温度 32°C最低温度 22°C微风, shanghai, 多云转阴最高温度 30°C最低温度 25°C东南风3级, guangzhou, 雷阵雨最高温度 33°C最低温度 26°C南风2级 ); Tool(description 根据城市名查询当前天气情况城市名请使用拼音例如北京为 beijing) public String getCurrentWeather(String cityName) { return WEATHER_DATA.getOrDefault(cityName, 暂无该城市的天气数据); } }Tool注解是核心description字段非常重要。这段描述是大模型判断什么时候该调用这个工具的关键依据描述写得越清晰模型调用的准确率越高。比如这里的描述里说明了城市名请使用拼音模型就会在用户说北京时自动转化为 beijing 再传参。这个转化过程看起来没什么实际操作中如果描述写得含混模型可能传中文、可能传英文、甚至可能不调用差别很大。如果你想给工具加更精细的参数描述可以用ToolParam注解Tool(description 查询某个用户最近一周的订单数量) public long countUserOrders( ToolParam(description 用户唯一标识ID) Long userId, ToolParam(description 查询的起始日期格式 yyyy-MM-dd) String startDate, ToolParam(description 查询的结束日期格式 yyyy-MM-dd) String endDate ) { // 业务逻辑 return orderService.countOrders(userId, startDate, endDate); }有了这些描述之后框架会生成对应的 JSON Schema大模型就能理解工具的入参结构并且在对话过程中自动补全参数。这个体验比传统的 function calling 要顺滑很多因为 MCP 的定义是标准化的不限制在某个模型厂商的私有格式里。3.3 MCP 协议层的工具注册链路Spring AI 在处理工具注册时有一套完整的链路。启动时McpToolAutoConfiguration会收集所有标注了Tool的方法然后通过ToolCallback包装成 MCP 协议需要的ToolSpecification再交给McpServer实例。当客户端连上来并发送tools/list请求时服务器会返回所有已注册工具的定义列表。这里有个容易忽略的机制Tool 方法所在的 Bean 必须是容器管理的也就是类上要有Component、Service这类注解。否则 Spring 扫描不到方法再标准也不会被注册。我在一开始测试时就犯过这个错误写了一个纯手工 new 出来的服务类结果启动日志里始终看不到工具注册信息排查了半天才反应过来是 Bean 没有进容器。4. 两种传输模式的选型逻辑与实战配置MCP Server 支持两种传输方式这两种方式对应的使用场景和部署方式有本质差异一定要搞清楚再选。4.1 stdio 模式本地进程间通信stdio 模式下MCP Server 不是一个独立监听的网络服务而是作为子进程被 MCP Client 启动双方通过标准输入输出流进行 JSON-RPC 消息交换。这种模式非常适合本地开发调试或者嵌入式场景——比如你想在 Claude Desktop 里配置一个本地工具直接在配置文件的 command 里写上java -jar your-mcp-server.jar就能跑。Spring AI 配置 stdio 模式只需要改一个参数spring.ai.mcp.server.transportstdio需要注意stdio 模式下 Spring Boot 的 Web 容器通常是不需要的因为没有任何端口要监听。如果你用的是spring-boot-starter-web反而要小心别让端口占用导致启动异常。更干净的做法是只引入spring-boot-starter基础依赖加上 Spring AI 的 MCP Server Starter同时把 Web 相关的自动配置排除掉。4.2 Streamable HTTP 模式跨网络服务化HTTP 模式则是把 MCP Server 部署成一个常规的 Web 服务客户端通过网络连接到服务器。这里的端点就是服务器暴露的 HTTP 接口Spring AI 默认会把它挂在一个固定的路径上默认配置是/mcp。客户端配置时把这个 URL 填进去就行标准端口 8080也可以自行通过配置项调整。spring.ai.mcp.server.transporthttp spring.ai.mcp.server.base-path/mcp选 HTTP 模式的主要动机是资源共享。团队内部可以部署一个中心化的 MCP Server让多个 Agent 应用都去连接工具能力做到统一维护、统一升级。我实际部署时就是这种架构一个 Spring Boot 服务承载了大约十几个业务工具上游有三个不同的 AI 应用在通过 MCP 调用完全解耦。4.3 鉴权与开放策略MCP Server 一旦走 HTTP 模式对外暴露鉴权就是一个必须考虑的问题。Spring AI 的 MCP Server 本身不强制鉴权它把安全策略交给 Spring Security 体系处理。你可以按常规 Web 服务的方式对/mcp路径做保护比如要求客户端携带 API Key 或 Bearer Token。我这里用了个简单的拦截器方式因为团队内部网络相对可信只需要一个自定义请求头做校验Component public class McpAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(X-MCP-Token); if (!your-secret-token.equals(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } }然后把拦截器注册到 WebMvcConfigurer 上只拦截/mcp/**路径。这样一来协议接口就只会被明确授权的客户端访问。如果你对安全要求更高建议直接用 Spring Security 的方案可以做更细粒度的控制比如为不同的工具设置不同的调用权限。5. 让 MCP Server 真正跑起来客户端联调与验证服务器搭好了工具注册了接下来最关键的一步就是验证协议链路是否真的通了。我一般分两个层面去验证先用最原始的 MCP 客户端工具做冷启动测试看看协议基础的握手和消息交换是否正常再接入 Spring AI 的 MCP Client 做完整的大模型调用链路测试确认模型确实能发现工具并正确调用。5.1 用命令行客户端做冷启动验证Swift 社区有一个对 MCP 调试很有帮助的工具叫mcp-cli可以直接通过命令行跟 MCP Server 通信。如果你不想引入额外的工具也可以直接用 curl 调 HTTP 端点。MCP 使用的是 JSON-RPC 2.0 格式初始化请求长这样curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: {name: test-client, version: 1.0.0} }, id: 1 }如果一切正常你会收到一个包含服务器名称、版本号和协议版本确认的响应。接着再发tools/list请求就能看到所有注册的工具列表了curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, method: tools/list, params: {}, id: 2 }返回的 JSON 里就能看到getCurrentWeather这个工具的完整描述和参数结构。这一步能通说明协议层的自动注册和序列化都没问题。5.2 接入 Spring AI Client 做端到端测试为了验证大模型真的会调用这些工具我建了一个 MCP Client 测试模块通过spring-ai-starter-mcp-client连接到我刚才部署的服务器然后调 OpenAI 接口跑一个完整的对话。客户端配置的关键是定义 MCP 服务器的连接地址spring.ai.mcp.client.namemcp-test-client # Streamable HTTP 模式 spring.ai.mcp.client.connections.test.server-urlhttp://localhost:8080/mcp然后在代码里使用McpToolCallbackProvider来加载远程服务器的工具注册到ChatClient中Component public class McpClientRunner { private final ChatClient chatClient; public McpClientRunner(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { this.chatClient builder .defaultTools(toolCallbackProvider) .build(); } public String ask(String userMessage) { return chatClient.prompt(userMessage) .call() .content(); } }接着让模型回答北京现在天气怎么样。测试结果里最让人兴奋的一刻就是看到模型先走了一遍选择工具 - 传参 - 返回结果 - 组织语言的完整链路最后给出自然语言回答北京当前天气为晴最高温度 32 度最低温度 22 度。这个过程中模型其实没有实时获取天气的渠道是靠 MCP 调了我注册的工具拿到真实数据后才生成的回答。链路跑通之后这套 MCP 服务器就可以真正接入业务了。6. 踩过的坑和排错思路从握手失败到参数错位任何一个新框架落地坑都是不可避免的。我把这次实践中遇到的典型问题整理出来按排查链路的顺序记录如下希望对大家有实际参考价值。6.1 MCP 版本不匹配导致的握手失败最早期遇到的坑是协议版本不一致。MCP 的协议版本号一直在演进从2024-10-07到2024-11-05再到更新版本。Spring AI 的 Starter 会默认使用它编译时对应的协议版本而如果客户端那边强制指定了一个更旧的版本初始化握手就可能失败现象是initialize请求返回协议版本不支持的错误。排查思路很简单打开客户端日志把发送的protocolVersion和服务器返回的消息对照一下就能确认问题。解决方案要么是升级 Spring AI 版本要么在配置中显式指定协议版本。Spring AI 的McpServerFeatures.Sync和异步版块里都提供了protocolVersion参数但多数情况下你根本不需要手动指定跟着框架默认走就行。6.2 工具参数被模型错误传入MCP 工具参数不像强类型接口那样有严格的运行时校验大模型生成参数时偶尔会出幺蛾子。最典型的是日期格式、数字单位这类问题。比如我有个查询订单的工具要求日期格式yyyy-MM-dd模型可能自作主张传成2025年1月1日这种格式导致后端解析异常。解决这类问题的思路不应该是在工具方法里写大段防御代码而是在ToolParam的描述里把边界说清楚。如果可能的话尽量用枚举或简单类型替代自由字符串入参比如城市名直接用固定枚举模型传错的可能性会大幅降低。另外还有一个实用经验工具方法内部一定要做参数校验校验失败时返回明确的错误消息而不是抛异常——因为 MCP Server 的异常信息会原样传给大模型如果异常信息写得不清楚模型可能直接懵掉回答出一堆胡话。6.3 stdio 模式下发现端口被占用有一次我从 HTTP 模式切换成 stdio 模式测试启动时突然报端口被占用。排查原因后发现原来是 Spring Boot 的 Web 容器还在虽然传输模式切成了 stdio但 Web 自动配置依然尝试监听端口。解决方式有两种一是把spring-boot-starter-web依赖移除二是显式排除 Web 自动配置SpringBootApplication(exclude { WebMvcAutoConfiguration.class })但这里要说清楚如果你同时还有 HTTP 的需求就不要简单排除 Web 配置因为 HTTP 模式依赖 Web 容器。我的习惯是一个项目用 profile 区分模式或者干脆拆成两个应用入口一个跑 stdio、一个跑 HTTP互不干扰。6.4 工具返回结果太大导致上下文膨胀这是最隐蔽的性能坑。MCP 工具可以返回任意大小的内容但返回结果会作为上下文的一部分交给大模型处理。如果工具返回一个上万行的数据库查询结果一次调用可能就把模型上下文窗口撑爆而且 token 成本会非常高。我在设计工具时给自己定了一个原则返回给模型的内容永远是经过加工的摘要而不是原始数据。比如查询日志场景工具的职责不是把 5000 条日志全部返回给模型分析而是返回错误率 3.2%、最近一次报错发生在 14:23、Top 3 错误类型如下让模型基于摘要做判断。这样既控制了上下文大小也提升了回答质量。如果模型确实需要看原始数据可以让它再调用另一个专门的工具去拉明细。7. MCP Server 的进阶玩法从单工具到组合式服务工具写多了之后你会发现MCP Server 的价值远不止把单个 API 暴露给模型。它可以做成一个组合式服务的聚合层把相关的业务能力编排在一起让模型通过多个工具协作完成复杂任务。我做了一个内部知识库检索的 MCP Server核心工具包括三个searchDocuments做全文检索、getDocumentSummary总结指定文档、getRelatedQuestions推荐相关问题。模型在处理某某模块怎么接入这类问题时会先调用检索工具找到相关文档再调用总结工具生成摘要最后可能还会拉一下推荐问题来扩展回答。整个过程对用户来说是黑盒但模型通过工具的组合完成了一个接近人类工作流的任务。这个体验让我觉得 Spring AI 的 MCP Boot Starter 确实是目前 Java 生态里接入 Agent 工具调用最顺滑的方式之一。另外值得尝试的是通过McpResource和McpPromptTemplate暴露资源和提示词模板。资源适合给模型提供只读的上下文数据提示词模板适合沉淀固定场景下的 prompt 结构。三者结合使用MCP Server 能覆盖更大范围的 Agent 需求而不只是简单的工具调用。8. 部署形态与监控生产环境必须考虑的几件事走到生产部署这一步有几个问题是我实际运维中反复确认过的这里一起说清楚。8.1 进程形态与资源配额MCP Server 作为一个 Spring Boot 应用本质上就是个 Java 进程。如果走 HTTP 模式部署方式跟其他微服务完全一样容器化、负载均衡都可以沿用。需要注意的只有一点JVM 内存配额要相对宽裕因为工具执行可能涉及大量反射和序列化操作GC 压力比普通 Web 服务可能更高。我的经验是基础堆内存至少给到 1G工具逻辑复杂的话建议 2G 起步。8.2 日志与调用链追踪MCP 的调用链是用户 - 大模型 - MCP Server - 业务系统。中间任何一环出问题排查都相当的难受。所以我强烈建议在工具方法里打详细的业务日志关键是记录入参、出参、耗时。Spring AI 已经为 MCP 请求提供了 Tracing 的支持配合 Micrometer 可以很轻松地观测到 MCP 调用的吞吐和延迟指标。生产环境至少要做到两个可观测性维度MCP 工具的调用次数、P95 延迟。8.3 安全沙箱与权限隔离MCP Server 暴露的工具本质上是让大模型直接操作业务系统的入口。这比普通 Web API 更危险因为模型生成的工具调用不一定符合预期而且调用链路的异常更难追踪。我的做法是所有关键写操作工具都必须做两层校验第一层是协议层的调用方鉴权第二层是业务级的目标资源归属校验。宁可多写一点校验代码也不要让模型获得一个可以随意执行任意操作的万能接口。还有一个容易被忽略的安全细节工具描述信息本身可能泄露内部结构。模型在看到工具的 JSON Schema 描述后确实可能提取出意想不到的信息。如果你在工具描述里写了太多内部数据库名、表名字样这些信息会原样出现在模型输出中。所以工具描述尽量使用业务化的表达避免暴露底层技术细节。9. 需要留意的几个坑最后一个补丁式提醒这篇文章前面内容已经很长了最后我再集中补一些不起眼但比较重要的小提醒。第一个是工具方法不要定义成private。如果你习惯把Tool方法写成私有方法省事Spring 会直接无视它。这个跟 Spring AOP 的原理有关系但这里不用深究记住结论就行工具方法必须是public。第二个是避免重载方法冲突。如果你在一个类里定义了两个同名但参数不同的Tool方法框架注册工具时会因为名字冲突直接抛异常。工具名在整个 MCP Server 范围内必须唯一这个唯一性默认就是方法名。如果真的需要同名可以通过Tool(name xxx)显式指定不同的名字。第三个是异常处理策略。在 MCP 工具方法里业务异常应该转成正常返回值返回而不是直接抛出去。原因前面提过——异常信息会原样交给大模型而大模型只会拿这句话去组织回答。我把一个用户不存在的异常直接抛了出去结果模型一本正经地回答用户系统发生了一个内部错误用户不存在且无法处理。这个回答看起来既怪诞又真实但实际体验就是工具定义不健壮。正确做法是返回未查询到该用户请确认用户ID这样的自然语言结果。第四个是版本升级的兼容性。Spring AI 版本迭代很快不同版本之间 MCP API 有过调整。比如早期版本用McpServerProperties配置传输模式后来版本改成了spring.ai.mcp.server.transport这种形式。升级的时候一定要先看官方迁移文档不要想当然地以为配置名没变。10. 一次从 0 到 1 的实际构建过程复盘最后复盘一下这次完整的构建经历。从拿到需求到 MCP Server 稳定运行大概用了不到一周的时间。前期最花时间的是理解 MCP 协议本身和 Spring AI 的抽象设计真正写工具反而是最快的部分。我的整体感受是这样的MCP Server Boot Starter 是比较值得纳入技术序列的。它把过去模型调用工具这种比较零散的事情收敛成了一整套框架化的能力而且是标准协议驱动的。对比自己实现一套工具调用体系在这个 Starter 上开发相当于站在一个持续迭代的框架上协议演进、客户端兼容这些问题它基本都帮你扛了。现在团队内部任何一个新业务工具要接入 AI 应用我的流程已经固定下来了写一个Tool方法 - 本地启动 MCP Server - 客户端联调验证 - 部署到中心的 MCP Server 上。整个流程非常标准新人上手也很快。对于 Java 技术栈的团队如果你们正在考虑给 AI 应用接入外部工具能力我建议可以直接从 Spring AI 的 MCP Boot Starter 入手省下来的时间可以用来打磨工具本身的质量这才是真正有价值的部分。
阅读完成 · 觉得有帮助?