Spring AI MCP Sampling Server Annotations 完整案例:从零搭建可复现的 Spring Boot 服务 1. 从一次真实踩坑说起为什么 Sampling 和 Server Annotations 值得单独写一篇如果你正在用 Spring Boot 做 MCP 服务端大概率会遇到两个绕不开的能力一是让服务端主动向客户端“借”一次模型推理也就是 Sampling二是用注解把普通 Java 方法直接暴露成 MCP 工具也就是 Server Annotations。前者解决的是“服务端没有模型但需要模型生成内容”的问题后者解决的是“不想手写协议解析只想写业务逻辑”的问题。把这两件事拼在一起就是一个能跑通、能复现、能扩展的最小闭环。我试过把这两个能力拆开写 demo结果发现单独看都简单合在一起就出问题注解方法里拿不到McpSyncServerExchange采样请求发出去客户端不响应日志通知顺序错乱模型偏好写了openai却返回了别的模型。这些坑不是文档没写而是文档写得太散。所以这篇按“从零搭建可复现的 Spring Boot 服务”来组织每一步都给可复制的配置和代码最后用统一 Key/API 通道完成端到端联调。适合谁看已经会写 Spring Boot、想快速把 MCP 服务端注解能力和 Sampling 跑通的开发者正在做多模型协作、想让服务端按需调用不同模型生成内容的同学以及被local proxy failed、reading choices这类报错卡住、想找一份对照排查表的人。核心检索词先明确Spring AI MCP Sampling Server Annotations 完整案例本质是“用注解声明 MCP 工具 在工具内发起 Sampling 请求 通过统一 API 通道完成模型调用”的 Spring Boot 落地路径。下面从依赖开始一步步来。2. 前置准备依赖、版本与 TaoToken 统一通道配置2.1 版本矩阵先对齐别让 BOM 坑了你Spring AI 的 MCP 模块迭代很快版本不对齐会出现注解类找不到、McpSyncServerExchange包路径变化等问题。下面这套组合是实测能跑通的组件版本说明Spring Boot3.4.53.4.x 对 WebMvc 支持稳定Spring AI1.1.0-SNAPSHOT需要开启 snapshot 仓库Java17MCP 模块要求 17Maven3.9构建工具MCP 注解包spring-ai-community提供McpTool等注解pom.xml里核心依赖和 BOM 这样写properties java.version17/java.version spring-ai.version1.1.0-SNAPSHOT/spring-ai.version /properties dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-server-webmvc/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-community-mcp-annotations/artifactId /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如果拉不到 SNAPSHOT在repositories里加上 Spring 的 snapshot 仓库即可。这一步不做后面McpTool会直接编译不过。2.2 用 TaoToken 统一 Key/API 通道避免多模型各配一套Sampling 的典型场景是“同一个输入用不同模型各生成一次”。如果 OpenAI 和 Anthropic 各配一套 Key、各写一套 base URL配置会迅速膨胀。更省事的做法是走统一通道一个 Key、一个 Base URL模型用 Model ID 区分。TaoToken 的 API 入口是https://taotoken.net/api控制台里可以创建 API Key模型对话页可以直接验证模型是否可用。配置时把 Base URL 指向它Key 用控制台生成的Model ID 按需填。这样在 Sampling 的ModelPreferences里加 hint 时客户端侧只需要认这一套通道。注意Base URL 填https://taotoken.net/api不要带多余路径Key 放在环境变量里别硬编码进仓库。2.3 application.properties 关键项spring.main.banner-modeoff spring.ai.mcp.server.namemcp-sampling-server-annotations spring.ai.mcp.server.version0.0.1 spring.ai.mcp.server.protocolSTREAMABLE logging.file.name./target/server.logbanner-modeoff不是可选项。STDIO 传输下任何多余的标准输出都会污染协议帧导致客户端解析失败。日志重定向到文件也是同一个原因。3. 可复制配置注解声明、Sampling 请求与 settings 片段3.1 主类与项目结构package org.springframework.ai.mcp.sample.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication public class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } }结构保持简单src/main/java/org/springframework/ai/mcp/sample/server/ ├── McpServerApplication.java └── WeatherService.java src/main/resources/ └── application.properties3.2 注解声明McpTool 与 McpToolParamMcpTool把一个方法标记为 MCP 工具McpToolParam描述参数。方法第一个参数可以是McpSyncServerExchange它让你在工具内部拿到客户端能力、发日志通知、发起 Sampling。McpTool(description Get the temperature (in celsius) for a specific location) public String getTemperature2( McpSyncServerExchange exchange, McpToolParam(description The location latitude) double latitude, McpToolParam(description The location longitude) double longitude) { // 业务逻辑 Sampling }这里有个容易忽略的点exchange参数不会被当成工具入参暴露给客户端框架会自动注入。如果你把它写成普通参数客户端调用时会要求传一个对象直接报参数校验错误。3.3 Sampling 请求构造与模型偏好Sampling 的核心是CreateMessageRequest。先检查客户端是否声明了 sampling 能力再构造请求if (exchange.getClientCapabilities().sampling() ! null) { var builder McpSchema.CreateMessageRequest.builder() .systemPrompt(You are a poet!) .messages(List.of(new McpSchema.SamplingMessage( McpSchema.Role.USER, new McpSchema.TextContent(Please write a poem about this weather...)))); var openAiRequest builder .modelPreferences(ModelPreferences.builder().addHint(openai).build()) .build(); CreateMessageResult openAiResp exchange.createMessage(openAiRequest); var anthropicRequest builder .modelPreferences(ModelPreferences.builder().addHint(anthropic).build()) .build(); CreateMessageResult anthropicResp exchange.createMessage(anthropicRequest); }addHint只是偏好提示最终选哪个模型由客户端决定。如果你走 TaoToken 统一通道客户端侧配置的 Model ID 才是真正生效的那个hint 用于在多个候选里做倾向。3.4 日志通知让采样过程可观测exchange.loggingNotification(LoggingMessageNotification.builder() .level(LoggingLevel.INFO) .data(Start sampling) .build()); // ... 采样 ... exchange.loggingNotification(LoggingMessageNotification.builder() .level(LoggingLevel.INFO) .data(Finish Sampling) .build());日志通知是单向的客户端收到后展示即可。顺序上建议“开始”在构造请求前发“结束”在两次采样都完成后发否则客户端看到的进度会跳。3.5 客户端侧 settings 片段以 Cline MCP 为例如果你用 Cline 作为 MCP 客户端cline_mcp_settings.json里这样配{ mcpServers: { spring-sampling-server: { url: http://localhost:8080/mcp, transport: streamable-http, env: { TAOTOKEN_API_KEY: 你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_MODEL_ID: claude-3-5-sonnet } } } }三件套齐了Base URL、Key、Model ID。缺任何一个Sampling 请求都会在客户端侧失败。Codex 的auth.json同理把 base URL 和 key 写进去model 字段填 Model ID。4. 验证请求从 tools/call 到 Sampling 响应全链路4.1 启动服务mvn spring-boot:run启动后确认日志文件里出现 MCP server 注册信息且没有 banner 输出到控制台。如果控制台有 Spring 图标说明banner-modeoff没生效STDIO 场景下会出问题。4.2 发起 tools/call用 MCP 客户端或直接发 HTTP 请求{ method: tools/call, params: { name: getTemperature2, arguments: { latitude: 37.7749, longitude: -122.4194 } } }4.3 预期响应结构成功时返回内容包含两段诗歌和原始天气 JSONOpenAI poem about the weather: [模型生成内容] Anthropic poem about the weather: [模型生成内容] {current:{time:2025-01-01T12:00:00,interval:900,temperature_2m:15.3}}同时客户端侧应收到两条日志通知Start sampling和Finish Sampling。如果只收到开始没收到结束多半是第二次createMessage抛异常被吞了检查客户端是否支持多模型切换。4.4 用模型对话页快速验证通道在正式联调前先去模型对话页发一条测试消息确认 Key 和 Base URL 可用。这一步能提前排除 401 和通道配置错误避免把问题带到 MCP 链路里排查。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth5.1 401 Unauthorized最常见。原因通常是 Key 没传、传错位置、或者 Base URL 写成了带路径的地址。检查顺序环境变量是否被读取 → Key 是否有多余空格 → Base URL 是否为https://taotoken.net/api。如果客户端侧配置了 Key 但服务端 Sampling 仍 401说明 Sampling 请求走的是客户端通道Key 要在客户端侧配。5.2 local proxy failed这个报错通常出现在客户端尝试连接本地 MCP 服务时。检查服务是否真的在监听、端口是否被占用、transport是否和spring.ai.mcp.server.protocol一致。STREAMABLE 对应streamable-http写成sse会连不上。5.3 reading choices 相关报错reading choices一般出现在解析模型响应时说明返回结构不是预期的 chat completion 格式。原因可能是 Model ID 填错、通道返回了错误 JSON、或者客户端把非模型响应当成了模型响应。先用模型对话页确认该 Model ID 能正常返回再回到 MCP 链路。5.4 OAuth 相关报错如果客户端要求 OAuth 而你的服务端没配会在握手阶段失败。MCP 服务端本身不强制 OAuth但部分客户端默认开启。在客户端 settings 里关掉 OAuth 或改用 API Key 模式即可。5.5 注解不生效McpTool方法没被注册通常是包扫描问题。确认WeatherService在SpringBootApplication所在包的子包下或者手动加ComponentScan。另外注解包依赖没引入也会导致编译期就找不到注解。6. 把链路固定下来从验证到长期使用的 CTA跑通一次之后建议把配置固化成模板Base URL、Key、Model ID 三件套写进客户端 settings服务端只保留业务逻辑和注解声明。这样换模型时只改 Model ID不用动代码。如果你还在选通道可以先用模型对话页验证模型可用性需要长期跑编码和 Agent 任务Coding Plan 更适合接入和排障过程中遇到 Key 或 Base URL 问题直接去 API Keys 页面重新生成再对照接入文档检查配置。把这三步走完Spring AI MCP Sampling Server Annotations 这条链路基本就稳了。