☰
SpringAI实现MCP服务端:协议原理、工具开发与生产避坑指南
2026/10/18 20:52:46 网站建设 项目流程

最近在梳理 AI 应用怎么把内部服务暴露给大模型时,绕不开一个词:MCP。尤其在 SpringAI 里写 MCP 服务端,已经不像早期那样要自己拼 JSON-RPC 协议了——一个注解、一个配置类,就能把普通 Java 方法变成大模型可以直接调用的工具。这篇文章我打算把这套东西从头到尾捋一遍,从协议原理讲到实际能跑通的代码,再把你大概率会踩的坑提前摆出来。

如果你是刚接触 SpringAI 的开发者,或者正在评估要不要把公司内部能力改造成 MCP Server,这篇文章应该能帮你省掉不少试错时间。我尽量不讲废话,直接说重点。

1. MCP 服务端到底解决什么问题

1.1 AI 应用接入外部能力的老大难

大模型本身是“静态知识库”,训练完之后不会再变,也无法主动查数据库、调接口、操作业务系统。所以稍微正经一点的 AI 应用,都会走一条路:让模型在回答问题时能调用外部工具,也就是 Function Calling。这本身不是新东西,但过去每个平台各搞一套,模型厂商定义一套工具调用协议,企业内部又是另一套封装,互相之间完全不通。

于是出现一种很尴尬的场面:你在 A 平台写好的工具调用逻辑,换到 B 平台全部重写;连接企业内部接口时,工具描述、参数结构、调用返回格式都要单独适配一套。MCP 就是想把这件事标准化——它把模型要用的“工具”“数据资源”“提示词”统一成一种服务端能力,客户端只需要按同一套协议去发现和调用。说得直白点,MCP Server 就是“模型的 USB 接口”,服务端定义好插头规格,谁都能插上来用。

1.2 MCP 服务端在整个架构里的角色

MCP 是个客户端-服务端模型。客户端通常是大模型应用,比如智能助手、IDE 插件、自动化机器人;服务端则是负责暴露能力的那一方。站在 SpringAI 写 MCP 服务端时,我们主要实现三个角色能力。

第一是 Tools,这是最常用的,相当于给模型提供可以执行的函数。第二是 Resources,向模型暴露数据文件、配置片段、数据库查询结果等只读信息。第三是 Prompts,提供可复用的提示词模板。Spring AI 目前最成熟、文档最全的就是 Tools 这一块,实际项目里用得也最多。

从协议层面看,客户端和服务端之间通过 JSON-RPC 2.0 消息通信,核心交互包括 initialize 初始化握手、tools/list 获取工具列表、tools/call 调用工具。服务端的责任很清晰:告诉客户端“我有哪些工具”,收到调用请求后真正执行业务逻辑,再把结果按统一格式返回。

1.3 SpringAI 天生的优势

Spring AI 是 Spring 生态官方推出的 AI 开发框架,强调“用 Spring 的方式写 AI 应用”。在 MCP 支持上,它没有像很多项目那样只提供一个薄薄的封装,而是直接提供了 spring-ai-starter-mcp-server 这类起步依赖,把协议处理和传输层都内置了。

最直观的体验就是:你不需要自己实现协议编解码,不需要管理 JSON-RPC 会话,只需要把普通 Java 方法暴露出去。Spring AI 会扫描@Tool注解,把方法元信息自动翻译成 MCP 工具定义,并通过配置好的 Transport 把服务端暴露出去。这种“零协议代码”的体验,对 Java 团队非常友好,因为团队里不是每个人都需要深挖 MCP 协议细节。

2. MCP 服务端的核心机制:从握手到工具调用

2.1 协议消息:客户端和服务端在聊什么

如果你没用过 MCP,可能会觉得“暴露一个 HTTP 接口让模型调用就行了”,但 MCP 不是简单 REST API,它是一套会话协议,所有消息都是 JSON-RPC 2.0 格式。整个生命周期大概是这样的。

客户端先发initialize请求,包含协议版本、客户端能力、客户端标识。服务端返回自己支持的协议版本、服务端能力和标识,这一步是做能力协商。随后客户端发送notifications/initialized通知,表示初始化完成。再往后,客户端会发tools/list获取工具清单,用户触发调用时发tools/call并带上工具名和参数。如果工具执行时间较长,MCP 还支持进度通知,当然实际开发中很少用到,但协议层面是有的。

理解这套流程对排查问题特别重要。比如你写好了服务端,客户端却告诉你“没有可用工具”,很多情况下不是代码逻辑错了,而是tools/list返回的 schema 有问题,或者初始化阶段协议版本协商不一致。网上很多人排查半天,最后发现是 Spring AI 版本和客户端库版本支持的 MCP 协议版本对不上。

2.2 传输方式选型:stdio、SSE、Streamable HTTP

MCP 在传输层有几种实现方式,Spring AI 里都能用,但适用场景差异很大。

传输方式工作原理适用场景Spring AI 支持情况
stdio通过标准输入输出流与父子进程通信本地工具、单机脚本、IDE 插件支持,基于spring-ai-starter-mcp-server-stdio
SSE服务端通过 Server-Sent Events 单向推送,客户端通过 HTTP POST 发请求远程服务、传统浏览器场景支持,基于 WebMVC/WebFlux 的 SSE endpoint
Streamable HTTP基于 HTTP 长连接,支持双向流式消息,同时保留 POST 透传生产环境远程服务,是目前主流推荐支持,基于 WebMVC/WebFlux 的/mcpendpoint

我个人的建议是:如果是微服务场景,多客户端远程访问,优先走 Streamable HTTP;如果只是本地调试或做桌面端工具,stdio 反而最省事。Spring AI 之所以同时支持多种 Transport,就是为了适配不同部署环境。之前有个朋友一开始用 stdio 写了个远程服务部署到服务器上,客户端根本连不上,因为 stdio 依赖进程间管道,跨网络完全行不通。

2.3 工具是如何被“翻译”成协议 schema 的

你有没有想过一个问题:客户端怎么知道你的 Java 方法有哪些参数、参数是什么类型、应该怎么传?答案是服务端把方法元数据翻译成了 JSON Schema。

Spring AI 里,这个翻译过程是通过ToolCallback完成的。当你用@Tool注解一个方法,Spring AI 解析方法名、参数名、参数类型和描述,生成对应的工具定义。方法名成为工具名,方法描述成为工具描述,参数注解@ToolParam提供参数描述。然后ToolCallbackProvider把这些定义打包,交给底层 MCP Server 在收到tools/list请求时返回。

这段逻辑非常关键,因为模型本身不具备理解代码的能力,它只依赖工具定义里的描述来决定“要不要调用这个工具”“参数该怎么传”。描述写得模糊,模型就会乱猜,甚至直接给出错误结果。这一点后面实操部分我会再强调。

3. 实操:用 SpringAI 手写一个可上线的 MCP 服务端

3.1 工程准备与依赖引入

我以 Spring Boot 3.4 + Spring AI 1.0 为例。项目名称你可以随意,结构上用标准 Maven 工程即可。引入依赖时最好用 BOM 统一管理版本,避免出现 Spring AI 各包版本不一致导致协议初始化失败。

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

然后是 MCP Server 的起步依赖。我建议按你的 Web 栈选择:

<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency>

如果你用 WebFlux,则换成spring-ai-starter-mcp-server-webflux。如果要做 stdio 模式,用spring-ai-starter-mcp-server-stdio。这里有个很容易犯的错:把多个 starter 同时引入,结果是应用启动时加载了多个 Transport 相关配置,行为变得不可预期。

3.2 定义一个工具类:用 @Tool 暴露能力

接下来写工具类。假设我们在做一个数据脱敏服务,希望大模型在处理用户信息时能调用我们的脱敏函数。这很适合演示,因为脱敏本身不依赖复杂 I/O,但又有真实业务价值。

@Component public class MaskService { @Tool(description = "对手机号进行脱敏处理,保留前3位和后4位,中间用星号代替") public String maskPhone( @ToolParam(description = "需要脱敏处理的完整手机号") String phone) { if (phone == null || phone.length() < 7) { return phone; } return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4); } @Tool(description = "对身份证号进行脱敏处理,保留前6位和后4位") public String maskIdCard( @ToolParam(description = "需要脱敏处理的完整身份证号") String idCard) { if (idCard == null || idCard.length() < 10) { return idCard; } return idCard.substring(0, 6) + "********" + idCard.substring(idCard.length() - 4); } }

这里要注意几个细节。类上加@Component是为了让 Spring 管理 Bean,这样才可能被扫描到。方法签名尽量简单,参数不要用复杂对象,因为 JSON Schema 生成对复杂嵌套类型的支持会带来额外的理解成本。@ToolParam的 description 一定要写,模型非常依赖这段描述来决定参数怎么填。

3.3 注册工具:让 SpringAI 知道要暴露什么

定义完工具类还不够,Spring AI 不会自动扫描所有@Tool方法,你需要通过ToolCallbackProvider告诉它哪些 Bean 里的哪些方法要暴露出去。最简单的方式是写一个配置类。

@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider registerMaskTools(MaskService maskService) { return MethodToolCallbackProvider.builder() .toolObjects(maskService) .build(); } }

这段代码做了两件事:构建了一个工具回调提供器,并把MaskService里面的@Tool方法注册为 MCP 工具。Spring AI 自动配置会在容器里找ToolCallbackProvider类型的 Bean,如果找到了,就把它们整合进 MCP Server。

如果你有多个工具类,可以这样写:

@Bean public ToolCallbackProvider registerTools(MaskService maskService, AnotherToolService another) { return MethodToolCallbackProvider.builder() .toolObjects(maskService, another) .build(); }

不需要为每个工具单独建 Provider,一个 Provider 可以装很多工具对象。

3.4 配置传输方式与端点

然后需要在 application.yml 里配置 MCP Server。以 Streamable HTTP 为例:

spring: application: name: mask-mcp-server ai: mcp: server: name: mask-server version: 1.0.0 enabled: true transport: streamable-http streamable-http: endpoint: /mcp

name和version是服务端标识,客户端握手时会拿到。transport决定用哪种传输方式。endpoint是客户端连接的路径,默认是/mcp,你可以改成别的,但客户端配置必须同步。

如果是 SSE 模式,配置大致是:

spring: ai: mcp: server: name: mask-server version: 1.0.0 transport: sse sse: endpoint: /mcp/sse

还有一种情况是服务端想显式控制 MCP Server 本身的一些行为。Spring AI 里支持通过自定义McpServerFeatures或更底层的McpAsyncServer来做。不过对大多数业务场景,用ToolCallbackProvider+ yml 配置足够了,底层的接口主要是给需要深度定制的场景用的。

3.5 启动、验证与常规检查

启动 Spring Boot 应用后,留意日志。Spring AI 相关 starter 会打印启动信息,你至少应该能看到类似Registered MCP Server或 Transport 的初始化日志。如果 Spring AI 没有打印任何 MCP 相关日志,大概率是配置没生效或者 starter 没引入成功。

验证端点是否可达,最简单的方式是用一个 MCP 调试客户端连上来。这类工具会先做 initialize,再发 tools/list,然后把候选工具列出来。当你在客户端里看到maskPhone、maskIdCard两个工具,说明服务端已经正常工作了。

如果暂时没有调试客户端,也可以先用 curl 探测 HTTP 端点是否能响应。注意 Streamable HTTP 不是一次 POST 就完成全部握手,所以 curl 结果只能证明端点可达,不能证明协议完全正常。最后验证还是要靠真实客户端。

curl -X POST http://localhost:8080/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}"

服务到这一步已经通了。但真正上生产环境,还有很多隐藏问题值得提前了解。

4. 生产环境必须盯住的几个坑:问题排查实录

4.1 客户端连不上,请求 404

这类问题最常见的原因就一个:路径不对。在 Spring AI 里,Streamable HTTP 默认端点可能是/mcp,如果你在 yml 里改了endpoint,客户端那边的 URL 也必须同步改,很多人只改了服务端,客户端还在旧地址上,自然 404。

另外还要检查 transport 类型是否一致。服务端配了streamable-http,客户端配了 SSE 或者 stdio,握手流程完全对不上,表现也是连不上。MCP 客户端一般会让你填“Transport Type”和“URL”,这两项必须和服务端配置保持完全一致。

我遇到过一次更隐蔽的问题:同一个应用里既引入了 webmvc 相关依赖又混入了 spring-ai-starter-mcp-server-stdio,结果 Spring 容器里同时存在多种传输配置,导致服务端启动时选择了错误的 Transport。排查一半天,最后清理依赖重启才恢复。

4.2 工具列表为空,或者工具数量不对

工具列表为空有几种典型原因。第一种是ToolCallbackProvider没有注册成 Bean,Spring AI 完全感知不到。第二种是工具类没有被 Spring 管理,即类上没有@Component或没有通过其他方式注册,导致注入不到容器里。第三种是@Tool注解写在了实现方法上但方法所在类不是 Spring Bean。

还有一种情况是注解写对了、类也注册了,但工具定义被覆盖了。比如你定义了两个ToolCallbackProviderBean,Spring AI 的自动配置在处理时可能只取其中一个,取决于装配顺序。这属于配置混乱,用上面的“一个 Provider 挂多个工具对象”的方式最稳。

如果你看到工具数量对,但某个工具调用时报错“tool not found”,那基本可以断定是服务端注册流程出了问题,而不是调用方的问题。用日志工具打开 MCP 协议包级别的日志,能直接看到客户端发过来的tools/call请求内容和服务端返回,排查效率会高很多。

4.3 模型总是理解不了参数,调用时瞎填

这个问题在普通 REST API 开发中不太会出现,但在 MCP 里特别明显。模型的“理解”完全基于你给的 description。你的参数名叫param1,description 又没写,模型只能靠猜,最后传个 null 或者字符串拼接都很正常。

所以给工具方法起名、写参数说明,一定要像写提示词一样认真。比如这个方法:

@Tool(description = "根据城市名称和日期查询天气情况") public String getWeather( @ToolParam(description = "城市中文名,例如:北京") String city, @ToolParam(description = "日期,格式为 yyyy-MM-dd") String date) { // ... }

每个参数的取值范围、格式、默认值都写清楚。如果参数允许为空,在 description 里注明。这些看起来是“描述文字”,其实直接影响模型行为。

另外,工具方法的返回类型也建议尽量简单。返回一个字符串、数字或者扁平的 JSON 对象,模型的后续处理最容易。如果你返回一个三层嵌套的 Java 对象,序列化之后是复杂 JSON,很多模型在理解时会出现信息丢失。实际项目里我习惯在工具方法内部先组装好最终的文本结果或扁平 Map。

4.4 调用超时、任务堆积

MCP Server 本质上是给大模型提供“远程执行入口”。如果某个工具方法里做了耗时很长的数据库查询或调用外部接口,客户端那边可能已经超时了,但服务端还在慢慢执行。你把超时调大能缓解一部分,但更合理的做法是给耗时任务做异步化,方法内部直接返回“任务已提交”,再把真实结果存库或通过其他渠道返回。

另一个与超时相关的问题是:默认情况下 Spring AI 的 MCP Server 处理请求是异步的,底层使用响应式或异步执行。如果你的工具方法内部用了阻塞式 IO,比如 JDBC 同步查询,高并发下很容易耗尽线程池。生产环境我建议给耗时操作单独配置一个线程池,不要让所有工具调用共享一个池子。

4.5 鉴权与安全

这块最容易被人忽略,但恰恰是真实的坑。MCP 端点一旦暴露到公网,任何能连到端点的客户端都可以调用你的工具。如果脱敏工具还好,但如果是“删除订单”“修改用户余额”这类危险工具,后果就很严重。

我建议至少在三个层面处理:第一,网络层隔离,MCP 端点只对内网或特定服务开放;第二,应用层鉴权,参考 Spring Security 的做法,在请求进入 MCP handler 前做身份校验;第三,工具方法内部做二次校验,尤其是涉及写操作的工具,不能只依赖协议层的信任。

Spring AI 自带 MCP Server 的鉴权能力不像业务开发那么完整,它更多是“让你专注于工具实现”。所以我的建议是把鉴权放在框架层面统一处理,而不是每个工具方法里自己判断。当然,如果你只是内部小范围使用,内网隔离可以暂时够了,但心里要清楚这个风险。

4.6 日志与可观测性

MCP 协议请求是结构化的 JSON-RPC,没有日志辅助,排查起来会非常痛苦。我自己通常在配置文件里把协议包日志打开:

logging: level: io.modelcontextprotocol: DEBUG

这样能看到 protocol 层的请求和响应。如果是 Spring AI 自身的装配问题,打开org.springframework.ai的 DEBUG 日志也能看到 MCP 相关 Bean 的注册过程。生产环境建议把这些日志输出到独立文件,避免和业务日志混在一起。

我还建议给工具方法埋点,至少记录调用时间、参数摘要、返回状态。毕竟 MCP Server 一旦被接入智能助手,很多调用是由模型发起的,不是人来发起的,你根本不知道哪个用户触发了哪个工具。日志就是唯一的可观测手段。

5. 一点个人体会

MCP 服务端看起来是“把一个 Java 方法暴露给 AI 调用”,但真正落地的时候,难点往往不在 SpringAI 的 API,而在你对协议、对模型行为、对客户端连接方式的理解。我写完第一个 MCP Server 之后最大的感受是:服务端代码本身就几十行,真正花时间的是调通客户端、调好工具描述、处理各种边缘情况。

如果你正准备上手,我建议先不要急着把公司所有服务都包成 MCP,而是挑一两个无副作用、边界清晰的工具实践一遍,比如查询天气、脱敏、号码归属地这类只读工具。跑通之后,再逐步扩展更复杂的场景。

最后再分享一个小技巧:调试 MCP 服务端时,不要只依赖大模型应用那边是否“回答正确”,要直接看 tools/call 请求和响应原文。因为很多时候模型根本没按你预期去调用工具,而是自己在编结果。日志里一旦发现工具调用频率异常低,先检查工具描述是否清晰、参数是否容易理解——大概率不是模型的问题,而是你的工具长得不像一个“好用”的工具。

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

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

立即咨询