
1. 从 Tool 到 McpTool为什么服务端注解总是不生效很多 Java 开发者第一次接触 Spring AI MCP 时脑子里装的还是 Spring AI 那套Tool的写法方法上打个注解ChatClient一调模型就能自动识别并调用。于是迁移到 MCP 服务端时很自然地写下McpTool启动 Spring Boot然后……客户端tools/list返回空数组或者调用时报Method not found。问题往往不在注解本身而在于 MCP 服务端注解的注册链路和 Spring AI 的Tool完全是两套机制。Tool是给ChatClient用的函数回调注册发生在ToolCallbackProvider装配阶段而McpTool是给 MCP Server 用的工具描述注册发生在 MCP 注解扫描器McpAnnotationScanner启动阶段最终要生成符合 MCP 协议的 JSON Schema 并挂到McpSyncServer或McpAsyncServer上。中间任何一环配置不对注解就只是普通注解不会被识别。这篇聚焦的就是这条链路怎么把本地一个普通的 Java 方法通过McpTool、McpResource、McpPrompt暴露成 MCP 服务端能力怎么配置 TaoToken 的统一 Key/API 通道让客户端能连上以及怎么用 MCP 客户端实际调用一次、核对返回结构确认整条注解链路真的跑通了。适合已经写过 Spring Boot、想快速把内部工具方法变成 MCP 服务的 Java 开发者。核心检索词先摆出来Spring AI MCP 服务端注解是把 Java 方法声明式注册为 MCP 工具/资源/提示的机制适合需要把已有业务方法快速暴露给 AI 客户端的场景。它和客户端注解是两套东西别混。2. TaoToken 前置统一 Key 与 API 通道怎么配在写注解之前先把通道打通。MCP 服务端本身不依赖外部模型但你要验证注解链路通常需要一个能发起 MCP 请求的客户端而客户端背后往往要接模型来做工具选择。这时候用 TaoToken 的统一 Key/API 通道最省事一个 Key 走所有模型Base URL 固定不用为每个模型单独配 endpoint。TaoToken 的 API 入口是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 OpenAI 兼容协议的 base URL 使用。官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册和拿 Key 都在这里。拿 Key 的路径登录后进控制台找到 API Keys 页面新建一个 Key。这个 Key 就是后面所有客户端配置里填的api-key。控制台地址是https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 页面是https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite。如果你用的是 Claude Code 这类编码工具TaoToken 也提供了对应的接入文档地址是https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。Claude Code 的 Anthropic 兼容入口在https://taotoken.net/api下具体路径参考文档里的 ClaudeCodeAnthropic 说明。这里要强调一个容易踩的坑MCP 服务端的注解配置和 TaoToken 的 Key 配置是两件独立的事。前者决定你的 Java 方法怎么被暴露后者决定客户端怎么连模型。很多人把两者混在一起排查结果方向就错了。正确的顺序是先确保 MCP 服务端注解注册成功用tools/list验证再确保客户端能通过 TaoToken 连上模型并调用工具。配置片段先给出来后面 §3 会展开完整的application.yml。TaoToken 在客户端侧的最小配置就是三件套Base URL 填https://taotoken.net/apiAPI Key 填控制台新建的那串Model ID 填你要用的模型名比如claude-sonnet-4-20250514或gpt-4o以控制台实际可用的为准。这三件套在 Cline、CC Switch、Codex 的auth.json里都是同样的结构只是字段名不同。3. 可复制配置McpTool 声明与 application.yml 全量片段先看服务端注解的最小可运行单元。一个McpTool方法要满足三个条件才会被扫描器注册类上有Component或任何 Spring Bean 注解方法上有McpTool参数上有McpToolParam描述。缺一个方法就不会出现在tools/list里。package com.example.mcp.tools; import org.springframework.ai.mcp.annotation.McpTool; import org.springframework.ai.mcp.annotation.McpToolParam; import org.springframework.stereotype.Component; Component public class CalculatorTools { McpTool(name add, description Add two numbers together) public int add( McpToolParam(description First number, required true) int a, McpToolParam(description Second number, required true) int b) { return a b; } McpTool(name calculate-area, description Calculate the area of a rectangle) public AreaResult calculateRectangleArea( McpToolParam(description Width, required true) double width, McpToolParam(description Height, required true) double height) { return new AreaResult(width * height, square units); } public record AreaResult(double area, String unit) {} }注意AreaResult是个 record返回对象会被自动序列化成 JSON Schema 的 output 部分。如果你返回的是CallToolResult那就走动态 schema 路径参数直接接CallToolRequest运行时自己解析request.arguments()。再看application.yml这是让注解扫描器真正工作的关键。很多人注解写对了但没配annotation-scanner.enabled扫描器默认不开注解自然不生效。spring: ai: mcp: server: name: my-mcp-server version: 1.0.0 type: SYNC annotation-scanner: enabled: true protocol: STREAMABLE_HTTP streamable-http: mcp-endpoint: /mcp port: 8080type: SYNC表示用同步服务器方法返回非响应式类型int、String、自定义 POJO、CallToolResult等。如果你写成ASYNC那Mono/Flux返回的方法才会被注册普通返回类型的方法会被过滤掉并打警告日志。这个过滤规则是 §5 排障的重点。protocol这里选STREAMABLE_HTTP对应 Streamable-HTTP 传输mcp-endpoint是客户端要连的路径。如果你用 STDIO那mcp-endpoint就不需要改成进程启动方式。SSE 的话协议名换成SSEendpoint 路径类似。客户端侧如果要接 TaoToken配置长这样以 Cline 的 MCP 配置为例放在cline_mcp_settings.json{ mcpServers: { my-spring-mcp: { url: http://localhost:8080/mcp, type: streamable-http, disabled: false } } }而模型通道的配置在 Cline 的 provider 设置里Base URL 填https://taotoken.net/apiAPI Key 填 TaoToken 控制台新建的 KeyModel ID 填你要用的模型。这三件套和 MCP 服务端的url是分开的两块别填串了。如果你用 Codexauth.json里的结构是{ OPENAI_API_KEY: 你的TaoToken Key, OPENAI_BASE_URL: https://taotoken.net/api }Model ID 在 Codex 的 config 里单独指定。CC Switch 的话在 provider 配置里同样填 Base URL、Key、Model ID 三件套MCP 服务端地址填http://localhost:8080/mcp。4. 验证请求用 MCP 客户端调用并核对返回结构配置写完启动 Spring Boot 应用。启动日志里应该能看到注解扫描器注册工具的记录类似Registered MCP tool: add。如果没看到说明扫描器没开或者方法被过滤了回到 §3 检查annotation-scanner.enabled和type。第一步验证tools/list。用 curl 直接打 Streamable-HTTP 端点curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: tools/list, params: {} }正常返回应该包含add和calculate-area两个工具每个工具有name、description、inputSchema。inputSchema里能看到a、b两个参数type是integerrequired是true。如果返回空数组说明注解没被扫描到检查类上有没有Component。第二步调用工具curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 2, method: tools/call, params: { name: add, arguments: {a: 3, b: 5} } }返回结构里result.content是一个数组第一项type是texttext是8。注意int返回值会被转成字符串放在 text content 里这是 MCP 协议的规定不是 bug。如果你返回的是AreaResultrecord那text里会是 JSON 字符串{area:15.0,unit:square units}。第三步用真实 MCP 客户端验证。在 Cline 里配置好上面的mcpServers然后在对话里说「用 add 工具算一下 3 加 5」。Cline 会先调tools/list拿到工具列表再调tools/call。你可以在 Cline 的 MCP 面板里看到调用记录和返回结果。如果模型没选对工具检查工具的description是否清晰模型是靠 description 来匹配意图的。McpResource的验证类似resources/list看资源列表resources/read读具体 URI。McpPrompt用prompts/list和prompts/get。McpComplete用completion/complete。每个注解对应的 JSON-RPC 方法不同但验证思路一致先 list 确认注册再 call/read/get 确认执行。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth报错一401 Unauthorized。这个通常出现在客户端连 TaoToken 的时候不是 MCP 服务端的问题。检查三件套Base URL 是不是https://taotoken.net/api不要带多余路径API Key 是不是控制台新建的那串注意有没有复制到空格Model ID 是不是控制台实际可用的。如果 Key 刚建等几秒再试有时候有缓存。401 和 MCP 服务端的url无关别去改mcp-endpoint。报错二local proxy failed。这个报错一般出现在客户端配置了本地代理但代理没起来或者 Base URL 填成了localhost但服务没启动。如果你在 Cline 里看到这个先确认 MCP 服务端的 Spring Boot 应用是不是真的在 8080 端口跑着curl http://localhost:8080/mcp能不能通。如果 MCP 服务端通了但模型通道报这个检查 TaoToken 的 Base URL 有没有被误填成http://localhost。报错三reading choices 相关错误。这个通常出现在模型返回结构解析阶段说明客户端拿到了响应但解析失败。常见原因是 Model ID 填错了或者模型返回的格式和客户端预期的不一致。换一个控制台里明确支持的 Model ID 再试。如果是在 MCP 工具调用后报这个检查工具的返回类型是不是客户端能解析的CallToolResult和普通 POJO 的序列化结果不一样。报错四OAuth 相关错误。如果你在 Claude Code 或某些客户端里看到 OAuth 报错说明客户端在走 OAuth 流程但 TaoToken 的 Key 是 API Key 模式。检查客户端的认证方式设置确保用的是 API Key 而不是 OAuth。Claude Code 的 Anthropic 兼容接入参考https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里的说明路径和认证方式要对上。报错五工具被过滤tools/list为空。这是服务端注解最常见的坑。检查spring.ai.mcp.server.type和方法的返回类型是否匹配。SYNC服务器只注册非响应式返回的方法ASYNC服务器只注册Mono/Flux返回的方法。如果你在SYNC服务器上写了MonoString返回的方法它会被过滤掉并打警告。反过来也一样。另外无状态服务器Stateless Streamable-HTTP会过滤掉带McpSyncRequestContext参数的方法只接受McpTransportContext或无上下文参数的方法。如果你用了McpSyncRequestContext但配的是无状态模式方法就不会注册。报错六McpToolParam的required没生效。检查参数类型是不是基本类型。int这种基本类型默认就是 requiredInteger这种包装类型才需要显式标required true。如果你把required false标在int上扫描器会忽略这个设置因为基本类型不能为 null。6. 语义一致 CTA把注解链路跑通之后注解链路跑通之后下一步通常是把它接到真实的编码或 Agent 工作流里。如果你只是验证模型能不能调用工具用模型对话入口最快https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite在里面直接问模型「调用 add 工具算 3 加 5」看它能不能正确发起 MCP 请求。如果你要把这套 MCP 服务端长期用在编码场景里比如让 Claude Code 或 Cline 持续调用你的内部工具那 Coding Plan 更合适https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite。它解决的是长期编码和 Agent 场景下的通道稳定性问题不用每次手动配 Key。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有针对不同客户端的完整配置示例。API Keys 管理在https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewriteKey 丢了或者要轮换都在这里操作。最后说一个实测下来的经验McpTool的description写得越具体模型选工具的准确率越高。别写「处理数据」这种模糊描述写「根据用户 ID 查询订单列表返回订单号和金额」。模型是靠 description 做意图匹配的这一步偷懒后面调用失败率就高。注解链路本身不复杂复杂的是让模型在正确的时机选对工具而 description 是你能控制的最直接的变量。