
1. 从注解到统一通道Spring AI MCP Annotations Server 到底解决什么问题如果你写过 Spring Boot 的 Controller那你已经理解 MCP Annotations Server 的核心思路了。RestController加GetMapping就能把一个方法暴露成 HTTP 接口而McpTool、McpResource、McpPrompt、McpComplete这组注解做的事情类似——把一个普通的 Java 方法声明成 MCP 协议里的工具、资源、提示或自动补全能力框架负责扫描、注册、序列化参数和返回结构。MCPModel Context Protocol是模型和外部能力之间的标准接口。以前要让大模型调用你的 Java 服务得手写 JSON Schema、手动拼参数、自己处理协议握手代码又长又容易出错。Annotations Server 把这些脏活全包了你只写业务逻辑注解负责描述“这个工具叫什么、参数是什么、返回什么”。这个案例适合谁三类人最值得跟做一是手里已经有 Spring Boot 业务系统、想把内部接口变成模型可调用工具的 Java 后端二是正在做 AI Agent、需要给模型挂载天气查询/用户资料/提示模板这类能力的开发者三是想搞明白 MCP 服务端到底怎么落地、不想只看官方 Demo 片段的人。但光有注解还不够。工具注册好了模型侧怎么调用这就涉及模型接入通道的问题。本地跑一个模型或者随便找个接口往往会遇到 Key 管理混乱、不同模型切换要改一堆配置、调用链路不统一的情况。我的做法是把模型侧统一走 TaoToken 的 API 通道https://taotoken.net/apiMCP Server 负责暴露能力TaoToken 负责模型调用两边职责清晰。下面从依赖、配置、注解实现到真实调用验证一步步走完。2. 前置准备依赖、版本与 TaoToken 统一通道配置先把工程骨架搭起来。Spring AI 的 MCP 支持在 1.1.x 版本里已经比较完整BOM 方式管理版本最省心。pom.xml里核心就两个依赖MCP Server 的 WebMVC Starter 和 Actuator方便看健康状态。dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.1.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意版本别用1.1.0-SNAPSHOT快照版在不同时间拉到的行为可能不一致正式版更稳。JDK 用 17 或 21Spring Boot 3.2 以上。接下来是模型侧通道。MCP Server 本身不负责调用大模型它只暴露能力真正发起对话、让模型决定调用哪个工具的是客户端。为了让模型调用走统一入口我用 TaoToken 的 API 作为模型通道。你需要在 TaoToken 控制台创建一个 API Key然后把它写进配置。先到控制台拿 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite在 API Keys 页面新建一个复制出来。模型 ID 可以在模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。配置统一放在application.yml里MCP Server 和模型通道分开写避免混在一起。下面这段可以直接复制路径和字段名保持原样spring: main: banner-mode: off ai: mcp: server: name: my-weather-server version: 0.0.1 protocol: STREAMABLE openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-20250514 temperature: 0.7 logging: file: name: ./target/server.log这里有个关键点base-url必须是https://taotoken.net/api不要带任何多余路径。api-key用环境变量注入别硬编码进代码提交到仓库。模型 ID 按你实际能用的填不同模型在工具调用能力上有差异选支持 function calling 的。如果你更习惯用 properties 格式等价写法是spring.ai.mcp.server.namemy-weather-server spring.ai.mcp.server.version0.0.1 spring.ai.mcp.server.protocolSTREAMABLE spring.ai.openai.base-urlhttps://taotoken.net/api spring.ai.openai.api-key${TAOTOKEN_API_KEY} spring.ai.openai.chat.options.modelclaude-sonnet-4-20250514启动前设置环境变量export TAOTOKEN_API_KEY你的Key。Windows 用set或直接在 IDE 的运行配置里加。这一步做完模型通道就通了接下来写注解。3. 可复制配置McpTool / McpResource / McpPrompt 注解实现注解驱动的精髓在于“声明即注册”。Spring 启动时会扫描带这些注解的 Bean 方法自动生成 MCP 协议需要的元数据。先看工具类这是最常用的。Service public class ToolProvider { private final RestClient restClient; public ToolProvider() { this.restClient RestClient.create(); } public record WeatherResponse(Current current) { public record Current(LocalDateTime time, int interval, double temperature_2m) {} } McpTool(description 获取特定位置的温度摄氏度) public WeatherResponse getTemperature( McpToolParam(description 位置纬度) double latitude, McpToolParam(description 位置经度) double longitude, McpToolParam(description 城市名称) String city) { return restClient.get() .uri(https://api.open-meteo.com/v1/forecast?latitude{latitude}longitude{longitude}currenttemperature_2m, latitude, longitude) .retrieve() .body(WeatherResponse.class); } }McpTool的description很重要模型就是靠这句话判断什么时候该调用这个工具。写得太模糊模型可能该调不调写得太长又浪费 token。McpToolParam同理每个参数的描述要让人一眼看懂单位。资源用McpResource适合暴露只读数据比如用户资料。URI 模板支持变量Service public class UserProfileResourceProvider { private final MapString, MapString, String userProfiles new HashMap(); public UserProfileResourceProvider() { MapString, String john new HashMap(); john.put(name, John Smith); john.put(email, john.smithexample.com); john.put(location, New York); userProfiles.put(john, john); } McpResource(uri user-profile://{username}, name User Profile, description 为特定用户提供用户资料信息) public ReadResourceResult getUserProfile(ReadResourceRequest request, String username) { MapString, String profile userProfiles.getOrDefault(username.toLowerCase(), Map.of()); String info profile.entrySet().stream() .map(e - e.getKey() : e.getValue()) .collect(Collectors.joining(\n)); return new ReadResourceResult( List.of(new TextResourceContents(request.uri(), text/plain, info))); } }提示模板用McpPrompt它返回的是给模型用的消息结构不是直接给用户看的Service public class PromptProvider { McpPrompt(name greeting, description 一个简单的问候提示) public GetPromptResult greetingPrompt( McpArg(name name, description 要问候的名称, required true) String name) { return new GetPromptResult(Greeting, List.of(new PromptMessage(Role.ASSISTANT, new TextContent(Hello, name ! Welcome to the MCP system.)))); } }自动补全用McpComplete适合给用户输入做联想Service public class CompletionProvider { private final MapString, ListString countryDatabase new HashMap(); public CompletionProvider() { countryDatabase.put(a, List.of(Afghanistan, Albania, Algeria, Argentina)); countryDatabase.put(b, List.of(Bahamas, Belgium, Brazil)); } McpComplete(prompt travel-planner) public CompleteResult completeCountryName(CompleteRequest request) { String prefix request.argument().value().toLowerCase(); if (prefix.isEmpty()) { return new CompleteResult(new CompleteCompletion(List.of(Enter a country name), 1, false)); } ListString matches countryDatabase .getOrDefault(prefix.substring(0, 1), List.of()) .stream() .filter(c - c.toLowerCase().startsWith(prefix)) .toList(); return new CompleteResult(new CompleteCompletion(matches, matches.size(), false)); } }主类上加SpringBootApplication即可注解扫描是自动的。如果你同时用 Spring AI 的Tool和 MCP 的McpTool需要额外注册ToolCallbackProvider但纯 MCP 注解场景不需要。4. 验证请求启动服务、查看工具列表与一次真实调用配置和代码都齐了先构建再启动。用 Maven Wrapper 避免本地 Maven 版本差异./mvnw clean install -DskipTests java -Dspring.ai.mcp.server.protocolSTREAMABLE \ -jar target/mcp-annotations-server-0.0.1-SNAPSHOT.jar启动后看日志正常会打印 MCP Server 的监听端口和已注册的能力。默认 WebMVC 模式下走 HTTP端口一般是 8080。用 curl 验证工具列表curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:1,method:tools/list,params:{}}返回里应该能看到getTemperature参数 schema 里 latitude、longitude、city 三个字段都在。如果返回空列表八成是注解所在的类没被 Spring 扫描到检查包路径是否在主类的子包下。接着做一次真实调用。先调工具curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d { jsonrpc:2.0,id:2,method:tools/call, params:{name:getTemperature,arguments:{latitude:39.9,longitude:116.4,city:Beijing}} }返回里会有temperature_2m字段说明工具链路通了。再验证资源curl -X POST http://localhost:8080/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,id:3,method:resources/read,params:{uri:user-profile://john}}应该返回 John 的资料文本。最后验证模型侧调用用 TaoToken 的模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite发一句“北京现在多少度”模型会自主决定调用getTemperature你就能看到完整的工具调用往返。这一步跑通说明注解暴露 统一通道调用整条链路都活了。5. 本篇常见错排查401、local proxy failed 与 reading choices实际跑的时候报错基本集中在几个地方。我按遇到频率排一下。401 Unauthorized。最常见的原因是TAOTOKEN_API_KEY环境变量没生效或者 Key 复制时带了空格。先在终端echo $TAOTOKEN_API_KEY确认有值再检查application.yml里是不是写成了${TAOTOKEN_API_KEY}而不是硬编码。如果 Key 本身没问题检查base-url有没有多写斜杠正确值是https://taotoken.net/api写成https://taotoken.net/api/有些客户端会拼出双斜杠导致鉴权失败。local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没启动或者环境变量里残留了HTTP_PROXY。先unset HTTP_PROXY HTTPS_PROXY再重启服务。另外确认没有把base-url指向一个不存在的本地端口。reading choices 相关报错。典型信息是Error reading choices或返回体里choices为空。这多半是模型 ID 写错了或者该模型不支持 function calling。换一个明确支持工具调用的模型 ID 再试。还有一种情况是请求体格式不对比如temperature传了字符串而不是数字。OAuth 相关报错。如果你在客户端看到 OAuth 流程失败检查是不是误开了需要 OAuth 的接入方式。用 API Key 直连的场景不需要走 OAuth把相关开关关掉即可。工具列表为空。注解类没被扫描、方法不是 public、或者返回类型不被支持都会导致注册失败。把日志级别调到 DEBUG看启动时有没有Registered MCP tool之类的输出。排查时记住一个原则先确认 MCP Server 本身能返回工具列表再确认模型通道能通最后才看模型有没有正确选择工具。分层定位比一股脑改配置快得多。6. 把注解服务接到统一通道长期编码与 Agent 场景的落地建议注解写完、验证跑通之后真正要思考的是怎么把它用在实际项目里。我的经验是MCP Server 负责“能力层”TaoToken 统一通道负责“模型层”两者解耦。这样换模型不用动工具代码加工具也不用改模型配置。如果你只是偶尔验证一下模型行为用模型对话页面就够了。但如果你在做长期的编码助手或者 Agent 应用建议用 Coding Plan 这类按周期计费的方式成本更可控也不用每次手动管 Key。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有不同语言的调用示例。几个落地时的实用技巧工具描述里把单位、取值范围写清楚模型选错工具的概率会明显下降资源 URI 用有意义的命名空间别用纯数字 ID提示模板尽量参数化别把业务逻辑写死在字符串里。还有一点MCP Server 的日志一定要开模型调用工具失败时服务端日志是唯一能看清参数到底传了什么的地方。最后提醒一句注解驱动虽然方便但别把所有方法都挂上McpTool。暴露给模型的工具越多模型选择时的干扰越大。按场景分组一个 Server 聚焦一类能力比堆一个大而全的服务更好维护。