1. 为什么单机 MCP Server 撑不住企业场景
很多同学第一次接触 Spring AI Alibaba 的 MCP 能力时,都是在本机跑一个 MCP Server,然后在 Agent 里配一个固定地址就完事了。这种玩法在 Demo 阶段没问题,但一旦放到企业内网,问题马上就暴露出来:MCP Server 只部署了一个实例,Agent 的所有工具调用都压在这一个进程上;某天运维要做滚动发布,Server 重启的几十秒里 Agent 直接报连接失败;再往后业务方想给订票工具加一个新参数,你得挨个通知所有 Agent 团队改配置重启。
这些问题的本质,是 MCP 协议本身只解决了「Agent 和工具之间怎么通信」,它没有规定「Agent 怎么找到工具」。而企业级部署里,服务实例是动态的、地址是会变的、工具元数据是会演进的,这就需要一个注册中心来兜底。Spring AI Alibaba 给出的答案是把 Nacos 拉进来:MCP Server 启动时把自己的 IP、端口、工具列表注册到 Nacos,MCP Client 订阅这个服务,实例上下线、工具增删都能实时感知,调用时再叠加负载均衡。
这套方案适合谁?如果你正在用 Spring Boot 做企业内部系统,想把已有的订单、库存、工单这类业务能力包装成 AI Agent 能调用的工具,或者你已经在写 Agent 但被多实例部署和动态更新卡住了,那这篇内容就是给你准备的。下面我会从 Nacos 准备开始,把 MCP Server 注册、MCP Client 发现、多实例验证、常见报错排查整条链路走一遍,配置和代码都可以直接复制。
需要说明的是,MCP 分布式部署解决的是「服务发现 + 负载均衡 + 元数据动态更新」这三件事,它不改变 MCP 工具本身的编写方式,你原来用@Tool注解写的业务方法,注册到 Nacos 之后照样能用,只是调用方从「直连某个 IP」变成了「通过服务名调用」。
2. Nacos 与依赖准备:命名空间和 starter 怎么选
在动手写代码之前,先把注册中心这一层准备好。Nacos 的安装这里不展开,假设你已经有一个可访问的 Nacos Server,版本上 Spring AI Alibaba 目前同时兼容 Nacos 2 和 Nacos 3,我用的是本地127.0.0.1:8848。
第一步是给 MCP 服务单独建一个命名空间。为什么要单独建?因为企业里 Nacos 往往还注册着大量微服务,MCP 的工具实例和普通微服务混在一起,排查问题时列表会非常乱,而且命名空间隔离后,Agent 订阅的范围也更干净。登录 Nacos 控制台,在「命名空间」里新建一个,名字可以叫nacos-default-mcp,创建完成后记下它的命名空间 ID,类似9ba5f1aa-b37d-493b-9057-72918a40ef35这样一串 UUID,后面 Server 和 Client 的配置都要填这个 ID。
接下来是依赖选择,这是很多人第一次踩坑的地方。Spring AI Alibaba 把 MCP 的 Nacos 能力拆成了两个 starter:
| starter 名称 | 作用 | 用在哪个应用 |
|---|---|---|
| spring-ai-alibaba-starter-nacos-mcp-server | 把 MCP 工具注册到 Nacos | MCP Server 应用 |
| spring-ai-alibaba-starter-nacos-mcp-client | 从 Nacos 发现并负载均衡调用 MCP | Agent / MCP Client 应用 |
注意这两个 starter 不要同时往一个应用里塞,除非你确实要做一个既提供工具又消费工具的混合应用,那种情况要额外注意 Bean 冲突。版本方面,我用的组合是 Spring AI1.0.0-M8配 Spring AI Alibaba1.0.0-M8.1-SNAPSHOT,这两个版本号要对齐,M 系列版本之间 API 变动比较频繁,混用很容易出现类找不到的问题。
Server 端的pom.xml关键依赖如下,注意properties里把两个版本号抽出来,方便统一管理:
<properties> <spring-ai.version>1.0.0-M8</spring-ai.version> <ai-alibaba.version>1.0.0-M8.1-SNAPSHOT</ai-alibaba.version> </properties> <dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter-nacos-mcp-server</artifactId> <version>${ai-alibaba.version}</version> </dependency> </dependencies>Client 端只需要把 artifactId 换成spring-ai-alibaba-starter-nacos-mcp-client,其余结构一致。这里有个细节:SNAPSHOT 版本需要你的 Maven 配置里能拉到 Spring 的 snapshot 仓库,如果公司内网有私服,记得让运维把对应仓库代理配上,否则会卡在依赖下载。
提示:命名空间 ID 一定要复制完整,Nacos 控制台上显示的是名称,但配置里填的是 ID,填错名称会导致注册到一个不存在的命名空间,表现为「Server 启动没报错,但控制台看不到实例」。
3. MCP Server 配置:把工具实例注册进 Nacos
这一节是整篇的核心,配置写对了,后面基本就顺了。先看 Server 端的application.yml,我把它拆成三段来理解:基础服务信息、MCP 元数据、Nacos 注册。
server: port: ${SERVER_PORT:19000} spring: application: name: mcp-server-provider main: banner-mode: off ai: mcp: server: name: mcp-server-provider version: 1.0.1 sse-message-endpoint: /mcp/messages_ type: SYNC alibaba: mcp: nacos: enabled: true server-addr: 127.0.0.1:8848 username: nacos password: nacos registry: service-namespace: 9ba5f1aa-b37d-493b-9057-72918a40ef35 logging: level: io: modelcontextprotocol: client: DEBUG spec: DEBUG server: DEBUG逐项说明几个关键点。server.port用了${SERVER_PORT:19000}这种写法,是为了后面演示多实例时能通过环境变量改端口,本地默认 19000。spring.ai.mcp.server.name和spring.application.name建议保持一致,都叫mcp-server-provider,这个名称就是 Client 端订阅时用的服务名,两边必须对得上。
type: SYNC表示这是一个同步类型的 MCP Server,对应 Client 端也要用同步的LoadbalancedMcpSyncClient,同步异步不能混。sse-message-endpoint是 SSE 传输的消息端点,保持默认即可。
Nacos 部分,enabled: true打开注册开关,server-addr指向你的 Nacos 地址,service-namespace填前面记下的命名空间 ID。用户名密码按你 Nacos 的实际配置填,如果没开鉴权可以留空,但生产环境强烈建议开启。
工具的定义方式和你平时写 MCP 工具完全一样,用@Tool和@ToolParam注解即可。比如一个查询订单的工具:
@Tool(description = "获取指定订单号的订单详情") public Order getOrder(@ToolParam(description = "订单号") String orderId) { return restTemplate.getForObject("http://order-service/order?id=" + orderId, Order.class); }这里restTemplate如果加了@LoadBalanced,它自己也能通过 Spring Cloud Alibaba 的服务发现去调后端微服务,这样 MCP Server 就变成了一个「代理层」:对 Agent 暴露 MCP 工具,对后端转发 HTTP 或 Dubbo 请求。这种模式特别适合存量系统改造,你不需要动原来的订单服务,只要新写一个 MCP Server 应用做适配就行。
启动这个 Server,如果日志里出现向 Nacos 注册成功的记录,并且没有抛异常,就说明注册这一步通了。接下来去 Nacos 控制台,切到nacos-default-mcp命名空间,在「服务列表」里应该能看到mcp-server-provider,点进去能看到实例的 IP 和端口。再切到 MCP 相关的元数据视图,能看到这个 Server 注册上来的工具列表,包括工具名、参数、描述。这一步验证通过,Server 端就算完成了。
4. MCP Client 配置:订阅服务并负载均衡调用
Server 注册好了,现在写 Agent 端。Client 的application.yml比 Server 多了一块「要订阅哪个服务」的配置,这是分布式调用的关键。
server: port: 8080 spring: application: name: mcp-client-webflux ai: openai: api-key: ${DASHSCOPE_API_KEY} base-url: https://dashscope.aliyuncs.com/compatible-mode chat: options: model: qwen-max alibaba: mcp: nacos: enabled: true service-namespace: 9ba5f1aa-b37d-493b-9057-72918a40ef35 server-addr: 127.0.0.1:8848 username: nacos password: nacos client: sse: connections: server1: mcp-server-provider mcp: client: enabled: true name: mcp-client-webflux version: 0.0.1 initialized: true request-timeout: 600s nacos-enabled: true type: sync toolcallback: enabled: true root-change-notification: true logging: level: io: modelcontextprotocol: client: DEBUG spec: DEBUG重点看spring.ai.alibaba.mcp.nacos.client.sse.connections这一段,server1是你给这个连接起的别名,mcp-server-provider是要订阅的 MCP Server 服务名。Client 只会订阅这里列出的服务,没列的服务即使注册在同一个命名空间也不会被拉进来,这样能避免 Agent 误调用到不相关的工具。
root-change-notification: true这个开关建议打开,它让 Client 能感知到工具元数据的变化,比如 Server 端新增了一个工具、改了参数描述,Client 不用重启就能拿到最新的工具定义。request-timeout: 600s是调用超时,AI Agent 场景下工具执行可能比较慢,设长一点避免误超时。
Client 端拿到工具的方式有两种。第一种是直接注入负载均衡的 MCP Client:
@Autowired private List<LoadbalancedMcpSyncClient> mcpClients;第二种,也是更常用的,是注入ToolCallbackProvider,把工具回调直接喂给 ChatClient:
@Autowired private LoadbalancedSyncMcpToolCallbackProvider toolCallbackProvider; ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();拿到toolCallbacks之后,挂到 ChatClient 上,Agent 就能在对话中自动决定调用哪个工具。整个链路是这样的:Agent 收到用户问题 → 模型判断需要调用工具 → 通过LoadbalancedMcpSyncClient发起调用 → Client 从 Nacos 拿到mcp-server-provider的实例列表 → 按负载均衡策略选一个实例 → 通过 SSE 把请求发过去 → Server 执行@Tool方法返回结果。
这里有个容易忽略的点:LoadbalancedSyncMcpToolCallbackProvider的 Bean 名称是固定的loadbalancedSyncMcpToolCallbacks,如果你用@Qualifier注入,名字别写错。异步版本对应LoadbalancedAsyncMcpToolCallbackProvider,Bean 名是loadbalancedMcpAsyncToolCallbacks。
5. 多实例验证与常见报错排查
配置写完了,怎么确认分布式真的生效了?最直接的办法是起两个 Server 实例。用环境变量改端口:
SERVER_PORT=19000 java -jar mcp-server-provider.jar SERVER_PORT=19001 java -jar mcp-server-provider.jar两个实例都起来后,去 Nacos 控制台看mcp-server-provider的服务列表,应该能看到两个健康实例,IP 相同但端口不同。这时候在 Agent 端连续发起几次工具调用,观察两个实例的日志,正常情况下请求会分散到两个实例上,这就是负载均衡在起作用。
再验证动态感知:手动停掉 19001 这个实例,等几秒让 Nacos 把它标记为不健康并摘除,然后继续调 Agent,你会发现请求全部落到 19000 上,Agent 端不需要重启,也不会报错。这就是「节点变更动态感知」的价值。
实际跑的时候,下面这几个报错很常见,我按现象和原因列一下:
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 401 Unauthorized | Nacos 用户名密码错误,或命名空间 ID 填错 | 核对username/password,确认service-namespace是 ID 不是名称 |
| local proxy failed / 连接被拒 | Server 没起来,或端口被占用 | 检查 Server 进程和端口,确认 Nacos 里实例是健康状态 |
| reading choices 相关异常 | 模型返回格式解析失败,通常是工具定义有问题 | 检查@Tool描述是否为空,参数类型是否被模型支持 |
| OAuth / 鉴权失败 | 模型 API Key 无效或过期 | 检查DASHSCOPE_API_KEY环境变量是否注入成功 |
| Client 启动后工具列表为空 | 订阅的服务名写错,或 Server 没注册成功 | 对比connections里的服务名和 Nacos 里的服务名 |
还有一个隐蔽的坑:如果你在同一个应用里既引了 server starter 又引了 client starter,可能会出现ToolCallbackProvider注入到错误的 Bean。这种情况建议拆成两个独立应用,Server 只负责提供工具,Client 只负责消费,职责清晰,排查也简单。
另外,request-timeout设得太短也会导致偶发失败,尤其是工具内部要调外部 HTTP 接口的时候。我一般设 600s,如果业务确实很快,可以适当调小,但不建议低于 30s。
6. 从注册发现到稳定调用:把链路跑通之后
把上面几步走完,你手上就有了一套能自动注册、自动发现、带负载均衡的 MCP 分布式调用链路。回过头看,Spring AI Alibaba 这套方案真正解决的问题,是把 MCP 从「点对点直连」升级成了「面向注册中心调用」,Agent 不再关心工具部署在哪台机器上,Server 扩容缩容对 Agent 透明,工具元数据更新也能实时同步。
如果你后续要把它用到生产,有几个方向可以继续打磨。一是把 Nacos 的鉴权和命名空间规划做细,不同业务线的 MCP 服务分命名空间隔离;二是给 MCP Server 加上健康检查接口,让 Nacos 能更准确地判断实例状态;三是关注 Nacos 3 的 mcp-registry 和 mcp-router 能力,Spring AI Alibaba 后续会基于这些做更灵活的路由策略。
调试阶段如果遇到模型侧的问题,比如工具调用返回的结果模型理解不了,可以先用模型对话能力单独验证一下模型是否正常,把模型问题和 MCP 链路问题分开排查,效率会高很多。工具调用的 Key 和接入配置,可以在 API Keys 页面统一管理,接入文档里有各语言 SDK 的完整示例,照着改就行。如果是要长期跑编码类或 Agent 类的任务,Coding Plan 里的额度模型更适合持续调用,不用每次单独申请。
最后留一个实操建议:第一次搭这套链路时,先不要接真实的业务系统,用一个返回固定值的假工具把 Server 注册、Client 发现、多实例负载均衡这三步验证通过,再替换成真实业务逻辑。这样出问题时你能快速定位是链路问题还是业务代码问题,省掉大量来回排查的时间。