资讯详情

Spring AI MCP Server 开发实战:从协议原理到工具调用全流程

📅 2026/9/24 21:06:06 | 华诺云谱 👁 阅读
Spring AI MCP Server 开发实战:从协议原理到工具调用全流程
最近在做 AI 应用集成的时候MCP 这个词几乎绕不开。它全称 Model Context Protocol是一套开放协议核心目的是让 AI 应用用标准化的方式调用外部工具和数据源。而 Spring AI 的 MCP Server 能力正好解决了 Java 生态里接入 MCP 这个“最后一公里”的问题。这篇内容我基于自己的实际工程经验从思路、配置、代码到排错完整拆一遍 Spring AI MCP Server 的开发流程希望能帮你少踩几个坑。1. MCP Server 整体思路拆解它到底解决了什么问题1.1 没有 MCP 之前工具接入是什么样子在 MCP 出现之前想让大模型调用外部系统基本是两条路要么在 Prompt 里硬塞工具描述让模型输出结构化 JSON再自己写解析逻辑去路由要么给每个平台单独封装一套 function calling 的适配层换一个前端、换一个协议就得重来。这样做最头疼的问题不是写代码而是“契约”没法统一——工具怎么描述、参数怎么传、错误怎么返回每家都有自己的写法。MCP 把这件事抽象成了三个角色Host 是 AI 应用本身比如桌面客户端、IDE、后端服务Client 负责和 Server 通信Server 负责把能力暴露出来。协议层面的原语也固定下来最常用的就是 tools工具、resources资源、prompts提示词模板。AI 客户端启动后通过 tools/list 拉取能力清单用户表达意图后客户端通过 tools/call 调用对应能力。1.2 Spring AI 在这里扮演了什么角色Spring AI 是 Spring 生态里的 AI 集成框架它对 MCP 做了比较彻底的支持既可以把下游能力封装成 MCP Server 对外提供也可以作为 Client 去连接别人家的 Server。对我们 Java 开发者来说最大的好处是不用自己维护底层传输协议和 JSON-RPC 细节只要关注业务方法怎么写。我之所以在项目里选 Spring AI 而不是直接用官方 MCP Java SDK核心原因有三个第一Spring AI 对 Tool 注解的封装非常顺手一个方法加个注解就能变成一个可被 AI 调用的工具第二默认集成了工具描述、参数 Schema 生成这类干活容易忽略的细节第三和 Spring Boot 的配置体系天然打通后续接模型、接数据库、接各种中间件都不用再粘一层代码。1.3 一条完整的调用链路长什么样拿我最近做的一个订单查询助手举例用户在小程序里问“最近三天有多少笔待发货订单”请求先到 Spring AI 应用模型判断需要查询订单系统于是走 MCP Client 发起 tools/call请求通过 SSE 通道到达 MCP ServerServer 定位到对应的 Tool 方法调用真实业务接口查询数据库把结果返回给模型模型组织成自然语言回给用户。这条链路里MCP Server 是要提前启动并注册的一个独立服务。理解好这个架构后面写代码的时候就不容易绕晕。2. 工程初始化与依赖配置先搭出能跑的最小骨架2.1 项目骨架与 Spring Boot 版本选择先用 Spring Initializr 创建一个普通 Spring Boot 项目。版本选型上建议用 Spring Boot 3.4.x 或 3.5.x对应的 Spring AI 版本选择 1.0.0 之后的稳定版。早期我用过 0.8.x那时候 MCP 还处于快速迭代期接口变化很大升级成本高现在到了 1.0 之后API 稳定多了可以放心用。注意一个细节Spring AI 的 MCP Server 有两种 IO 模型一种是基于 WebMVC 的同步实现一种是基于 WebFlux 的响应式实现。如果项目里没有特殊要求优先选 webmvc 版本排查问题、打印日志都更直观。只有当你确定要面对高并发流式场景再考虑 webflux。2.2 Maven 依赖配置与版本号避坑依赖的坑主要体现在版本对应关系上。Spring AI 1.0.0 对应 MCP Java SDK 1.0.0如果你混用 Spring AI 0.9.x 和 MCP SDK 1.0.x大概率会在启动时遇到 NoSuchMethodError 这类问题。这里给出一组我实测可用的依赖配置parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.4.5/version relativePath/ /parent dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MCP Server 同步实现 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency /dependencies有个细节需要特别提醒Spring AI 的 BOM 主要是管 Spring AI 自己模块的版本但 MCP SDK 的传递依赖版本不一定总能对齐如果本地启动时报了 mcp 包内部类的编译错误优先检查 Maven 依赖树看io.modelcontextprotocol.sdk:mcp实际被解析到哪个版本。另外如果你需要把国内的智谱 AI、通义千问等模型接入 Spring AI 客户端又不确定具体依赖坐标可以在 search.maven.org 上直接搜spring-ai-alibaba或智谱对应的 starter一般会明确标注支持的最低 Spring AI 版本。这里提个经验MCP Server 本身和模型是哪家的没有关系它只负责通过标准协议暴露工具模型接入属于另一个模块。2.3 配置文件里需要预留的关键参数application.yml 中 MCP Server 的配置比较少核心是服务标识server: port: 8081 spring: application: name: mcp-order-server ai: mcp: server: name: order-tool-server version: 1.0.0这个 name 和 version 会体现在 MCP 初始化握手的 serverInfo 里客户端拿到后可以用来做版本判断。除此之外如果你的工具方法要调用外部 API注意预留好超时和连接池配置不然工具本身被模型调用的时候一个慢接口能把整条链路拖死。3. 核心代码实现把一个 Spring Bean 变成 AI 可调用的工具3.1 注册 MCP Server 端点当你在 pom 里引入了spring-ai-starter-mcp-server-webmvc之后Spring Boot 会自动配置一个 SSE 端点默认路径是/sse新版规范里客户端会先 POST 到这个地址做握手再通过返回的 event stream 建立长连接。如果想自定义路径可以通过配置项调整spring: ai: mcp: server: endpoint: /mcp/sse这时客户端连接地址就变成了http://localhost:8081/mcp/sse。我不太建议随意改路径除非你有统一的网关前缀控制需求。保持默认能减少联调时的认知成本。3.2 用 Tool 注解暴露业务能力MCP Server 的核心代码非常简单就是你平时写的 Service 方法加个注解就行。拿订单查询工具举例Component public class OrderToolService { private final OrderService orderService; public OrderToolService(OrderService orderService) { this.orderService orderService; } Tool(description 根据订单状态查询订单列表status 参数PENDING-待支付PAID-已支付SHIPPED-已发货) public ListOrderInfo queryOrders(ToolParam(description 订单状态) String status) { return orderService.listByStatus(status); } Tool(description 根据订单号查询订单详情) public OrderInfo getOrderDetail(ToolParam(description 订单编号) String orderId) { return orderService.getByOrderId(orderId); } }这里最考验功底的是 description 的写法。模型不像人一样能读代码它只能靠 description 判断一个工具在什么场景下可用如果描述太模糊比如只写“查询订单”模型根本不知道这个方法是按状态查还是按单号查调用就会频繁失误。最好把参数的取值范围也写进去像上面“PENDING-待支付”这种写法实测能让模型准确率高不少。3.3 工具的自动发现与注册原理Spring AI 在启动的时候会扫描容器里所有带有 Tool 注解的方法为每个方法生成一个 ToolCallback然后组装成 tools/list 接口的返回数据。这里有一个隐性问题被扫描的 Bean 必须能进入 Spring 容器也就是你放 Tool 方法的类要么被 Component 标注要么能被组件扫描覆盖到。一个常见的坑是新手把工具类放在启动类子包之外导致扫描不到启动日志里 MCP server 正常起来了但客户端 tools/list 里永远只有空数组。排查的方法很简单启动时看日志里有没有这样一行类似Registered tool callbacks: [queryOrders, getOrderDetail]没有就说明扫描有问题。3.4 参数类型与 JSON Schema 的映射MCP 协议里工具参数是以 JSON Schema 形式暴露的Spring AI 在生成 Schema 时会依赖 Jackson 的序列化规则。这就意味着你的参数和返回类型最好满足两个原则参数尽量用 String、Integer、Boolean 这些简单类型返回对象尽量用扁平结构的 DTO。复杂的嵌套对象不是不能用而是容易出问题。比如返回一个带泛型的 Result Jackson 在生成工具描述时可能把 T 解析成 Object模型就无法理解里面该包含什么字段调用出来的结果自然不对。踩过这个坑之后我的原则是工具方法返回外部类型一律先转成格式固定的字符串或简单 DTO反正最终模型需要的也是可读文本不追求消灭一切嵌套但要让结构尽量简单。如果是并发量比较大的场景还可以给工具方法做缓存。MCP 调用粒度比普通 HTTP 接口粗一次调用背后可能是一个复杂报表的查询影响面大值得在方法内做本地缓存或者 Redis 缓存。4. 客户端调用与全链路联调确保工具真的能被模型用起来4.1 用 MCP Inspector 快速验证服务端服务端代码写完之后第一件事不是去接模型而是先用官方提供的调试工具验证 MCP Server 是否符合协议规范。MCP Inspector 的启动方式是npx modelcontextprotocol/inspector启动后打开浏览器工具界面在 Server 配置里填写 transport type 为 SSEURL 填http://localhost:8081/mcp/sse连接成功后Inspector 会自动发起 initialize 和 tools/list 请求你就能直观看到服务端暴露了哪些工具以及每个工具的 JSON Schema 长什么样。这一步对排查问题非常有用。如果你在界面里看到了 queryOrders、getOrderDetail 这些工具说明服务端注册没问题接下来就可以放心接模型了。如果看不到工具优先查日志而不是去改客户端代码。4.2 在 Spring AI 客户端中调用远程 MCP Server如果你的 AI 应用本身也是 Spring Boot 项目接入 MCP Server 会方便很多。在客户端项目里引入依赖dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client-webmvc/artifactId /dependency然后在配置文件里指定要连接的 MCP Serverspring: ai: mcp: client: connections: order-server: url: http://localhost:8081/mcp/sseSpring AI 启动时就会自动连接远程 MCP Server把对端工具拉取到本地。接下来要做的是把这些工具合并到 ChatClient 的 ToolCallback 列表里Service public class ChatService { private final ChatClient chatClient; public ChatService(ToolCallbacksProvider provider, ChatClient.Builder builder) { // 从 MCP 客户端拉取到的工具 ListToolCallback mcpTools provider.getToolCallbacks(); this.chatClient builder .defaultTools(mcpTools.toArray(new ToolCallback[0])) .build(); } }这里有个容易忽略的点如果同时配置了本地 Tool 方法和远程 MCP 工具要注意工具名冲突的问题。同名工具会后者覆盖前者而且不会报错排查起来很隐蔽。我的习惯是在命名上做约定远程工具统一用模块名做前缀。4.3 工具描述对模型决策的影响联调的时候你会慢慢发现模型调不调用某个工具很大程度上取决于你写 description 的方式。同样是查询天气如果你写“查询天气”模型可能不知道怎么传城市名如果你写“根据城市名称查询实时天气和未来三天预报城市名称支持中文模糊匹配”模型的判断就准确多了。我建议在一个工具类完成后专门花十分钟把 description 过一遍逐字检查有没有歧义。这个投入产出比非常高因为在真正跑的环节模型一旦反复调用错工具浪费的不只是时间还有 Token 成本。5. 常见问题与排查技巧实录5.1 工具列表为空的三种可能客户端连接成功但 tools/list 返回空是我遇到最多的问题主要分三类一是服务端类没有被组件扫描到解决方法是检查包路径二是 Spring AI 版本和 MCP SDK 版本不对齐导致工具注册过程抛异常被吞掉升级到匹配版本就行三是项目里存在多个 MCP Server 依赖自动配置被覆盖。实际排查时先看服务端启动日志有没有注册工具回调的提示再决定往哪边找。5.2 SSE 断连与超时控制新版 MCP 的 Streamable HTTP 传输客户端和服务端之间是长连接受网络环境的影响很明显。如果你部署在公网环境建议在网关层给 SSE 接口关掉缓冲并在 Nginx 配置里调长 proxy_read_timeout否则默认的 60 秒可能不够模型长时间思考后再发起调用。另外应用容器这边也要注意Tomcat 的异步请求超时时间如果设置得太短连接会被容器主动断开。我一般把server.tomcat.async-timeout设置为 300000 毫秒测试阶段足够用了。症状可能原因处理方式客户端连接 404SSE 端点路径不对确认服务端 spring.ai.mcp.server.endpoint 配置初始化握手失败MCP 协议版本不匹配让 Spring AI 和 MCP SDK 版本保持同一版本线tools/list 返回空工具类未被扫描注册检查组件扫描范围与启动日志工具调用超时服务端方法执行超过容器超时调大 async-timeout优化工具方法耗时中文参数乱码客户端没有正确编码确认 HTTP 请求头包含 UTF-8 编码5.3 日志定位的小技巧排查 MCP 问题时强烈建议把 Spring AI 的日志级别打开logging: level: org.springframework.ai.mcp: DEBUG org.springframework.ai.tool: DEBUG打开之后你能在日志里看到完整的 tools/list 请求、tools/call 请求以及参数的一个真实结构。我之前排查一个“工具方法始终被调用但参数永远是默认值”的问题就是靠 DEBUG 日志发现模型传进来的 JSON 字段名和 Java 方法参数名不一致导致的这种问题在了解协议层数据之前很难定位。5.4 生产环境部署要额外注意的事MCP Server 作为一个独立服务部署时要注意鉴权。因为是长连接通道如果直接裸奔公网任何人都能调用你的工具方法。目前 Spring AI 自带的能力比较有限通常做法是在前面加一层网关或者利用 Spring Security 对 SSE 端点做认证配置。同时工具方法内部一定要做权限校验因为调用方是 AI不是指定用户上下文里可能没有身份信息。我的做法是在工具方法第一个参数里注入用户 ID并忽略返回值中不该暴露的敏感字段。最后再分享一个小经验工具方法的设计和普通 Controller 非常不一样。写 Controller 时接口参数由前端决定而 MCP 工具的参数由模型理解后生成所以每个参数的描述都要经得住“一个不太聪明的人”来读。我在实际项目中每次写完一个工具都会先打开 Inspector自己手动调用一遍检查返回格式是不是稳定的、可读的。MCP 的生态还在快速演进工具注册、传输层实现这些细节未来可能会变但把工具描述清楚、把边界考虑清楚这个核心原则什么时候都不过时。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。