1. 为什么要在 Spring AI 里把 MCP 双端跑通
MCP(Model Context Protocol)说白了就是给大模型配一套“工具插座标准”:模型不直接碰你的数据库和内部接口,而是通过一份声明好的工具清单去调用。Spring AI 从 1.0.0-M6 起把 MCP 的 Server 端和 Client 端都做进了 starter,Java 团队不用再自己写一套 JSON-RPC 胶水层。这篇要解决的就是一个很具体的问题:怎么在同一个 Spring 生态里,一边把业务能力暴露成 MCP Server,一边用 MCP Client 把模型和工具串起来,并且模型调用统一走 TaoToken 的 Key,避免每个项目各自维护一套模型凭证。
适合谁看:手上已经有 Spring Boot 3.x 项目、想给现有系统加“AI 能调工具”能力的后端同学;或者正在做 AI Agent 原型、被 MCP 的 SSE/STDIO 两种传输方式绕晕的人。整条链路我按“服务端暴露工具 → 客户端连接 → 模型决策 → 工具执行 → 结果回填”的顺序写,每一步都给可复制的配置和代码,最后用 curl 验证端到端是否真的跑通。
先说清楚两个角色。MCP Server 是工具提供方,它不认识模型,只负责把@Tool标注的方法注册成可被发现的工具,通过 SSE 或 STDIO 暴露出去。MCP Client 是调用方,它持有模型客户端,把用户的自然语言问题交给模型,模型判断需要哪个工具,Client 再去 Server 拉工具列表并执行。模型这一层我们统一指向 TaoToken 的 OpenAI 兼容接口,Base URL 用https://taotoken.net/api,Key 在控制台生成,这样 Server 和 Client 两个工程只需要一份模型凭证。
一个容易踩的认知坑:很多人以为 MCP Server 里也要配模型。不需要。Server 只做工具声明与执行,模型推理全部发生在 Client 侧。你把模型配置塞进 Server,只会多一份无用的连接。下面第 2 节先把 Key 和依赖版本定下来,第 3 节开始写代码。
2. TaoToken 前置准备与 Spring AI 版本对齐
统一 Key 的意思是:模型对话、工具调用决策都走同一个入口,工程里只出现一处base-url和一处api-key。先去 TaoToken 控制台创建 API Key,地址是https://taotoken.net/api-keys,生成后复制成环境变量,别硬编码进 yml。模型 ID 在模型对话页能看到当前可用的名称,选一个支持 function calling 的对话模型即可,工具调用依赖这个能力。
依赖版本必须对齐,这是 Spring AI MCP 最容易翻车的地方。BOM 版本、mcp-server starter、mcp-client starter、openai starter 四者版本要一致,混用不同 milestone 会出现NoSuchMethodError或者工具列表为空。下面这份pom.xml片段是双端工程共用的版本管理部分:
<properties> <java.version>21</java.version> <spring-ai.version>1.0.0-M6</spring-ai.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>${spring-ai.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>Server 端额外引入 WebFlux 版 MCP Server starter,走 SSE 传输;Client 端引入 MCP Client starter 加 OpenAI 模型 starter。注意 Client 端不要引 WebFlux 的 server starter,否则端口会和你的业务服务打架。环境变量这样设,Linux/macOS 用 export,Windows 用系统环境变量面板:
export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"模型名建议单独放一个变量TAOTOKEN_MODEL,方便切换。这里有个细节:OpenAI 兼容接口的 base-url 末尾不要带/v1,Spring AI 的 OpenAI starter 会自己拼路径,多写一段会变成/v1/v1/chat/completions直接 404。我试过在 yml 里写完整路径,结果报404 Not Found,去掉后缀就好了。
版本对齐之后,两个工程的目录结构建议分开:mcp-server和mcp-client各自独立启动,Server 默认 8081,Client 默认 8080,避免端口冲突。下面进入具体配置。
3. 可复制配置:application.yml 与工具声明
先写 Server 端。application.yml里开启 MCP Server、指定名称版本和 SSE 端点,同时把 WebFlux 端口改成 8081:
server: port: 8081 spring: application: name: weather-mcp-server ai: mcp: server: enabled: true name: weather-mcp-server version: 1.0.0 type: SYNC sse-endpoint: /mcp/stream工具类用@Tool注解声明,description 要写清楚“什么时候该调用”,模型靠这句话做决策,写得太含糊会导致工具不被选中。下面这个天气工具返回结构化 Map,方便模型二次组织语言:
@Component public class WeatherTool { @Tool(description = "查询指定城市的当前天气,输入参数为城市中文名,例如 深圳") public Map<String, Object> getCurrentWeather(String city) { System.out.printf("MCP Server 收到天气查询: %s%n", city); return Map.of( "city", city, "temperature", "26C", "condition", "晴", "humidity", "60%" ); } }注册成ToolCallbackProvider,这一步不做的话工具不会出现在 SSE 的工具清单里:
@Configuration public class McpServerConfig { @Bean public ToolCallbackProvider weatherTools(WeatherTool weatherTool) { return MethodToolCallbackProvider.builder() .toolObjects(weatherTool) .build(); } }Client 端的application.yml要同时配模型和 MCP 连接。模型走 TaoToken,MCP 用 SSE 连本地 Server:
server: port: 8080 spring: ai: openai: base-url: ${TAOTOKEN_BASE_URL} api-key: ${TAOTOKEN_API_KEY} chat: options: model: ${TAOTOKEN_MODEL} temperature: 0.3 mcp: client: enabled: true name: weather-mcp-client version: 1.0.0 type: SYNC sse: connections: weather-server: url: http://localhost:8081 sse-endpoint: /mcp/stream这段配置里三个关键件必须齐全:Base URL 指向https://taotoken.net/api,Key 从环境变量注入,Model ID 用你控制台里可用的对话模型。少任何一个,Client 启动时不会报错,但调用时会抛401或model not found。如果你用 Cline 或 CC Switch 这类工具做本地调试,同样按这三件套填:Base URL、API Key、Model ID,不要只填 Key 就以为能通。
工具声明和连接配置都齐了,接下来写 Client 的调用代码,把 ChatClient 和工具执行串起来。
4. 验证请求:从 curl 到端到端工具调用
Client 侧的核心是把ChatClient和 MCP 工具绑定。Spring AI 的 MCP Client starter 会自动把连接到的工具注册进ToolCallbackProvider,你在构建 ChatClient 时把 provider 传进去,模型就能看到工具清单:
@Service public class WeatherAgentService { private final ChatClient chatClient; public WeatherAgentService(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) { this.chatClient = builder .defaultToolCallbacks(toolCallbackProvider) .build(); } public String ask(String question) { return chatClient.prompt() .user(question) .call() .content(); } }再暴露一个 HTTP 接口方便 curl 验证:
@RestController public class AskController { private final WeatherAgentService agentService; public AskController(WeatherAgentService agentService) { this.agentService = agentService; } @PostMapping("/ask") public Map<String, String> ask(@RequestBody Map<String, String> body) { String answer = agentService.ask(body.get("question")); return Map.of("answer", answer); } }启动顺序很重要:先起 Server(8081),确认 SSE 端点可访问,再起 Client(8080)。验证 Server 是否暴露成功,直接 curl SSE 端点看是否返回事件流:
curl -N http://localhost:8081/mcp/stream正常会看到event: endpoint之类的 SSE 帧,说明工具清单通道已打开。然后调 Client 的接口:
curl -X POST http://localhost:8080/ask \ -H "Content-Type: application/json" \ -d '{"question":"深圳今天天气怎么样?"}'预期返回类似深圳今天晴,气温 26C,湿度 60%。同时 Server 控制台会打印MCP Server 收到天气查询: 深圳,这条日志是端到端打通的最硬证据——它证明模型确实做出了工具调用决策,Client 也确实把参数传到了 Server 并执行。如果只看到模型回复“我无法获取实时天气”,说明工具没被注册进 ChatClient,回去检查defaultToolCallbacks是否传了 provider。
再补一个多轮验证:连续问“北京呢”,观察 Server 日志是否再次打印。两次都命中说明工具清单在会话间是复用的,不是一次性绑定。到这一步,模型调用走 TaoToken、工具注册走 MCP Server、调用决策走 MCP Client,整条链路闭环。
5. 本篇常见错排查:401、local proxy failed 与工具为空
排障按报错原文对照,别凭感觉改配置。下面这几个是我在实际项目里反复遇到的。
401 Unauthorized出现在 Client 调用模型时,九成是 Key 没注入或 base-url 写错。检查环境变量是否在当前 shell 生效,echo $TAOTOKEN_API_KEY看有没有值;再确认 yml 里是${TAOTOKEN_BASE_URL}而不是写死的带/v1的地址。TaoToken 的 API 入口是https://taotoken.net/api,路径拼接交给 starter。
local proxy failed或连接被拒,通常是 Client 连 Server 的地址不对。SSE 连接配置里url是主机加端口,sse-endpoint是路径,两者不要重复拼。比如写成url: http://localhost:8081/mcp/stream再加sse-endpoint: /mcp/stream,实际请求会变成双路径。正确写法就是第 3 节那份,url 只到端口。
reading choices或反序列化异常,多半是模型返回结构和你预期不符,常见于模型 ID 填错、走到了非对话模型。回控制台确认 Model ID,并确认该模型支持 function calling。还有一种情况是 temperature 设太高导致工具参数格式漂移,调到 0.3 以下更稳。
工具列表为空、模型始终不调用工具,检查三处:Server 端ToolCallbackProviderBean 是否被扫描到(配置类别放在启动类扫描不到的包);Client 端构建 ChatClient 时是否真的传了 provider;@Tool的 description 是否写清了触发条件。另外 SYNC/ASYNC 类型两端要一致,一端 SYNC 一端 ASYNC 会出现握手后工具清单拉取失败。
OAuth 相关报错一般出现在你误配了需要鉴权的远程 MCP 服务,本地自建 Server 不需要 OAuth,把多余的鉴权配置删掉即可。如果确实要接第三方远程 MCP,鉴权信息放在对应 connection 下,别塞进全局模型配置里。
最后提醒一个隐蔽问题:Server 和 Client 都用spring.ai.mcp前缀,但子键不同,复制配置时容易把 server 段贴到 client 工程里,启动不报错但行为诡异。建议两个工程的 yml 分开维护,改完用curl各验一次再联调。
6. 把 Key 与工具链固定下来的下一步
链路跑通后,建议把模型凭证和工具连接都收敛到环境变量加一份共享配置模板,团队里谁拉代码都只改 Key 不改结构。模型侧继续用 TaoToken 的统一入口,工具侧把新增业务能力按@Tool方法增量注册,Server 不用重启模型配置。需要长期跑编码类 Agent 的,可以看 Coding Plan 的额度方式;只是验证模型和工具是否通,用模型对话页手动发一轮请求最快;接入细节和参数说明在接入文档里查。把第 4 节的 curl 命令存成脚本,每次改完配置跑一遍,比肉眼检查 yml 靠谱得多。