
1. 为什么 Java 后端需要自己写一个 MCP-ServerMCP-Server 是什么一句话解释它把你的业务能力查数据库、调内部接口、算指标包装成 AI 客户端能直接调用的标准工具。适合谁适合手里已经有一堆 Spring Boot 服务、想让 Claude Code、Cursor、Cline 这类客户端直接复用这些能力的 Java 后端。我试过把公司内部的订单查询、库存校验、日志检索三个接口做成 MCP 工具客户端侧只改一个 JSON 配置就能用比给每个 AI 客户端单独写插件省事得多。核心原因在于 MCPModel 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-starterM6 之后拆成了 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.version17/java.version spring-ai.version1.0.0-M6/spring-ai.version spring-boot.version3.4.3/spring-boot.version /properties dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp-server-webmvc-spring-boot-starter/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.36/version /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意spring-ai-bom必须用importscope否则各 starter 版本会各拉各的出现NoSuchMethodError。另外 Spring AI 的里程碑版本不在 Maven 中央仓库需要在settings.xml或 pom 里加 Spring Milestone 仓库repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url snapshotsenabledfalse/enabled/snapshots /repository /repositories2.2 TaoToken 通道在这里的角色MCP-Server 只暴露工具不负责模型推理。真正跑起来时客户端Cursor、Cline、Claude Code需要一边调模型、一边调你的工具。模型这一侧如果每个客户端都单独配 Key管理会很乱。TaoToken 提供统一 Key 和 API 通道官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。你可以把它理解成「模型调用的统一出口」而你的 MCP-Server 是「工具调用的统一出口」两者职责分离互不干扰。如果你用的是 Claude Code 这类编码客户端可以在客户端侧配置 Base URL 指向 TaoToken 的 API 通道Key 用 TaoToken 控制台生成的 KeyModel ID 按客户端要求填。这样模型调用走统一通道工具调用走你自己的 MCP-Server排查问题时边界清晰。2.3 传输方式选 SSE 还是 STDIOMCP 支持两种传输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) { MapString, String mockData Map.of( 西安, 晴天气温 18-26 度, 北京, 小雨气温 12-20 度, 上海, 大雨气温 15-22 度 ); return mockData.getOrDefault(city, 抱歉未查询到该城市的天气数据); } }description非常关键模型就是靠这句话决定要不要调用这个工具。写得太模糊比如「获取天气」模型可能不调写清楚「根据城市名称获取天气预报」命中率明显更高。然后注册成 ProviderComponent public class WeatherToolConfig { Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }MethodToolCallbackProvider会扫描toolObjects里所有带Tool的方法自动生成 JSON Schema。方法参数名会成为 schema 里的属性名所以参数名不要用arg0这种编译时记得加-parametersplugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId configuration parameterstrue/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 用控制台生成的 KeyModel ID 按客户端要求填。工具侧保持指向你自己的 MCP-Server两边不要混。4. 验证请求确认工具列表与返回结果4.1 先验证 SSE 端点活着服务启动后第一件事是确认 SSE 端点能建立连接curl -N -H Accept: text/event-stream http://localhost:8080/sse正常会看到类似输出并且连接保持不关闭event: endpoint data: /mcp/messages?sessionId8f3a2b1c-...这个sessionId就是后续 POST 消息要带的。如果这里直接返回 404说明sse.enabled没生效或者路径被 Spring Security 拦了。4.2 用 JSON-RPC 拉工具列表拿到 sessionId 后发一个tools/list请求curl -X POST http://localhost:8080/mcp/messages?sessionId8f3a2b1c-... \ -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/callcurl -X POST http://localhost:8080/mcp/messages?sessionId8f3a2b1c-... \ -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 choices5.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方法所在类有没有ComponentToolCallbackProviderBean 有没有注册maven-compiler-plugin有没有加-parametersspring-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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建接入细节看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你更偏向长期编码和 Agent 场景可以了解 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把模型调用统一管理起来。最后给一个实用建议MCP-Server 的工具描述要当成 API 文档来写模型是靠 description 决定调不调的。我踩过的坑是描述写得太短模型该调的时候不调后来把「根据城市名称获取天气预报」改成带返回格式说明的完整句子命中率立刻上来了。工具方法本身保持无状态、幂等涉及写操作的一定要在 description 里标注清楚避免模型误调。