1. Spring AI MCP 工具动态更新到底解决什么问题
如果你正在用 Spring AI 搭智能体,大概率遇到过这个场景:工具类写好了,@Tool注解也标了,但每次加一个新工具就得重启服务。开发阶段还能忍,生产环境重启一次就是一次抖动。MCP(Model Context Protocol)在 Spring AI 里的动态工具更新能力,就是冲着这个痛点来的——服务端不重启,客户端能感知,模型下一轮对话立刻用上新工具。
先把概念对齐。MCP 是一套标准化接口,让 AI 应用和智能体能够访问外部工具、获取资源、使用提示模板。它遵循 client-server 架构:MCP Server 暴露工具、资源和提示;MCP Client 连接 Server 并消费这些能力;AI 模型通过 Client 与外部世界交互。Spring AI 提供了完整的 MCP 实现,包含客户端和服务端组件。
动态工具更新的价值在于三点:Server 无需重启即可增删工具;Client 能检测到变更;模型可以立即使用新功能。这三点串起来,才叫真正的"热更新"。
但实际落地时,很多人卡在另一个地方:模型 Key 和 MCP 端点散落在各个配置文件里,测试环境一套、生产环境一套,换个模型要改好几个地方。这篇就聚焦这个组合场景——把 MCP 配置统一改到 TaoToken,同时把工具动态注册和调用链路自检跑通。适合谁?需要统一管理多模型 Key 与 MCP 服务端点、又想让工具热更新真正可用的 Spring AI 开发者。
我试过把 Server 和 Client 拆成两个独立进程来验证动态更新,这样能清楚看到"通知"到底有没有生效。下面按可复制的顺序来。
2. TaoToken 前置准备与 MCP 端点统一管理
在写配置之前,先把"Key 和端点从哪来"这件事定下来。TaoToken 在这里扮演的角色是统一的模型接入层:你拿到一个 Base URL 和一个 API Key,就能在 Spring AI 里指向它,不用为每个模型单独维护一套凭证。
第一步,去官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并进入控制台。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_console&utm_campaign=rewrite ,在里面创建 API Key。创建完先别关页面,Key 只完整显示一次,复制到安全的地方。
第二步,确认你要用的模型 ID。不同模型在请求里用的 model 字段不一样,这个值后面要写进application.yml。可以在模型对话页面 https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_chat&utm_campaign=rewrite 里先手动发一条消息,确认模型能正常响应,顺便把返回里的 model 名称记下来。
第三步,理解 API 端点。TaoToken 的 API 地址是 https://taotoken.net/api ,注意这个地址不带 UTM 参数,直接作为 Spring AI 的 base-url 使用。Spring AI 的 OpenAI 兼容客户端会自动在 base-url 后面拼接/v1/chat/completions这类路径,所以配置里只写到/api即可。
这里有个容易踩的坑:MCP Server 和 MCP Client 是两套配置。Server 负责暴露工具,Client 负责连接 Server 并把工具喂给模型。TaoToken 的 Key 是给"模型调用"用的,不是给 MCP 传输用的。也就是说,MCP 的 SSE 连接走的是你自己的 localhost 或内网地址,而模型推理走的是 TaoToken 的 API。这两条链路要分清,否则会出现"工具注册成功了但模型调不动"的假象。
如果你打算长期跑编码类 Agent,可以顺带看下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_plan&utm_campaign=rewrite ,它更适合高频调用的场景。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_doc&utm_campaign=rewrite ,API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_keys&utm_campaign=rewrite 。这几个地址先存着,后面排障会用到。
3. 可复制的 application.yml 与 MCP 客户端配置
这一节是全文的核心,配置片段可以直接抄。先明确版本:MCP Server 用1.0.0,MCP Client 用1.0.0-M7。为什么 Client 用 M7?因为部分模型服务与1.0.0的 Client 存在兼容问题,M7 更稳。这个版本差异是实测出来的,不是随便选的。
先看依赖。Server 侧:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> <version>1.0.0</version> </dependency>Client 侧:
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client</artifactId> <version>1.0.0-M7</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> <version>1.0.0-M7</version> </dependency>然后是 Server 的application.yml,这里把 MCP Server 打开,并指向 TaoToken 作为模型后端:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: your-model-id mcp: server: enabled: true name: ai_mcp_server version: 1.0.0 type: SYNC tool-change-notification: true注意tool-change-notification: true这一行。它控制工具变更后是否自动通知客户端。默认行为就是通知,但显式写出来更清楚。如果你在调试阶段想手动控制刷新时机,可以临时设为false,等确认链路通了再打开。
Client 的application.yml:
spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: your-model-id mcp: client: name: ai-mcp-client initialized: true type: ASYNC sse: connections: server1: url: http://localhost:8888这里type: ASYNC是关键。同步模式下,工具变更通知的处理会阻塞主线程;异步模式才能让 Client 在后台接收通知并更新工具列表。sse.connections.server1.url指向你的 MCP Server 地址,本地调试就是http://localhost:8888。
把 Key 放进环境变量TAOTOKEN_API_KEY,不要硬编码在 yml 里。启动前 export 一下:
export TAOTOKEN_API_KEY=sk-你的key如果你用的是 IDE 运行,在 Run Configuration 里加环境变量即可。这一步做完,配置层面就齐了。
4. 工具动态注册与调用链路验证
配置写完,接下来验证"动态"两个字是不是真的生效。先写两个工具类。一个是启动时自动注册的:
@Component public class WeatherService { @Tool(description = "获取当前天气预报") WeatherResponse getCurrentWeather(WeatherRequest request) { // 实际实现 } @Tool(description = "获取IP地址详细信息") String getIpAddressInfo(String ip) { // 实际实现 } }另一个是需要动态管理的:
@Service public class MathService { @Tool(name = "sum", description = "计算2个数的和") public int sum(int a, int b) { return a + b + 1000; } @Tool(name = "sub", description = "计算2个数的差值") public int sub(int a, int b) { return a - b - 1000; } }注意sum和sub里故意加了+1000和-1000,这是为了在验证时能一眼看出模型到底调没调这个工具——如果返回结果带偏移量,说明工具真的被执行了。
注册自动工具:
@Configuration public class McpToolConfig { @Bean ToolCallbackProvider weatherTools(WeatherService weatherService) { return MethodToolCallbackProvider.builder() .toolObjects(weatherService) .build(); } }动态增删的 Controller:
@RestController @RequestMapping("/mcp") public class McpToolController { private final McpSyncServer mcpSyncServer; private final MathService mathService; public McpToolController(McpSyncServer mcpSyncServer, MathService mathService) { this.mcpSyncServer = mcpSyncServer; this.mathService = mathService; } @GetMapping("/add") public ResponseEntity<?> addTool() { List<SyncToolSpecification> newTools = McpToolUtils .toSyncToolSpecifications(ToolCallbacks.from(this.mathService)); for (SyncToolSpecification newTool : newTools) { this.mcpSyncServer.addTool(newTool); } return ResponseEntity.ok("添加成功"); } @GetMapping("/remove") public ResponseEntity<?> removeTool(String toolName) { this.mcpSyncServer.removeTool(toolName); return ResponseEntity.ok("删除工具【" + toolName + "】成功"); } }启动 Server 后,先访问http://localhost:8888/mcp/add,再访问http://localhost:8888/mcp/remove?toolName=sum。每次操作后,Client 侧应该自动收到通知并更新工具列表。
验证调用链路,写一个测试接口:
@RestController @RequestMapping("/tools") public class ToolController { private final ChatClient chatClient; public ToolController(ChatClient.Builder aiClientBuilder, ToolCallbackProvider mcpTools) { this.chatClient = aiClientBuilder.defaultTools(mcpTools).build(); } @GetMapping("/sum") public ResponseEntity<String> calcSum(String prompt) { String response = this.chatClient.prompt(prompt).call().content(); return ResponseEntity.ok(response); } }调用http://localhost:8080/tools/sum?prompt=帮我算一下 3 加 5。如果返回结果里出现 1008(3+5+1000),说明工具被正确调用且动态注册生效。如果返回 8,说明模型没走工具,直接心算了——这时候要回去检查 Client 的initialized和type配置。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
动态更新跑不通,报错通常集中在几个地方。下面按真实报错对照排查。
401 Unauthorized。这个最常见,八成是 Key 的问题。检查三处:环境变量TAOTOKEN_API_KEY是否真的 export 了;application.yml里api-key的占位符拼写是否一致;Key 是否在控制台被删除或过期。如果 Key 没问题,检查base-url是否写成了https://taotoken.net/api,多一个斜杠或少一个/api都会导致 401。
local proxy failed。这个报错通常出现在 Client 连接 Server 的阶段。检查sse.connections.server1.url是否可达,本地调试确认 Server 真的在 8888 端口监听。如果 Server 和 Client 在不同机器,把 localhost 换成实际 IP。另外确认没有多余的代理配置干扰本地回环地址。
reading choices 相关报错。这类错误一般出现在模型响应解析阶段,说明请求发出去了但返回结构不符合预期。检查model字段是否填了正确的模型 ID,以及该模型是否支持工具调用(function calling)。不是所有模型都支持工具调用,选一个明确支持 tool use 的模型。
OAuth 相关报错。如果你在配置里误加了 OAuth 认证流程,而 TaoToken 的 API Key 走的是 Bearer Token 方式,就会冲突。检查application.yml里有没有多余的oauth2配置块,有的话删掉。API Key 认证只需要api-key一个字段。
还有一个隐蔽的坑:MCP Client 的initialized: true如果没写,Client 启动后不会主动拉取工具列表,表现就是"Server 明明有工具,Client 一个都看不到"。这个字段默认值在不同版本里行为不一致,显式写true最保险。
排查顺序建议:先确认 401(Key 和 base-url),再确认 local proxy failed(网络连通性),最后看工具列表是否为空(initialized 和 type)。按这个顺序走,大部分问题能在五分钟内定位。
6. 把配置沉淀下来,让工具热更新真正可用
走到这里,你应该已经跑通了"Server 动态增删工具 → Client 自动感知 → 模型调用新工具"这条完整链路。回头看,真正让这套东西可用的不是某一个配置项,而是把模型接入层和 MCP 传输层分清楚了:TaoToken 负责模型推理的 Key 和端点统一,MCP 的 SSE 连接负责工具传输,两者各司其职。
实际项目里,我建议把tool-change-notification保持为true,让通知自动化。只有在需要精确控制刷新时机(比如批量增删工具时避免频繁通知)才临时关掉。另外,MathService那种带偏移量的返回值设计,在联调阶段非常有用,上线前记得改回正常逻辑。
如果你还要接 Claude Code 这类工具,配置三件套要写全:Base URL 填https://taotoken.net/api,Key 填控制台创建的 API Key,Model ID 填你验证过的模型名。这三项缺一个都连不上。Claude Code 的接入说明在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=mcp_springai_cc&utm_campaign=rewrite 里有详细步骤。
最后留一个实用技巧:把 Server 和 Client 的启动日志级别调到 DEBUG,能看到工具注册和通知的完整过程。logging.level.org.springframework.ai.mcp=DEBUG这一行加进 yml,排查动态更新问题时比猜快得多。