news 2026/10/2 12:07:07

Spring AI 实现 MCP-Server:从零搭建可复用的工具服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring AI 实现 MCP-Server:从零搭建可复用的工具服务

1. 为什么 Java 后端需要自己写一个 MCP-Server

MCP-Server 是什么?一句话解释:它把你的业务能力(查数据库、调内部接口、算指标)包装成 AI 客户端能直接调用的标准工具。适合谁?适合手里已经有一堆 Spring Boot 服务、想让 Claude Code、Cursor、Cline 这类客户端直接复用这些能力的 Java 后端。

我试过把公司内部的订单查询、库存校验、日志检索三个接口做成 MCP 工具,客户端侧只改一个 JSON 配置就能用,比给每个 AI 客户端单独写插件省事得多。核心原因在于 MCP(Model Context Protocol)把「工具描述 + 参数结构 + 调用结果」抽象成了统一协议,服务端只管注册工具,客户端只管发现和调用。

Spring AI 从 1.0.0-M6 开始提供了spring-ai-mcp-server-webmvc-spring-boot-starter,让你用注解就能把普通 Bean 方法暴露成 MCP 工具。整个链路是这样的:客户端通过 SSE 长连接拿到工具列表,模型决定调用哪个工具后,客户端把 JSON-RPC 请求 POST 回服务端,服务端执行方法并返回结果。

这篇要解决三个具体问题:依赖怎么配不踩版本坑、工具怎么注册才能被正确发现、客户端怎么验证工具列表和返回结果符合预期。全程用可复制的 pom、yml、Java 代码,最后给一次真实的调用验证。

需要说明的是,MCP-Server 本身只负责「暴露工具」,它不绑定任何模型。如果你希望客户端侧统一走一个 Key 和 API 通道来调用模型,可以用 TaoToken 的 API 通道(https://taotoken.net/api),把模型调用和工具调用分开管理,服务端专注做工具,客户端专注做编排。

2. 前置准备:Spring AI 版本、依赖与 TaoToken 通道

2.1 版本矩阵先定死

Spring AI 的 MCP starter 在里程碑版本里改过 artifactId,这是最容易踩的坑。M6 之前叫spring-ai-mcp-server-spring-boot-starter,M6 之后拆成了 webmvc 和 webflux 两个版本。如果你照着老博客抄依赖,大概率会报ClassNotFoundException。

我实测下来稳定可用的组合是:Java 17、Spring Boot 3.4.3、Spring AI 1.0.0-M6。JDK 必须 17 起步,Spring Boot 3.x 不支持 8 和 11。

<properties> <java.version>17</java.version> <spring-ai.version>1.0.0-M6</spring-ai.version> <spring-boot.version>3.4.3</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.36</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

注意spring-ai-bom必须用importscope,否则各 starter 版本会各拉各的,出现NoSuchMethodError。另外 Spring AI 的里程碑版本不在 Maven 中央仓库,需要在settings.xml或 pom 里加 Spring Milestone 仓库:

<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> <snapshots><enabled>false</enabled></snapshots> </repository> </repositories>

2.2 TaoToken 通道在这里的角色

MCP-Server 只暴露工具,不负责模型推理。真正跑起来时,客户端(Cursor、Cline、Claude Code)需要一边调模型、一边调你的工具。模型这一侧如果每个客户端都单独配 Key,管理会很乱。

TaoToken 提供统一 Key 和 API 通道,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。你可以把它理解成「模型调用的统一出口」,而你的 MCP-Server 是「工具调用的统一出口」,两者职责分离,互不干扰。

如果你用的是 Claude Code 这类编码客户端,可以在客户端侧配置 Base URL 指向 TaoToken 的 API 通道,Key 用 TaoToken 控制台生成的 Key,Model ID 按客户端要求填。这样模型调用走统一通道,工具调用走你自己的 MCP-Server,排查问题时边界清晰。

2.3 传输方式选 SSE 还是 STDIO

MCP 支持两种传输:STDIO 和 SSE。STDIO 是客户端把服务端当子进程启动,通过标准输入输出通信,适合本地单机;SSE 是服务端独立跑在 HTTP 端口上,客户端通过 Server-Sent Events 订阅消息,适合内网多客户端共享。

面向「本地或内网暴露可被 AI 客户端调用的工具服务」这个场景,选 SSE 更合适:服务端一次部署,多个客户端都能连,还能挂到 K8s 上做滚动更新。下面的配置都按 SSE 来。

3. 可复制配置:application.yml 与工具注册

3.1 application.yml 完整配置

spring: application: name: mcp-server-weather server: port: 8080 ai: mcp: server: enabled: true type: ASYNC sse-message-endpoint: /mcp/messages stdio: enabled: false sse: enabled: true

几个参数的含义要讲清楚,不然改错了很难查:

type: ASYNC表示工具执行走异步线程池,避免阻塞 SSE 连接。如果你的工具是纯内存计算,用 SYNC 也行,但涉及 HTTP 调用建议 ASYNC。

sse-message-endpoint是客户端 POST 消息的路径,默认是/mcp/messages。客户端先 GET/sse建立事件流,拿到 sessionId 后,所有 JSON-RPC 请求都 POST 到这个 endpoint。

stdio.enabled: false和sse.enabled: true必须成对出现,两个都开会导致启动时报传输冲突。

3.2 用 @Tool 注解注册工具

Spring AI 的工具注册靠@Tool注解加ToolCallbackProviderBean。先写工具类:

@Component public class WeatherService { @Tool(description = "根据城市名称获取天气预报,返回该城市的天气状况") public String getWeatherByCity(String city) { Map<String, String> mockData = Map.of( "西安", "晴天,气温 18-26 度", "北京", "小雨,气温 12-20 度", "上海", "大雨,气温 15-22 度" ); return mockData.getOrDefault(city, "抱歉:未查询到该城市的天气数据"); } }

description非常关键,模型就是靠这句话决定要不要调用这个工具。写得太模糊(比如「获取天气」)模型可能不调,写清楚「根据城市名称获取天气预报」命中率明显更高。

然后注册成 Provider:

@Component public class WeatherToolConfig { @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }

MethodToolCallbackProvider会扫描toolObjects里所有带@Tool的方法,自动生成 JSON Schema。方法参数名会成为 schema 里的属性名,所以参数名不要用arg0这种,编译时记得加-parameters:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <parameters>true</parameters> </configuration> </plugin>

不加这个参数,客户端看到的工具参数名会变成arg0,模型填参时容易出错。

3.3 客户端侧配置片段

服务端起在http://localhost:8080后,客户端配置长这样(以 Cursor 的mcp.json为例):

{ "mcpServers": { "weather": { "url": "http://localhost:8080/sse", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } }

如果你在客户端里同时要配模型通道,把模型侧的 Base URL 指向 TaoToken 的 API 通道,Key 用控制台生成的 Key,Model ID 按客户端要求填。工具侧保持指向你自己的 MCP-Server,两边不要混。

4. 验证请求:确认工具列表与返回结果

4.1 先验证 SSE 端点活着

服务启动后,第一件事是确认 SSE 端点能建立连接:

curl -N -H "Accept: text/event-stream" http://localhost:8080/sse

正常会看到类似输出,并且连接保持不关闭:

event: endpoint data: /mcp/messages?sessionId=8f3a2b1c-...

这个sessionId就是后续 POST 消息要带的。如果这里直接返回 404,说明sse.enabled没生效或者路径被 Spring Security 拦了。

4.2 用 JSON-RPC 拉工具列表

拿到 sessionId 后,发一个tools/list请求:

curl -X POST "http://localhost:8080/mcp/messages?sessionId=8f3a2b1c-..." \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }'

预期返回里能看到你注册的工具:

{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "getWeatherByCity", "description": "根据城市名称获取天气预报,返回该城市的天气状况", "inputSchema": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } ] } }

如果tools是空数组,八成是ToolCallbackProviderBean 没被扫描到,或者@Tool方法所在类没加@Component。

4.3 实际调用一次工具

再发tools/call:

curl -X POST "http://localhost:8080/mcp/messages?sessionId=8f3a2b1c-..." \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "getWeatherByCity", "arguments": { "city": "西安" } } }'

预期返回:

{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "晴天,气温 18-26 度" } ], "isError": false } }

到这里,工具列表和返回结果都符合预期,说明服务端注册和传输链路都通了。接下来在客户端里连上,模型就能自动发现并调用这个工具。

5. 常见报错排查:401、local proxy failed 与 reading choices

5.1 401 Unauthorized

客户端连上后报 401,通常有两个来源。一是客户端配置里的Authorizationheader 和服务端预期不一致;二是模型调用侧 Key 配错。先分清是工具侧还是模型侧:如果tools/list用 curl 能通,但客户端里报 401,那是客户端 header 没带上;如果 curl 也 401,检查服务端是否加了鉴权拦截器。

模型侧如果走 TaoToken 通道,Key 从控制台生成,Base URL 用 https://taotoken.net/api ,不要多加路径后缀。401 时先确认 Key 有没有多余空格。

5.2 local proxy failed

这个报错一般出现在客户端尝试连接 MCP-Server 时。常见原因是 URL 写成了http://localhost:8080但漏了/sse后缀,或者服务端只开了 STDIO 没开 SSE。检查application.yml里sse.enabled: true,以及客户端 URL 是否精确到/sse。

另一个原因是端口被占用,服务端实际没起来。用curl -N http://localhost:8080/sse确认一下,连不上就是服务端问题,不是客户端配置问题。

5.3 reading choices 相关报错

reading choices这类报错通常出现在模型响应解析阶段,说明模型返回的 JSON 结构不符合客户端预期。如果你在客户端里同时配了模型通道和工具通道,先确认模型通道的 Base URL 和 Model ID 是否匹配。Model ID 填错时,有些客户端会把错误响应当成正常响应解析,报出reading choices这种看起来和工具无关的错。

排查顺序:先用 curl 直接打模型通道的/v1/chat/completions,确认模型侧能返回标准结构;再单独测 MCP-Server 的tools/list;两边都通之后再在客户端里合起来用。

5.4 工具列表为空

tools/list返回空数组,按这个顺序查:@Tool方法所在类有没有@Component;ToolCallbackProviderBean 有没有注册;maven-compiler-plugin有没有加-parameters;spring-ai-bom有没有用importscope。这四个点覆盖了九成空列表问题。

5.5 参数名变成 arg0

客户端看到的工具参数是arg0、arg1,说明编译时没保留参数名。加上-parameters编译参数重新打包即可。这个坑很隐蔽,因为服务端本地测试用反射能拿到参数名,但打包后字节码里没有,客户端拿到的 schema 就退化了。

6. 把工具服务接进你的 AI 工作流

服务端跑通只是第一步,真正省事的是把它接进日常编码流程。如果你用 Claude Code 做长期编码,可以在客户端侧配置模型通道指向 TaoToken 的 API 通道,工具通道指向你自己的 MCP-Server。这样模型调用和工具调用各走各的,出问题时能快速定位是哪一侧。

需要生成 Key 的话,进 TaoToken 控制台(https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )创建,接入细节看文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= )。如果你更偏向长期编码和 Agent 场景,可以了解 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ),把模型调用统一管理起来。

最后给一个实用建议:MCP-Server 的工具描述要当成 API 文档来写,模型是靠 description 决定调不调的。我踩过的坑是描述写得太短,模型该调的时候不调,后来把「根据城市名称获取天气预报」改成带返回格式说明的完整句子,命中率立刻上来了。工具方法本身保持无状态、幂等,涉及写操作的一定要在 description 里标注清楚,避免模型误调。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 12:06:58

ESP32无进程沙箱?编译期、链接期、运行时四层隔离方案实战

做嵌入式最头疼的一类需求&#xff0c;不是把某个外设调通&#xff0c;而是“代码不调通还得防着它”。前两天就遇到一个很典型的问题&#xff1a;我们要在 ESP32 上开放一个小应用平台&#xff0c;让用户上传自己的逻辑进去跑&#xff0c;典型场景就是 ROS2 humble 串口桥接的…

作者头像 李华
网站建设 2026/10/2 12:06:12

96.3%准确率背后:Routine框架如何让企业级LLM Agent稳定落地TaoToken

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 12:05:50

DeepSeek Harness 桌面端 DSH 上手指南:从安装到 Skill 部署与报错排查

1. 桌面端来了&#xff0c;为什么这件事比想象中重要DeepSeek Harness 这个工具&#xff0c;早几个月前还只能在命令行里敲来敲去。那会儿社区里就有人念叨&#xff0c;什么时候能有个正经的桌面端&#xff0c;不用每次都开终端、配环境变量、对着黑框框敲命令。现在官方桌面端…

作者头像 李华
网站建设 2026/10/2 12:05:44

信号与系统:从理论到工程实践的完整指南

1. 信号与系统到底在讲什么&#xff1a;从一门课到一套工程思维如果你翻过《信号与系统》的目录&#xff0c;大概率会看到连续时间信号、离散时间信号、傅里叶变换、拉普拉斯变换、Z变换、系统响应、卷积、采样定理这些词。很多人第一次学的时候会觉得这是一门纯数学课&#xf…

作者头像 李华