做Agent应用这段时间,我最大的感受是:模型的能力再强,也得有“手”去干活。这双手就是工具,而工具怎么暴露给模型,就成了架构设计里绕不开的问题。MCP(Model Context Protocol)标准出来之后,工具接入总算有了统一接头,Java生态里还有了Spring AI Alibaba这种开箱即用的底座框架。但真正把MCP Server部署到生产环境,问题跟着就来了——多个MCP实例如何注册、如何被发现、怎么做负载均衡和高可用?我最后选了 SAA + Nacos 这套组合把分布式MCP应用跑通了。这篇文章就是把这套方案的完整落地过程,从架构思路到踩坑明细,原原本本写出来,给正在做类似项目的同学一个可复用的参考。
1. 先把架构想明白:为什么是 SAA + Nacos + MCP
1.1 MCP 解决的是什么问题
MCP 是2024年底随 Claude 生态一起火起来的开放协议,全称 Model Context Protocol,中文语境里常被翻译成“模型上下文协议”。它的设计目标非常直接:把 AI 应用和外部工具之间的对接方式标准化。以前要让大模型查订单、改库存、读文件,开发人员得针对每一个工具写一套专用适配逻辑,工具提供方也得为不同 AI 应用分别定制接口。MCP 相当于在两者之间插入一个统一接头,工具方只需按协议暴露能力,AI 应用只需按协议发现和调用能力,谁也不绑死谁。
拿实际生态举例:设计圈的 Blender、Figma,开发圈的 Chrome DevTools、Playwright,甚至安全测试工具 Burp Suite、Yakit,都出了对应的 MCP Server。只要接入 MCP,AI 就能直接操作这些原本只能人工使用的软件。这个趋势说明,MCP 不是某个云厂商的单点方案,而是整个行业在工具接入方式上的一次收敛。
如果用过 USB 接口就能类比:传统 API 集成像是给每个设备定制一根专用线缆,MCP 则是统一成了 USB-C——不管设备原来长什么样,接上同一个标准就都能通。这也是为什么我在新项目里直接押注 MCP,不在一对一适配里继续“造轮子”。
1.2 单机 MCP Server 在分布式场景下的瓶颈
MCP Server 有两种落地形态。第一种是进程内模式,工具逻辑直接和 AI 应用跑在同一个进程里,没有网络开销,适合工具与主应用强绑定的单体场景。第二种是远程模式,MCP Server 独立部署成服务,AI 应用通过网络协议(如 streamable HTTP 或 WebSocket)去调用。
单体项目里第一种形态就够用,但一旦进入微服务架构,问题立刻变复杂。最直接的一个:MCP Server 的地址写在哪?如果直接把http://10.0.0.5:8080/mcp写死在 AI 应用配置里,那实例挂了、扩容了、换机器了,全都得手动改配置重新发布。第二个问题是负载均衡,AI 应用自己的并发上来了,一个 MCP Server 实例扛不住,多实例之后请求怎么分发?第三个问题是治理,工具有没有被调用、调用延迟多高、某个实例是否健康,这些在裸连接情况下全是黑盒。
这些问题本质上指向同一个答案:需要引入注册中心。分布式系统里的服务发现、健康检查、负载均衡都是成熟课题,没必要为 MCP 单独再发明一套。Nacos 本来就是 Spring Cloud 生态里的标准注册中心,社区成熟度高,顺手就能把 MCP Server 也纳入统一治理体系——这就是我选 Nacos 的根本原因。
1.3 SAA 在整个链路中的位置
Spring AI Alibaba(下文统一简称 SAA)是阿里开源的 Java AI 应用开发框架,定位是让 Java 开发者能用 Spring Boot 的方式快速构建 AI 应用。它兼容 Spring AI 标准 API,同时在模型接入上对通义千问做了深度优化,开箱即用;对 OpenAI、Ollama 等模型也有对应适配。
在这套分布式 MCP 方案里,SAA 扮演了三个角色。第一,它是 AI 应用的主框架,聊天对话、提示词模板、Agent 编排这些能力都由它承载。第二,它内置了 MCP 客户端支持,只要配置了 MCP Server 地址,SAA 会自动加载远程工具列表,开发者不需要手写 WebSocket 或 HTTP 调用逻辑。第三,它把模型层的差异屏蔽掉了,换模型只改配置,不涉及业务代码。
一句话总结三层关系:MCP Server 是干活的人,SAA 是调度干活的人,Nacos 是让调度方知道“谁在哪、谁能干”的通讯录。三者合起来,才是一个能在生产环境横向扩展的分布式 MCP 应用。
2. 环境准备:版本坑比你想的多
2.1 Nacos 版本怎么选
Nacos 的版本选择是我这次踩坑最多的一个环节,真的不夸张。当前社区里主流在用的是 2.5.x 和 3.x 两条线。2.5.x 胜在稳定,资料多,遇到问题搜索引擎一抓一大把;3.x 是后续演进版本,在配置管理和服务发现上做了一些能力增强。
但版本不能光看大版本号,要结合你的运行环境一起定。比如我在 ARM 架构的 Mac 上跑过 Nacos 2.5.0,整体可用,但如果用老版本的 JDK(比如 8u 以下)可能会遇到调度器相关的问题;Linux ARM 服务器上则建议直接用 2.5.x 以上的版本,对 ARM 的适配更完整。还有一点,Nacos 的存储层默认建议用 MySQL,我环境里正好是 MySQL 8.4.11,这里就有一层隐藏的门槛:老版本 Nacos 自带的数据库驱动对 MySQL 8.x 的支持并不完美,跑起来会出现诡异的时区报错或认证插件问题。Nacos 2.5.x 对 MySQL 8.x 的支持已经比较稳,3.x 更好一些。生产环境建议先用 2.5.x 稳定版,不赶新功能的话没必要上 3.x 当小白鼠。
Windows 上启动 Nacos 也有经典坑。用startup.cmd默认是以集群模式启动的,本地开发必须手动加参数:startup.cmd -m standalone。如果启动后访问 8848 端口没反应,先去看logs/start.out日志,十有八九是data目录权限问题或者 MySQL 连接没通。另外强烈建议本地开发也把 MySQL 配好,不要用 Nacos 内置的 Derby 数据库,否则后面做集群实验时会遇到一致性问题。
2.2 SAA 版本与 MCP 依赖的匹配关系
Spring AI Alibaba 从 1.0.0.0 版本开始正式 GA,这个版本对应的 Spring Boot 基线是 3.4.x/3.5.x,JDK 要求 17 以上。如果项目还在用 JDK 8 或 Spring Boot 2.x,很遗憾,这套方案和你的技术栈不兼容,需要先做基础版本升级。
MCP 相关的 Java 依赖分两块:Server 端用spring-ai-mcp-server-webmvc(阻塞式 WebMVC)或spring-ai-mcp-server-webflux(响应式 WebFlux);客户端用spring-ai-mcp-client。这些依赖在 SAA 的 BOM 里已经统一管理,工程里引入后不需要手工指定版本。
这里要特别提醒:Spring AI 在 1.0.0 正式版之前经历了多个里程碑版本,MCP SDK 的接口频繁调整。如果你从网上复制了一段老代码,跑起来报NoClassDefFoundError或者 MCP 协议握手失败,优先检查 Spring AI 版本是否一致——这种问题十有八九是版本混搭。
2.3 初始化项目骨架
基础工程的依赖可以直接抄这份。项目用一个 Maven 多模块结构,mcp-server模块做工具服务,ai-app模块做主应用。
<!-- 父 POM 关键依赖管理 --> <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-bom</artifactId> <version>1.0.0.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>Server 模块引入 MCP Server 与 Nacos 注册发现相关依赖:
<dependency> <groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-alibaba-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webmvc</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>配置上,Nacos 注册中心指向本地,服务名就叫mcp-order-server:
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 service: mcp-order-server username: nacos password: nacos ai: mcp: server: name: orderTools到这里骨架就搭完了,下一步开始写真正的工具代码。
3. 手写一个 MCP Server:订单工具 + NL2SQL
3.1 用 @Tool 定义业务工具
在 SAA 里暴露 MCP 工具非常简单,核心就是给业务方法加一个@Tool注解,框架会自动扫描并代理为标准 MCP 工具调用。我这边写了一个订单查询服务,内部对接 MySQL 8.4.11 的数据源,提供订单查询、库存扣减、价格计算三个能力。
@Service @Slf4j public class OrderToolService { private final JdbcTemplate jdbcTemplate; public OrderToolService(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Tool(name = "queryOrder", description = "根据订单号查询订单详情,返回订单状态、金额、商品明细") public String queryOrder(String orderNo) { // 从 MySQL 查询订单主表 String sql = "SELECT order_no, status, total_amount FROM orders WHERE order_no = ?"; Map<String, Object> row = jdbcTemplate.queryForMap(sql, orderNo); return JSON.toJSONString(row); } @Tool(name = "deductStock", description = "扣减指定商品库存,传入商品ID和扣减数量,返回剩余库存") public String deductStock(String productId, Integer count) { // 扣减前先查剩余量,余额不足直接失败 Integer remain = jdbcTemplate.queryForObject( "SELECT stock_count FROM products WHERE product_id = ?", Integer.class, productId); if (remain == null || remain < count) { return "{\"success\": false, \"message\": \"库存不足\"}"; } jdbcTemplate.update("UPDATE products SET stock_count = stock_count - ? WHERE product_id = ?", count, productId); return "{\"success\": true, \"remainStock\": " + (remain - count) + "}"; } }这段代码里有两个细节值得展开。第一个是工具的描述信息,description字段不是摆设,它会随着工具定义一起发给大模型,模型就靠它来判断该不该调用这个工具、该传什么参数。描述写得模糊,AI 就会频繁误调用,这也是很多同学说“我的 Agent 不调用工具”的常见原因。第二个是返回值,MCP 工具方法返回的是字符串,这个字符串会作为模型继续推理的上下文输入。返回结构要尽量结构化,直接返回 JSON 字符串,让模型一眼能看明白结果。
如果想进一步体现 AI 的能力,可以在工具内部加一个 NL2SQL 的处理环节。比如增加一个queryByNaturalLanguage工具,用户问一句“最近七天销量最高的三个商品”,工具内部用通义千问生成 SQL、执行、返回结果。这样 MCP Server 不仅暴露了“原子能力”,还暴露了“理解能力”,在真实业务场景里价值更高。
3.2 把 MCP Server 注册到 Nacos
MCP Server 本身是一个独立的 Spring Boot 服务,注册到 Nacos 用的是最常规的 Spring Cloud Alibaba Discovery 机制。只要spring-cloud-starter-alibaba-nacos-discovery在依赖里,配置了spring.cloud.nacos.discovery的坐标,应用启动后就会自动上报实例信息。
注册完成后,在 Nacos 控制台的服务列表里就能看到一个mcp-order-server服务,点开详情能看到实例 IP 和端口。这就为消费端提供了一份实时准确的“MCP 工具通讯录”。
这里说一下多实例部署。MCP Server 完全可以起多个副本,比如同一台机器上 8081、8082 两个端口各跑一个实例,或者部署到多台机器上。Nacos 注册中心会自动维护这两个实例的健康状态。AI 应用消费端拉取服务列表时,拿到的是多个地址,再选择其中一个发起连接,这就在不引入额外网关的情况下实现了负载均衡。
我实测下来,这个方案对比写死地址最直观的收益是:扩容时只需要再起一个 JVM 实例,自动注册进集群,AI 应用这边什么都不用改;缩容时直接下线实例,Nacos 的健康检查会在几秒内摘除节点。整个过程对上层调用无感。
3.3 消费端:AI 应用通过 SAA 动态发现并调用工具
消费端是另一个 Spring Boot 应用,同样基于 SAA 搭建。如果把 MCP Server 地址写死在配置文件里,等于绕了一圈又回到了原点。所以我在消费端做了一个动态发现组件,启动时从 Nacos 拉取mcp-order-server的实例列表,拿到地址后动态构建 MCP Client。
@Configuration @Slf4j public class McpDynamicClientConfig { @Bean(destroyMethod = "close") public McpClient orderMcpClient(NacosDiscoveryClient discoveryClient) { List<ServiceInstance> instances = discoveryClient.getInstances("mcp-order-server"); if (instances.isEmpty()) { throw new IllegalStateException("Nacos 中未发现 mcp-order-server 实例"); } // 简单轮询:取第一个可用实例,实际项目可用 LoadBalancer 扩展 ServiceInstance instance = instances.get(0); String mcpUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/mcp"; log.info("动态发现 MCP Server 地址: {}", mcpUrl); McpClient client = McpClient.http(mcpUrl) .type(McpSchema.ToolCapabilities.TOOLS) .build(); client.initialize().block(Duration.ofSeconds(10)); return client; } }MCP Client 就绪后,在 SAA 里注入McpToolService,就能把它暴露的能力挂载到 ChatClient 的工具调用列表里。后续用户对话时,SAA 的 Agent 编排会自行判断该调用哪个工具,并自动填充参数。
关于传输方式,Spring AI 的 MCP 客户端支持 HTTP 和 WebSocket 两类连接。Streamable HTTP 是短连接,每次调用走一次 HTTP 请求,简单直接;WebSocket 适合高频工具调用场景,连接复用率高。生产环境如果走外网,建议用 wss 协议,URL 形式类似wss://api.example.com/mcp/?token=xxxx,Token 通过连接参数传入,服务端校验通过后才建立会话——这也是 MCP Server 暴露到公网后的安全底线。
4. 分布式 MCP 的几个硬骨头
4.1 配置中心:把工具开关和密钥动态化
MCP Server 和 AI 应用都接入 Nacos 之后,配置中心的能力自然也要用起来。我最常用的两个场景:第一,把模型密钥、数据库地址这类容易变的环境信息放到 Nacos 配置中心,修改配置后服务自动感知,省去重新发版的流程;第二,把工具的启停做成配置项,实现“动态开关工具”。
比如某个 MCP 工具因为数据源维护需要临时下线,不需要改动代码,只需在 Nacos 配置里把tools.deduct-stock.enabled=false设置上,服务端通过@ConditionalOnProperty或者运行时判断,该工具就不会出现在 MCP 工具列表里,AI 应用自然也就调不到了。这个能力在灰度发布和故障应急时非常有用。
配置动态刷新的底层原理是 Nacos 客户端的长轮询机制:配置变更后服务端主动推送,配合@RefreshScope注解实现上下文的刷新。不过在 MCP Server 里要注意,不能只在“读配置”的地方加@RefreshScope,还得确保工具注册管理器在配置变更后重新拉取生效。我用的是一个配置刷新监听器,订阅配置变更事件,收到推送后重建 MCP 工具注册表。
4.2 分布式事务:订单与库存的经典难题
MCP 工具一旦跨服务,分布式事务的问题就会浮上来。典型场景:用户下单时要同时调用订单服务创建订单、库存服务扣减库存。这一过程如果通过 MCP 工具实现,就变成了两次独立的远程调用,任何一次失败都会造成数据不一致。
针对这个问题,我的建议分两层来看。第一层,如果是新建系统,尽量把强一致的跨服务操作放到同一个 MCP 工具方法里,用数据库本地事务解决。比如把“创建订单+扣库存”封装到同一个事务方法中,一条数据库事务搞定,完全避开了分布式事务的复杂度。第二层,如果订单和库存确实分属不同库、不同服务,那就要引入最终一致性方案。可以用本地消息表 + 定时任务,或者直接上 Seata 的 AT 模式。
需要特别提醒的是:AI Agent 场景下的工具调用和传统 RPC 的事务模型很不一样。Agent 可能会在两次工具调用之间穿插大模型推理,事务的边界被拉长,这导致传统分布式事务几乎无法在这种链路里有效运作。更现实的思路是让工具本身支持“补偿操作”,也就是每个关键工具都提供对应的“反操作”,失败时由 Agent 或者人工触发补偿流程。我现在的实践就是组合这两种思路:库内强一致,跨库最终一致加补偿。
4.3 分布式锁与幂等
AI 场景有一个非常棘手的特性:同一个请求可能被重复处理。比如网络超时后客户端自动重试,或者模型在多轮对话里反复触发同一个工具,都可能导致库存被重复扣减。传统接口可以通过幂等表解决,MCP 工具同样需要幂等控制。
我常用的方案是 Redis 分布式锁 + 业务幂等号。以扣库存为例:工具调用时传入一个requestId,先尝试在 Redis 里写入这个请求号,写入成功说明是首次调用,继续执行扣减;写入失败说明是重复请求,直接返回上一次的结果。锁的过期时间要合理设置,太短会导致正常的慢请求还没执行完锁就被释放,太长又会阻塞其他请求;我一般设置在 10 到 30 秒之间,具体看工具方法的耗时分布。
4.4 安全与鉴权
最后必须聊安全。很多人在本地开发时把 Nacos 的鉴权关掉图省事,测试环境还好,一旦服务地址暴露在办公网或公网,这就是严重的风险点。Nacos 的鉴权配置入口在控制台的nacos.core.auth.enabled=true,同时要设置身份识别的 key 和 value,并修改 admin 的默认密码。从 Nacos 2.2 之后,这一套配置是生产环境必须做的,网上搜到的相关风险通告基本都是因为未开启鉴权导致配置泄露。
MCP Server 这一侧同样要做连接校验。远程 MCP Server 暴露在公网时,至少要在 HTTP 层校验Authorization头;更稳妥的方案是在 Nginx 层做 TLS 终结,把ws升级为wss,然后通过自定义拦截器校验连接参数里的 token。我在部署里就是这样一个组合:Nacos 开启鉴权,MCP Server 的 HTTP 接口校验 token,SSL 证书挂在网关层,TLS 1.2 起步。工具调用链路还挂了限流器,防止 AI 应用异常突发的调用打垮下游库存服务。
5. 常见问题与排查实录
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| Nacos 服务列表看不到 MCP Server | 注册中心地址配错、Nacos 未启动、网络不通 | 检查spring.cloud.nacos.discovery.server-addr,确认 8848 端口可达;查看 Nacoslogs/start.out |
| Nacos 启动后控制台访问不了 | 数据库连接失败、非 standalone 模式启动 | 确认 MySQL 配置正确,Windows 启动加-m standalone参数 |
| MCP Client 初始化报协议错误 | 服务端和客户端 MCP 协议版本不一致 | 统一 Spring AI 依赖版本,检查传输方式是否都是 streamable HTTP 或 WebSocket |
| 工具方法没有被 AI 调用 | 工具描述不清晰、工具名太隐晦 | 优化@Tool的name和description,让模型能明确判断调用时机 |
| 配置中心修改了但服务没生效 | @RefreshScope没加对位置、监听器没实现 | 将动态读取的 Bean 加@RefreshScope,或实现 Nacos 配置变更监听 |
| MySQL 8.4.11 连接报时区或认证错误 | JDBC URL 缺serverTimezone、驱动版本过低 | 使用新版mysql-connector-j,URL 后追加serverTimezone=Asia/Shanghai |
| 扣库存重复执行 | 缺少幂等控制 | 引入 Redis 分布式锁 + 业务幂等号 |
| AI 应用频繁出现工具调用失败 | 单个 MCP Server 实例过载 | 启动多个 MCP Server 副本,依赖 Nacos 自动负载均衡 |
踩过的坑里,最值得说的是版本问题。我第一次搭这套环境时,按网上旧教程拿了 Spring AI 0.8.x 的依赖,和 SAA 1.0.0.0 的 BOM 混在一起用,结果 MCP Client 初始化时直接因为内部 SDK 类缺失报错。当时排查了很久,最后才发现是父 POM 里没有引入 SAA 的 BOM 做统一版本管理。所以这里给一个最朴素的建议:依赖别手动指定子版本号,全部交给 BOM 管理,基本能规避大部分版本冲突。
另一个困扰很多人的问题,是 AI 应用发现 MCP Server 之后,工具列表还是空的。这一般不是注册中心的问题,而是 MCP Server 的工具注册路径没有被扫描到。SAA 默认扫描主应用类所在的包以及子包,如果@Tool方法所在的@Service类不在扫描范围内,工具就不会暴露。解决办法很简单,在启动类上显式加@ComponentScan(basePackages = "com.example.mcpserver")。
另外,如果是通过自定义McpClient动态建连,记得把连接初始化放到异步任务里,不要在@Bean的同步方法中做阻塞式握手。我最初在启动时同步调用initialize().block(),结果生产环境里某个 MCP Server 恰好启动慢了,整个 AI 应用直接起不来。后来改成异步初始化加上重试机制,应用可用性明显提升,即使外部工具服务临时故障,主应用也能照常启动、照常提供对话能力。
这套方案跑到现在,我的整体感受是:Nacos 和 SAA 的最大价值在于把你从工具对接的细节里解放出来,让你能把精力放到业务本身。而且它留给后续的扩展空间很大——比如可以在多个 MCP Server 之上做一个统一聚合网关,把订单、库存、物流这些工具都收口到一个入口;也可以把 Nacos 里接入 Dubbo 服务,让 MCP 工具直接下沉到已有的微服务能力。工具会被标准化,AI 应用会越来越多,尽早把这套分布式 MCP 的底座打好,后面加工具就是几行代码的事。