1. 为什么要在 Spring AI 里用 MCP 调本地工具
如果你正在用 Spring AI 做 AI 应用,大概率会遇到一个很现实的问题:大模型本身只会“聊天”,它不知道你本地的库存表、订单接口、天气数据长什么样。想让模型真正干活,就得把本地方法暴露成它能调用的工具。MCP(Model Context Protocol)就是干这个的——它定义了一套标准协议,让大模型和本地自定义工具之间能双向通信:客户端发现工具、传参数,服务端执行本地业务逻辑,再把结果回给模型生成最终回答。
但真正落地时,麻烦往往不在协议本身,而在“Key 和鉴权”。我见过太多项目,服务端一个 Key、客户端一个 Key、换个模型又要改配置,多模型切换时 Key 分散在好几个 yml 里,调试一次要翻半天。这篇就聚焦一条完整链路:Spring AI 应用通过 MCP 协议调用本地自定义工具,同时用 TaoToken 统一 Key 和 API 通道,把多模型鉴权收敛成一份配置。适合谁?适合已经写过 Spring Boot、想快速把本地业务方法接进大模型、又不想被 Key 管理拖后腿的开发者。
核心检索词先摆出来:Spring AI、MCP 协议、大模型调用本地自定义工具、TaoToken 统一 Key。整套案例基于 Spring Boot + Spring AI + MCP 异步服务端/客户端,可直接落地复用。下面从原问题讲起,再给 TaoToken 前置配置、可复制代码、验证请求和排错。
2. TaoToken 统一 Key 前置准备与 Base URL 配置
在动手写 MCP 代码之前,先把“模型通道”这件事解决掉。传统做法是每个模型厂商一个 Key,DashScope 一个、OpenAI 一个,客户端配置里散落一堆api-key。TaoToken 的思路是提供一个统一的 API 通道,你只需要一个 Key,就能通过兼容接口调用不同模型。对 Spring AI 来说,这意味着客户端配置里只维护一份 Base URL 和一份 Key,换模型时改 Model ID 就行。
先拿 Key。打开官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 列表在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。创建完复制那串sk-开头的字符串,后面配置要用。
这里有个关键点:TaoToken 的 API 入口是 https://taotoken.net/api ,注意这个地址不带任何查询参数,配置 Base URL 时就用它。Spring AI 的 OpenAI 兼容 starter 会把/v1/chat/completions拼在后面,所以 Base URL 填到/api这一层即可。
三件套先记牢,后面每个环节都要对齐:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 统一 API 通道入口 |
| API Key | 控制台创建的sk-开头字符串 | 一份 Key 走通多模型 |
| Model ID | 如claude-sonnet-4-5、gpt-4o等 | 按需切换,不改 Key |
如果你用的是 Claude Code 这类编码工具,TaoToken 也提供了对应的接入文档,地址是 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有 Base URL、Key、Model ID 的完整填写示例。对 Spring AI 项目来说,我们主要用 OpenAI 兼容协议,所以客户端依赖选spring-ai-openai-spring-boot-starter即可,不需要为每个厂商单独引 starter。
为什么要先做这一步?因为 MCP 客户端本身要调用大模型来决定“是否调用工具、调用哪个工具”。如果模型通道没配好,MCP 工具注册得再漂亮,模型也收不到工具列表。把 TaoToken 作为统一出口后,客户端配置里只有一份base-url和一份api-key,MCP 服务端则完全不需要关心模型 Key——它只负责暴露工具。职责一分开,排错也清晰:连不上模型查客户端,工具调不到查服务端。
3. 可复制的 MCP 服务端与客户端配置片段
这一节直接给能粘贴的配置和代码。先明确一个高频踩坑点:spring-ai-starter-mcp-server-webflux依赖绝对不能和spring-boot-starter-web共存。web 依赖会强制拉起 Tomcat,而 MCP 的 WebFlux 服务需要 Netty 容器。两者共存时程序能启动,但 MCP 服务端异常,客户端连不上、工具调不到。所以服务端只引 webflux,客户端可以正常引 web。
3.1 服务端 pom 依赖
<dependencies> <!-- Spring Boot 核心启动器,不引 web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- MCP 服务端 WebFlux 异步依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> </dependencies>3.2 服务端 application.yml
server: port: 8014 servlet: encoding: enabled: true force: true charset: UTF-8 spring: application: name: SAA-14LocalMcpServer ai: mcp: server: type: async name: customer-define-mcp-server version: 1.0.03.3 自定义工具类(天气查询)
用@Tool注解标记方法,description写清楚用途,模型靠它识别工具能力。
package com.atguigu.study.service; import org.springframework.ai.tool.annotation.Tool; import org.springframework.stereotype.Service; import java.util.Map; @Service public class WeatherService { @Tool(description = "根据城市名称获取天气预报") public String getWeatherByCity(String city) { Map<String, String> weatherMap = Map.of( "北京", "降雨频繁,今天和后天雨势较强,部分地区有暴雨并伴强对流天气", "上海", "多云,15℃~27℃,南风3级,当前温度27℃", "深圳", "多云40天,阴16天,雨30天,晴3天" ); return weatherMap.getOrDefault(city, "抱歉:未查询到对应城市!"); } }3.4 工具注册配置类
把工具类注册成ToolCallbackProvider,对外暴露。
package com.atguigu.study.config; import com.atguigu.study.service.WeatherService; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.method.MethodToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class McpServerConfig { @Bean public ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }3.5 客户端 pom 依赖
客户端要引 web 提供接口测试能力,模型通道用 OpenAI 兼容 starter 对接 TaoToken。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- OpenAI 兼容 starter,对接 TaoToken 统一通道 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId> </dependency> <!-- MCP 客户端依赖 --> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> </dependency> </dependencies>3.6 客户端 application.yml(三件套齐全)
server: port: 8015 servlet: encoding: enabled: true force: true charset: UTF-8 spring: application: name: SAA-15LocalMcpClient ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-5 mcp: client: type: async request-timeout: 60s toolcallback: enabled: true sse: connections: mcp-server1: url: http://localhost:8014注意api-key用环境变量注入,别硬编码进仓库。model这一项就是 Model ID,换模型只改这里,Base URL 和 Key 不动。
3.7 ChatClient 集成 MCP 工具
package com.atguigu.study.config; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SaaLLMConfig { @Bean public ChatClient chatClient(ChatModel chatModel, ToolCallbackProvider tools) { return ChatClient.builder(chatModel) .defaultToolCallbacks(tools.getToolCallbacks()) .build(); } }3.8 测试控制器(对比启用/关闭 MCP)
package com.atguigu.study.controller; import jakarta.annotation.Resource; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController public class McpClientController { @Resource private ChatClient chatClient; @Resource private ChatModel chatModel; @GetMapping("/mcpclient/chat") public Flux<String> chat(@RequestParam(name = "msg", defaultValue = "北京") String msg) { System.out.println("【使用MCP工具调用】"); return chatClient.prompt(msg).stream().content(); } @GetMapping("/mcpclient/chat2") public Flux<String> chat2(@RequestParam(name = "msg", defaultValue = "北京") String msg) { System.out.println("【未使用MCP工具调用】"); return chatModel.stream(msg); } }配置片段到这里就齐了。服务端只暴露工具、不碰模型 Key;客户端一份 TaoToken 三件套 + MCP 连接地址。启动顺序是先 8014 再 8015,让客户端自动连上服务端加载工具。
4. 验证请求与成功结果:一次本地工具调用全链路
配置写完,最关键的是验证“模型真的调了本地工具”,而不是自己编答案。启动顺序:先起 MCP 服务端(8014),确认监听正常,再起客户端(8015)。客户端启动日志里会打印发现工具的信息,看到weatherTools或getWeatherByCity相关字样,说明工具已加载。
先测启用 MCP 的接口:
curl "http://localhost:8015/mcpclient/chat?msg=上海天气怎么样"预期结果:模型不会直接凭知识库回答,而是先触发工具调用,请求 8014 服务端的getWeatherByCity,拿到我们自定义的“多云,15℃~27℃,南风3级”这段数据,再组织成自然语言返回。返回内容里会包含我们写死在 Map 里的那段文本,这就是“本地工具被真正调用”的铁证。
再测未启用 MCP 的接口做对比:
curl "http://localhost:8015/mcpclient/chat2?msg=上海天气怎么样"这个接口走的是原生ChatModel,没有注入工具回调。模型只能用自身知识库回答,返回的是泛泛的天气描述,绝不会出现我们 Map 里那段自定义文本。两个接口一对比,MCP 的价值就直观了:一个能读本地数据,一个只能靠模型记忆。
如果你想更细地看调用过程,可以在服务端getWeatherByCity里加一行日志:
System.out.println("MCP工具被调用,入参 city=" + city);再次请求/mcpclient/chat,控制台出现这行日志,说明链路完全打通:客户端 → 模型决策 → MCP 协议 → 服务端工具执行 → 结果回传 → 模型生成回答。实测下来,这个链路里最容易出问题的不是代码,而是依赖冲突和配置地址,下一节专门排。
另外,如果你想把模型换成别的,比如验证不同模型对工具调用的支持,可以直接在模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 里先手动试一下工具描述是否清晰,再回到代码里改 Model ID。对长期跑编码和 Agent 任务的场景,Coding Plan 页面 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 里有更细的通道说明。
5. 本篇常见错误排查:401、连接失败与工具不触发
排错这块我按真实报错来对照,基本都是配置层的问题,代码本身很少出错。
报错一:401 Unauthorized / invalid api key。这是 TaoToken Key 没配对。检查三处:api-key是否用了环境变量且已 export;Key 是否是sk-开头且没多余空格;Base URL 是否写成 https://taotoken.net/api 而不是带/v1的地址。Spring AI 的 OpenAI starter 会自己拼/v1/chat/completions,你多写一层就 404 或 401。三件套对齐:Base URL、Key、Model ID,缺一不可。
报错二:local proxy failed / connection refused。客户端连不上 MCP 服务端。先确认 8014 是否真的起来了,curl http://localhost:8014看有没有响应。再检查客户端sse.connections.mcp-server1.url是否写对端口。最常见的原因是服务端误引了spring-boot-starter-web,Tomcat 把端口占了,WebFlux 的 MCP 服务没起来,表面看进程在,实际工具通道是死的。删掉 web 依赖重启即可。
报错三:reading choices 相关解析异常。通常是模型返回格式和客户端预期不一致,或者 Model ID 写错导致通道返回了非预期内容。先确认model字段是 TaoToken 支持的模型名,再确认 Base URL 没写错。如果换了模型后突然报这个,八成是 Model ID 拼错。
报错四:模型不调用工具,直接自己回答。检查@Tool的description是否清晰,模型靠描述判断要不要调。描述太模糊,模型就忽略工具。再确认客户端是否真的注入了ToolCallbackProvider,defaultToolCallbacks(tools.getToolCallbacks())这行不能少。最后确认 MCP 客户端配置里toolcallback.enabled=true。
报错五:工具参数匹配失败。工具方法参数名和类型要和大模型推断的一致。简单字符串参数最稳,复杂对象容易匹配不上。如果一定要传对象,把参数拆成多个基础类型字段。
报错六:OAuth / 鉴权相关提示。如果你在别的工具里见过 OAuth 报错,本质是鉴权头没带对。Spring AI 这边走的是Authorization: Bearer <key>,由 starter 自动加,你只要保证api-key正确即可。别手动去拼鉴权头,容易重复或格式错。
排错顺序建议:先确认模型通道通(单独调一次模型对话),再确认 MCP 服务端通(curl 端口),最后确认工具注册通(看启动日志)。三层分开查,比一上来就翻代码快得多。
6. 把统一 Key 接入沉淀成可复用工程习惯
走到这里,链路已经跑通了。最后说几个我踩过坑之后沉淀下来的习惯,能让这套方案在真实项目里更稳。
第一,把 TaoToken 的三件套抽成环境变量或配置中心,别写死在 yml。TAOTOKEN_API_KEY、TAOTOKEN_BASE_URL、TAOTOKEN_MODEL三个变量,本地用.env,线上用配置中心。换模型时只改TAOTOKEN_MODEL,代码零改动。
第二,MCP 服务端和客户端分仓或分模块。服务端只依赖 webflux,客户端才依赖 web。用 Maven 多模块把依赖边界卡死,避免有人手滑把 web 引进服务端。这个坑我见过不止一次,排查起来很费时间。
第三,工具描述当成接口文档来写。@Tool(description = ...)里的文字直接决定模型会不会调、调得对不对。把参数含义、返回格式、适用场景写清楚,比事后调 prompt 有效得多。
第四,验证阶段保留/mcpclient/chat2这个无工具接口。它是你的对照组,任何时候怀疑“模型是不是没调工具”,跑一下对比就知道。等业务稳定了再决定要不要删。
第五,扩展工具时按业务域拆类。天气一个 Service、订单一个 Service、库存一个 Service,每个类里用@Tool标方法,注册时用MethodToolCallbackProvider.builder().toolObjects(...)把多个对象一起传进去。这样工具多了也不会乱。
这套结构跑顺之后,往数据库查询、内部接口调用、文件处理上扩都很自然——本地方法加个@Tool,注册进去,模型就能用。真正花时间的从来不是写工具,而是把 Key 和鉴权收敛干净。统一通道 + 一份配置 + 清晰的三件套,后面加多少工具都不慌。