news 2026/9/26 9:53:10

MCP 完整学习指南与 Spring AI 实战:从零搭建可复用的 MCP 服务端

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MCP 完整学习指南与 Spring AI 实战:从零搭建可复用的 MCP 服务端

1. 为什么 Java 开发者现在要盯紧 MCP

MCP 全称 Model Context Protocol,模型上下文协议,你可以把它理解成 AI 世界里的 USB-C 接口。以前每接一个外部工具,AI 应用就得单独写一套适配代码,工具一多就是 M×N 的适配地狱;MCP 把这层交互标准化之后,复杂度降到 M+N,一次开发、随处可用。对 Java 开发者来说,这件事的意义在于:你不再需要为了给模型接一个数据库查询、文件读取或者图片搜索能力,去写一堆胶水代码,而是用 Spring Boot 熟悉的注解和 Bean 就能把能力暴露出去。

这篇聚焦一个具体目标:用 Spring Boot + Spring AI 从零搭一个可复用的 MCP 服务端,覆盖依赖引入、配置骨架、本地联调,最后用 curl 验证服务能被客户端发现。适合已经会写 Spring Boot、想快速把现有 Java 能力接进 AI 工作流的同学。我试过把公司内部一个查询接口包成 MCP 工具,整个过程比想象中短,坑主要集中在依赖版本和传输模式的选择上,下面一步步来。

2. TaoToken 前置:把模型调用这层先铺好

MCP 服务端本身只负责暴露工具,真正要跑通「模型决定调用哪个工具」这条链路,你还需要一个能访问模型的入口。这里用 TaoToken 来做模型侧的统一接入,它的 API 地址是 https://taotoken.net/api,兼容常见的调用方式,Java 侧用 WebClient 或 OpenAI 风格 SDK 都能接。

你需要先拿到一个 API Key,去控制台创建即可:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=console 。创建完在 API Keys 页面复制密钥:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api-keys 。如果你只是想先验证模型能不能正常对话,可以直接在模型对话页面试:https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model-chat 。

这一步的意义是:MCP 服务端跑起来之后,客户端(比如支持 MCP 的 IDE 或你自研的 Spring AI 应用)需要调用模型来判断「用户这句话该不该触发某个工具」。模型这层通了,后面的工具发现和调用才有意义。如果你打算长期做编码类 Agent,可以顺手看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan 。

3. 可复制配置:pom.xml 与 application.yml 骨架

3.1 依赖引入

先建一个标准 Spring Boot 工程,JDK 建议 17 或 21。核心依赖是 Spring AI 的 MCP Server starter,如果你要 SSE 远程模式,再加 webmvc 那个 starter。下面这段可以直接粘:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.4</version> </parent> <groupId>com.example</groupId> <artifactId>demo-mcp-server</artifactId> <version>1.0.0</version> <properties> <java.version>21</java.version> <spring-ai.version>1.0.0-M6</spring-ai.version> </properties> <dependencies> <!-- MCP Server 核心,Stdio 模式必需 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> <version>${spring-ai.version}</version> </dependency> <!-- SSE 远程模式再加这个 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> <version>${spring-ai.version}</version> </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> </dependencies> </project>

注意:Spring AI 的版本迭代很快,1.0.0-M6 是里程碑版本,坐标和注解包路径在不同版本间可能微调。如果编译报找不到@Tool,先确认你引入的是spring-ai-starter-mcp-server而不是旧的实验性坐标。

3.2 配置骨架

Stdio 模式下服务作为本地子进程运行,不需要 Web 容器,配置要关掉 banner 和 web 类型:

spring: ai: mcp: server: name: demo-mcp-server version: 1.0.0 type: SYNC main: banner-mode: off web-application-type: none

如果你要 SSE 远程模式,让多个客户端通过 HTTP 连进来,改成这样:

spring: ai: mcp: server: name: demo-mcp-server version: 1.0.0 stdio: false type: ASYNC sse-endpoint: /sse sse-message-endpoint: /mcp/messages main: banner-mode: off web-application-type: servlet server: port: 8080

两种模式的区别很直接:Stdio 适合本地个人工具,客户端把服务当子进程拉起;SSE 适合部署到服务器上给多人用。本地联调阶段我建议先用 SSE,因为 curl 能直接打,排查方便。

3.3 写一个最小工具

工具类就是普通的 Spring Bean,方法上加@Tool注解,参数加@ToolParam描述:

package com.example.mcp.service; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.stereotype.Service; @Service public class EchoTool { @Tool(description = "回显输入文本,用于验证 MCP 工具是否被正确调用") public String echo( @ToolParam(description = "要回显的文本内容") String text) { return "echo: " + text; } }

然后注册成 ToolCallbackProvider,这一步是把注解方法真正暴露给 MCP 协议的关键:

package com.example.mcp.config; import com.example.mcp.service.EchoTool; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpConfig { @Bean public ToolCallbackProvider echoTools(EchoTool echoTool) { return MethodToolCallbackProvider.builder() .toolObjects(echoTool) .build(); } }

启动类保持最简:

package com.example.mcp; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoMcpApplication { public static void main(String[] args) { SpringApplication.run(DemoMcpApplication.class, args); } }

4. 验证请求:确认 MCP 服务能被客户端发现

4.1 启动服务

用 SSE 模式打包启动:

mvn clean package -DskipTests java -jar target/demo-mcp-server-1.0.0.jar

看到 Tomcat 在 8080 起来,说明 Web 容器正常。Stdio 模式则不会有端口,进程会挂在标准输入上等客户端通信。

4.2 用 curl 检查 SSE 端点

MCP 的 SSE 传输会先建立一个长连接,服务端通过这个连接推送消息端点地址。用 curl 打一下:

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

正常的话你会看到类似这样的流式输出,第一行是 event,第二行是 data,data 里带着后续发消息用的 endpoint:

event: endpoint data: /mcp/messages?sessionId=8f3a1c2e-xxxx

这个sessionId就是本次会话的标识,客户端拿到它之后,所有工具调用请求都往/mcp/messages发。如果你 curl 完什么都没返回,先检查sse-endpoint配置有没有写对,以及是不是被 Spring Security 拦了。

4.3 发一条初始化请求

拿到 sessionId 后,用另一个终端发 JSON-RPC 初始化请求,验证协议层能通:

curl -X POST "http://localhost:8080/mcp/messages?sessionId=8f3a1c2e-xxxx" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "curl-test", "version": "1.0"} } }'

返回里如果能看到serverInfo和capabilities.tools,说明服务端已经准备好暴露工具了。接着发tools/list就能看到你注册的 echo 工具:

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

返回的 JSON 里result.tools数组应该包含 name 为echo的条目,description 就是你注解里写的那句。到这一步,MCP 服务端就算真正可被发现了。

4.4 调用工具

最后验证工具能执行:

curl -X POST "http://localhost:8080/mcp/messages?sessionId=8f3a1c2e-xxxx" \ -H "Content-Type: application/json" \ -d '{ "jsonrpc":"2.0", "id":3, "method":"tools/call", "params":{"name":"echo","arguments":{"text":"hello mcp"}} }'

返回result.content里出现echo: hello mcp,整条链路就通了。如果你想让模型自动决定调用这个工具,把 MCP 服务端配置到支持 MCP 的客户端里,模型侧走 TaoToken 的 API 即可,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc 。

5. 本篇常见错排查

启动报NoClassDefFoundError: org/springframework/ai/tool/annotation/Tool八成是依赖坐标不对或者版本没对齐。确认spring-ai-starter-mcp-server的版本和spring-ai.version属性一致,别一个用 M6 一个用 M5。

curl SSE 端点返回 404检查sse-endpoint配置值和你 curl 的路径是否一致。默认可能是/sse,但如果你在 yml 里改成了别的,curl 也要跟着改。另外确认web-application-type是servlet而不是none,none是 Stdio 模式用的,不会有 HTTP 端口。

tools/list 返回空数组工具没被注册。最常见的原因是忘了写ToolCallbackProvider那个 Bean,或者@Tool方法所在的类没有被 Spring 扫描到。检查包路径是否在启动类的同级或子包下。

sessionId 用一次就失效SSE 会话是有生命周期的,curl 断开后 session 就没了。每次测试都要重新 curl 一次 SSE 拿新的 sessionId,别复用旧的。

Stdio 模式下进程启动后立刻退出Stdio 模式依赖标准输入保持打开,如果你在终端直接java -jar跑,没有客户端连着 stdin,进程可能直接结束。这是正常的,Stdio 模式本来就该由客户端拉起,本地调试建议用 SSE。

中文参数乱码curl 发 JSON 时确保终端编码是 UTF-8,Content-Type带上charset=utf-8更稳妥。Spring 侧默认 UTF-8,问题一般出在终端。

6. 接下来怎么走

把 echo 换成你真实的业务能力,比如查数据库、调内部接口、读文件,套路完全一样:写个 Service,方法上加@Tool,注册成 Bean。工具描述写得越清楚,模型判断该不该调用就越准,这是实测下来最影响效果的一个细节。

如果你要长期跑编码类 Agent,或者想让 MCP 服务端和模型侧配合做多轮工具调用,可以看下 Coding Plan 的额度方案:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding-plan 。Claude Code 相关的接入说明在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claudecode 。服务端这边,先把 SSE 模式的会话管理和超时控制做扎实,再考虑上容器隔离,别一上来就堆安全策略,容易把自己绕进去。

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

macOS Homebrew 四层适配指南:权限、架构、换源与生态

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

作者头像 李华
网站建设 2026/9/26 9:52:11

FAST-LIO2落地实战:ikd-tree增量地图与重定位全链路解析

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

作者头像 李华
网站建设 2026/9/26 9:52:06

EvoMap 全解:让 OpenClaw 不停进化的秘密与 TaoToken 配置实践

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

作者头像 李华
网站建设 2026/9/26 9:51:01

开源无人机蜂群编队全流程工程链:从散件到协同飞行

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

作者头像 李华