拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringAI(GA):Nacos2下的分布式MCP,TaoToken统一Key接入实践

SpringAI(GA):Nacos2下的分布式MCP,TaoToken统一Key接入实践

1. 分布式 MCP 的真实痛点:多节点下 Key 满天飞

先说结论:SpringAI 1.0.0 GA 之后,MCP 的接入方式已经稳定,但真正让人头疼的不是协议本身,而是多节点部署后模型调用凭证的分散管理。我在一个 3 节点的 MCP Server 集群里踩过这个坑——每个节点各自读一份application.yml,里面写着不同的api-key,改一次 Key 要滚动重启整个集群,鉴权逻辑还各写各的。

这个场景其实很典型:你有一个 MCP Server 集群(比如 21000、21001 两个实例),对外统一暴露为webflux-mcp-server,Nacos2 负责服务注册与发现,MCP Client 通过 Nacos 拿到实例列表后做负载均衡。问题出在 Client 侧——它要调用大模型来驱动工具调用(Tool Calling),而大模型的 endpoint 和 API Key 如果散落在每个 Client 节点、每个 Server 节点,就会出现三个麻烦:

第一,凭证轮换成本高。Key 泄露要换,你得登录每一台机器改配置。第二,鉴权口径不统一。有的节点走 OpenAI 兼容协议,有的走 DashScope 原生 SDK,返回格式和错误码都不一样。第三,调试困难。Client 报 401,你根本不知道是哪个节点的 Key 失效了。

我试过的最笨的办法是写个配置中心同步脚本,把 Key 推到 Nacos 配置里,各节点监听变更。但这只是把问题从"改文件"变成"改配置中心",鉴权逻辑还是散的。真正干净的解法是:把所有模型调用收敛到一个统一通道,Client 和 Server 都不再持有真实 Key,只认一个 Base URL + 一个统一 Key。这就是本文要落地的方案——用 TaoToken 作为统一模型通道,配合 Nacos2 做分布式 MCP 的服务发现。

适合谁看:已经在用 SpringBoot 3.4.x + SpringAI 1.0.0 GA 搭 MCP 服务,且节点数超过 1 个的团队;或者正准备把单机 MCP 改造成分布式、但不想在鉴权上重复造轮子的开发者。下面我按"环境准备 → Server 端配置 → Client 端配置 → 连通性验证 → 排障"的顺序,给出可直接复制的片段。

2. TaoToken 前置:统一 Key 通道的接入准备

在动手改配置之前,先把 TaoToken 这条通道准备好。它的定位很简单:一个 OpenAI 兼容的模型网关,你拿到的是一组 Base URL + API Key,Client 侧按标准 OpenAI 协议调用即可,不需要为每个模型厂商单独适配 SDK。对分布式 MCP 来说,这意味着所有节点的base-url和api-key可以完全一致,鉴权逻辑收敛成一份。

具体操作路径:打开官网 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=console&utm_campaign=rewrite 创建 API Key。注意 Key 只在创建时完整显示一次,复制后存到你的密码管理器里。然后在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 可以随时查看和吊销。

这里有个关键点要提前说清楚:TaoToken 的 API 入口是 https://taotoken.net/api,注意这个地址不带任何查询参数,是纯粹的 API 端点。你在 SpringAI 的base-url里填的就是它。模型 ID 方面,常用的qwen-max、gpt-4o这类都可以在模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 先手动验证一下能不能通,确认模型可用再写进配置。

为什么要在分布式场景下强调这一步?因为 MCP Client 触发 Tool Calling 时,模型返回的是结构化的工具调用指令(tool_calls),如果通道不稳定或模型不支持 function calling,你会看到reading choices之类的解析错误。先在对话页面确认模型能正常返回 tool_calls,再去配 Client,能省掉一半排障时间。

另外,如果你的 MCP 服务是长期跑在后台、需要持续调用模型的(比如 Agent 场景),建议看一下 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它的计费方式对高频调用更友好。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的 OpenAI 兼容协议说明,包括/v1/chat/completions的请求体和响应体格式,配 Client 时对照着看。

环境版本我锁定为:JDK21 + SpringBoot 3.4.5 + SpringAI 1.0.0 + SpringAI Alibaba 1.0.0.3-SNAPSHOT。注意 1.0.0.2 版本有个已知 bug,不支持填写 Nacos 命名空间 ID,所以必须升到 1.0.0.3-SNAPSHOT。Nacos 用 2.x 版本,新建一个命名空间,记下命名空间 ID,后面 Server 和 Client 都要填。

3. 可复制配置:Server 端与 Client 端的完整片段

这一节是全文的核心,给出可以直接粘贴的配置。先看 Server 端的pom.xml依赖部分:

<properties> <spring-ai-alibaba.version>1.0.0.3-SNAPSHOT</spring-ai-alibaba.version> </properties> <dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-nacos2-mcp-server</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webflux</artifactId> </dependency> </dependencies>

Server 端的application.yml,重点是spring.ai.alibaba.mcp.nacos下的注册配置:

server: port: 21000 spring: main: banner-mode: off application: name: mcp-nacos2-server ai: mcp: server: name: webflux-mcp-server version: 1.0.0 type: ASYNC instructions: "This reactive server provides time information tools and resources" sse-message-endpoint: /mcp/messages capabilities: tool: true resource: true prompt: true completion: true alibaba: mcp: nacos: enabled: true server-addr: 127.0.0.1:8848 username: nacos password: nacos registry: enabled: true service-namespace: 9ba5f1aa-b37d-493b-9057-72918a40ef35 service-group: mcp-server

注意service-namespace填的就是你在 Nacos 新建的命名空间 ID。Server 端本身不直接调模型,所以这里没有api-key配置——模型调用发生在 Client 侧。工具服务用一个TimeService演示:

@Service public class TimeService { private static final Logger logger = LoggerFactory.getLogger(TimeService.class); @Tool(description = "Get the time of a specified city.") public String getCityTimeMethod(@ToolParam(description = "Time zone id, such as Asia/Shanghai") String timeZoneId) { logger.info("The current time zone is {}", timeZoneId); return String.format("The current time zone is %s and the current time is %s", timeZoneId, getTimeByZoneId(timeZoneId)); } private String getTimeByZoneId(String zoneId) { ZoneId zid = ZoneId.of(zoneId); ZonedDateTime zonedDateTime = ZonedDateTime.now(zid); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); return zonedDateTime.format(formatter); } }

启动类里注册ToolCallbackProvider:

@SpringBootApplication public class Nacos2ServerApplication { public static void main(String[] args) { SpringApplication.run(Nacos2ServerApplication.class, args); } @Bean public ToolCallbackProvider timeTools(TimeService timeService) { return MethodToolCallbackProvider.builder().toolObjects(timeService).build(); } }

Client 端的pom.xml需要额外引入 OpenAI 自动配置和 chat-client:

<dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-openai</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-autoconfigure-model-chat-client</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-nacos2-mcp-client</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-client-webflux</artifactId> </dependency> </dependencies>

Client 端的application.yml是统一 Key 的落点,base-url和api-key都指向 TaoToken:

server: port: 121100 spring: application: name: mcp-nacos2-client main: web-application-type: none ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api chat: options: model: qwen-max mcp: client: enabled: true name: my-mcp-client version: 1.0.0 request-timeout: 30s type: ASYNC nacos-enabled: true alibaba: mcp: nacos: enabled: true server-addr: 127.0.0.1:8848 username: nacos password: nacos registry: service-namespace: 9ba5f1aa-b37d-493b-9057-72918a40ef35 service-group: mcp-server client: sse: connections: server1: webflux-mcp-server

这里api-key用环境变量${TAOTOKEN_API_KEY}注入,避免明文写进仓库。base-url固定为https://taotoken.net/api,model填qwen-max。Client 启动类需要排除Nacos2DynamicMcpServerAutoConfiguration,因为我们这里没有第三方 RESTful 服务要动态加载:

@SpringBootApplication(exclude = Nacos2DynamicMcpServerAutoConfiguration.class) public class Nacos2ClientApplication { public static void main(String[] args) { SpringApplication.run(Nacos2ClientApplication.class, args); } @Bean public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, @Qualifier("loadbalancedMcpAsyncToolCallbacks") ToolCallbackProvider tools, ConfigurableApplicationContext context) { return args -> { var chatClient = chatClientBuilder.defaultToolCallbacks(tools).build(); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("\n>>> QUESTION: "); String userInput = scanner.nextLine(); if (userInput.equalsIgnoreCase("exit")) break; if (userInput.isEmpty()) userInput = "北京时间现在几点钟"; System.out.println("\n>>> ASSISTANT: " + chatClient.prompt(userInput).call().content()); } scanner.close(); context.close(); }; } }

三件套对照表,方便你检查有没有漏:

配置项Server 端Client 端
Base URL不涉及https://taotoken.net/api
API Key不涉及${TAOTOKEN_API_KEY}
Model ID不涉及qwen-max
Nacos 命名空间9ba5f1aa-...9ba5f1aa-...
服务组mcp-servermcp-server

4. 验证请求:从 Nacos 注册到工具调用成功

配置写完,按顺序启动验证。第一步启动 Nacos2,确认 8848 端口可访问。第二步启动 Server 端,用-Dserver.port=21000和-Dserver.port=21001分别起两个实例,两个实例的spring.application.name都是mcp-nacos2-server,注册到 Nacos 后对外统一暴露为webflux-mcp-server。

打开 Nacos 控制台,在服务列表里应该能看到webflux-mcp-server,点进去能看到两个实例,端口分别是 21000 和 21001。同时在配置管理里能找到 MCP Server 和 Tool 的配置信息——这是 SpringAI Alibaba 自动写入的,包含工具名称、参数 schema 等元数据。这一步确认了服务发现是通的。

第三步启动 Client 端。启动日志里会打印从 Nacos 拉取到的 MCP Server 实例列表,以及loadbalancedMcpAsyncToolCallbacks的初始化信息。如果看到reading choices相关的报错,说明模型通道有问题,先回到 TaoToken 对话页面确认qwen-max能正常返回 tool_calls。

Client 启动后进入交互模式,输入"北京时间现在几点钟",观察日志。第一次工具请求应该由 21000 端口的 Server 处理,Server 日志里会打印The current time zone is Asia/Shanghai。再输入一次同样的问题,第二次请求应该落到 21001 端口——这就是 Nacos 负载均衡在起作用。

验证成功的标志有三个:Client 控制台返回了格式化的时间字符串;两个 Server 实例的日志里各出现了一次工具调用记录;Nacos 控制台的服务实例健康状态都是 UP。如果只看到一个实例被调用,检查 Client 的connections配置里server1: webflux-mcp-server是否写对,以及 Nacos 的service-group是否和 Server 端一致。

这里补充一个实测细节:request-timeout: 30s这个值在工具调用链较长时可能不够。如果你的 MCP Server 工具涉及外部 API 调用,建议调到60s,否则会看到TimeoutException。另外type: ASYNC是 WebFlux 场景的推荐值,如果你用的是 WebMvc,改成SYNC。

5. 本篇常见错排查:401、local proxy failed 与 reading choices

排障部分按报错类型对照,这些都是我在联调时真实遇到的。

401 Unauthorized。最常见的原因是TAOTOKEN_API_KEY环境变量没注入成功。检查方式:在 Client 启动日志里搜索api-key,确认它读到的不是空值。另一个原因是 Key 被吊销了,去 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 确认状态。还有一种隐蔽情况:base-url末尾多写了/v1,导致请求路径变成https://taotoken.net/api/v1/v1/chat/completions。正确写法就是https://taotoken.net/api,不要加/v1。

local proxy failed。这个报错通常出现在 Client 尝试连接 MCP Server 时。根因是 Nacos 返回的实例地址 Client 访问不到。检查 Nacos 控制台里实例的 IP 是不是127.0.0.1——如果 Server 和 Client 不在同一台机器,注册的 IP 必须是可达的内网地址。解决方式是在 Server 端显式指定spring.cloud.nacos.discovery.ip。另外确认service-namespace两边填的是同一个命名空间 ID,填错会导致 Client 在 public 命名空间里找不到服务。

reading choices 解析失败。这个报错来自 OpenAI 兼容层的响应解析。原因通常是模型返回的tool_calls结构不符合预期,或者模型本身不支持 function calling。先在模型对话页面 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 用同样的 prompt 测试,确认返回体里有tool_calls字段。如果模型不支持,换qwen-max或gpt-4o。还有一种情况是spring-ai-autoconfigure-model-openai版本和 SpringAI 核心版本不匹配,检查pom.xml里有没有显式指定版本号导致冲突。

OAuth 相关报错。如果你在 Client 配置里误加了 OAuth 相关的client-registration配置,会看到OAuth2初始化失败。MCP 的 Nacos2 接入不需要 OAuth,删掉相关配置即可。另外Nacos2DynamicMcpServerAutoConfiguration如果没有排除,会尝试加载动态 MCP Server 配置,在纯 Client 场景下会报 Bean 创建失败。

工具调用返回空。Client 收到了模型响应,但工具没有被触发。检查defaultToolCallbacks(tools)是否真的注入了loadbalancedMcpAsyncToolCallbacks。如果@Qualifier写错,Spring 会注入一个空的ToolCallbackProvider,模型就看不到任何工具。在启动日志里搜索ToolCallbackProvider,确认注册的工具数量大于 0。

6. 把统一 Key 通道固化到你的 MCP 工程里

走到这一步,你的分布式 MCP 应该已经能跑通了:Nacos2 负责服务发现,两个 Server 实例对外统一暴露,Client 通过 TaoToken 统一通道调用模型,所有节点的base-url和api-key完全一致。后续要做的就是把这份配置固化下来——把TAOTOKEN_API_KEY写进 CI/CD 的 secret 管理,把base-url和model写进团队的基础配置模板,新节点接入时直接复用。

如果你还在单机阶段,建议现在就把base-url指向 TaoToken,而不是直连某个厂商的 endpoint。这样等节点数涨到 3 个、5 个的时候,你不需要改任何鉴权逻辑,只需要在 Nacos 里多注册几个实例。接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 里有完整的协议说明,配 Client 时对照着看能少走弯路。长期跑 Agent 类任务的话,Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 的计费方式更适合高频调用场景。

返回列表