1. 从注解到统一通道:Spring AI MCP Annotations Server 到底解决什么问题
如果你写过 Spring Boot 的 Controller,那你已经理解 MCP Annotations Server 的核心思路了。@RestController加@GetMapping就能把一个方法暴露成 HTTP 接口,而@McpTool、@McpResource、@McpPrompt、@McpComplete这组注解做的事情类似——把一个普通的 Java 方法声明成 MCP 协议里的工具、资源、提示或自动补全能力,框架负责扫描、注册、序列化参数和返回结构。
MCP(Model Context Protocol)是模型和外部能力之间的标准接口。以前要让大模型调用你的 Java 服务,得手写 JSON Schema、手动拼参数、自己处理协议握手,代码又长又容易出错。Annotations Server 把这些脏活全包了:你只写业务逻辑,注解负责描述“这个工具叫什么、参数是什么、返回什么”。
这个案例适合谁?三类人最值得跟做:一是手里已经有 Spring Boot 业务系统、想把内部接口变成模型可调用工具的 Java 后端;二是正在做 AI Agent、需要给模型挂载天气查询/用户资料/提示模板这类能力的开发者;三是想搞明白 MCP 服务端到底怎么落地、不想只看官方 Demo 片段的人。
但光有注解还不够。工具注册好了,模型侧怎么调用?这就涉及模型接入通道的问题。本地跑一个模型或者随便找个接口,往往会遇到 Key 管理混乱、不同模型切换要改一堆配置、调用链路不统一的情况。我的做法是把模型侧统一走 TaoToken 的 API 通道(https://taotoken.net/api),MCP Server 负责暴露能力,TaoToken 负责模型调用,两边职责清晰。下面从依赖、配置、注解实现到真实调用验证,一步步走完。
2. 前置准备:依赖、版本与 TaoToken 统一通道配置
先把工程骨架搭起来。Spring AI 的 MCP 支持在 1.1.x 版本里已经比较完整,BOM 方式管理版本最省心。pom.xml里核心就两个依赖:MCP Server 的 WebMVC Starter 和 Actuator(方便看健康状态)。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.1.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>注意版本别用1.1.0-SNAPSHOT,快照版在不同时间拉到的行为可能不一致,正式版更稳。JDK 用 17 或 21,Spring Boot 3.2 以上。
接下来是模型侧通道。MCP Server 本身不负责调用大模型,它只暴露能力;真正发起对话、让模型决定调用哪个工具的是客户端。为了让模型调用走统一入口,我用 TaoToken 的 API 作为模型通道。你需要在 TaoToken 控制台创建一个 API Key,然后把它写进配置。
先到控制台拿 Key:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite,在 API Keys 页面新建一个,复制出来。模型 ID 可以在模型对话页面试:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite。
配置统一放在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.name=my-weather-server spring.ai.mcp.server.version=0.0.1 spring.ai.mcp.server.protocol=STREAMABLE spring.ai.openai.base-url=https://taotoken.net/api spring.ai.openai.api-key=${TAOTOKEN_API_KEY} spring.ai.openai.chat.options.model=claude-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}¤t=temperature_2m", latitude, longitude) .retrieve() .body(WeatherResponse.class); } }@McpTool的description很重要,模型就是靠这句话判断什么时候该调用这个工具。写得太模糊,模型可能该调不调;写得太长,又浪费 token。@McpToolParam同理,每个参数的描述要让人一眼看懂单位。
资源用@McpResource,适合暴露只读数据,比如用户资料。URI 模板支持变量:
@Service public class UserProfileResourceProvider { private final Map<String, Map<String, String>> userProfiles = new HashMap<>(); public UserProfileResourceProvider() { Map<String, String> john = new HashMap<>(); john.put("name", "John Smith"); john.put("email", "john.smith@example.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) { Map<String, 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 Map<String, List<String>> 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)); } List<String> 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.protocol=STREAMABLE \ -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_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite)发一句“北京现在多少度”,模型会自主决定调用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_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,里面有不同语言的调用示例。
几个落地时的实用技巧:工具描述里把单位、取值范围写清楚,模型选错工具的概率会明显下降;资源 URI 用有意义的命名空间,别用纯数字 ID;提示模板尽量参数化,别把业务逻辑写死在字符串里。还有一点,MCP Server 的日志一定要开,模型调用工具失败时,服务端日志是唯一能看清参数到底传了什么的地方。
最后提醒一句:注解驱动虽然方便,但别把所有方法都挂上@McpTool。暴露给模型的工具越多,模型选择时的干扰越大。按场景分组,一个 Server 聚焦一类能力,比堆一个大而全的服务更好维护。