
1. 为什么 Java 开发者现在要盯紧 MCPMCP 全称 Model Context Protocol模型上下文协议你可以把它理解成 AI 世界里的 USB-C 接口。以前每接一个外部工具AI 应用就得单独写一套适配代码工具一多就是 M×N 的适配地狱MCP 把这层交互标准化之后复杂度降到 MN一次开发、随处可用。对 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentconsole 。创建完在 API Keys 页面复制密钥https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi-keys 。如果你只是想先验证模型能不能正常对话可以直接在模型对话页面试https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel-chat 。这一步的意义是MCP 服务端跑起来之后客户端比如支持 MCP 的 IDE 或你自研的 Spring AI 应用需要调用模型来判断「用户这句话该不该触发某个工具」。模型这层通了后面的工具发现和调用才有意义。如果你打算长期做编码类 Agent可以顺手看下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。3. 可复制配置pom.xml 与 application.yml 骨架3.1 依赖引入先建一个标准 Spring Boot 工程JDK 建议 17 或 21。核心依赖是 Spring AI 的 MCP Server starter如果你要 SSE 远程模式再加 webmvc 那个 starter。下面这段可以直接粘project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.4/version /parent groupIdcom.example/groupId artifactIddemo-mcp-server/artifactId version1.0.0/version properties java.version21/java.version spring-ai.version1.0.0-M6/spring-ai.version /properties dependencies !-- MCP Server 核心Stdio 模式必需 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server/artifactId version${spring-ai.version}/version /dependency !-- SSE 远程模式再加这个 -- dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId version${spring-ai.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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第二行是 datadata 里带着后续发消息用的 endpointevent: endpoint data: /mcp/messages?sessionId8f3a1c2e-xxxx这个sessionId就是本次会话的标识客户端拿到它之后所有工具调用请求都往/mcp/messages发。如果你 curl 完什么都没返回先检查sse-endpoint配置有没有写对以及是不是被 Spring Security 拦了。4.3 发一条初始化请求拿到 sessionId 后用另一个终端发 JSON-RPC 初始化请求验证协议层能通curl -X POST http://localhost:8080/mcp/messages?sessionId8f3a1c2e-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?sessionId8f3a1c2e-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?sessionId8f3a1c2e-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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc 。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而不是nonenone是 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-8Content-Type带上charsetutf-8更稳妥。Spring 侧默认 UTF-8问题一般出在终端。6. 接下来怎么走把 echo 换成你真实的业务能力比如查数据库、调内部接口、读文件套路完全一样写个 Service方法上加Tool注册成 Bean。工具描述写得越清楚模型判断该不该调用就越准这是实测下来最影响效果的一个细节。如果你要长期跑编码类 Agent或者想让 MCP 服务端和模型侧配合做多轮工具调用可以看下 Coding Plan 的额度方案https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding-plan 。Claude Code 相关的接入说明在 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentclaudecode 。服务端这边先把 SSE 模式的会话管理和超时控制做扎实再考虑上容器隔离别一上来就堆安全策略容易把自己绕进去。