做Agent应用这段时间我最大的感受是模型的能力再强也得有“手”去干活。这双手就是工具而工具怎么暴露给模型就成了架构设计里绕不开的问题。MCPModel Context Protocol标准出来之后工具接入总算有了统一接头Java生态里还有了Spring AI Alibaba这种开箱即用的底座框架。但真正把MCP Server部署到生产环境问题跟着就来了——多个MCP实例如何注册、如何被发现、怎么做负载均衡和高可用我最后选了 SAA Nacos 这套组合把分布式MCP应用跑通了。这篇文章就是把这套方案的完整落地过程从架构思路到踩坑明细原原本本写出来给正在做类似项目的同学一个可复用的参考。1. 先把架构想明白为什么是 SAA Nacos MCP1.1 MCP 解决的是什么问题MCP 是2024年底随 Claude 生态一起火起来的开放协议全称 Model Context Protocol中文语境里常被翻译成“模型上下文协议”。它的设计目标非常直接把 AI 应用和外部工具之间的对接方式标准化。以前要让大模型查订单、改库存、读文件开发人员得针对每一个工具写一套专用适配逻辑工具提供方也得为不同 AI 应用分别定制接口。MCP 相当于在两者之间插入一个统一接头工具方只需按协议暴露能力AI 应用只需按协议发现和调用能力谁也不绑死谁。拿实际生态举例设计圈的 Blender、Figma开发圈的 Chrome DevTools、Playwright甚至安全测试工具 Burp Suite、Yakit都出了对应的 MCP Server。只要接入 MCPAI 就能直接操作这些原本只能人工使用的软件。这个趋势说明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.xJDK 要求 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 groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-bom/artifactId version1.0.0.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagementServer 模块引入 MCP Server 与 Nacos 注册发现相关依赖dependency groupIdcom.alibaba.cloud.ai/groupId artifactIdspring-ai-alibaba-starter/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-mcp-server-webmvc/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency配置上Nacos 注册中心指向本地服务名就叫mcp-order-serverspring: 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订单工具 NL2SQL3.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 ?; MapString, 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 注册到 NacosMCP 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) { ListServiceInstance 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/?tokenxxxxToken 通过连接参数传入服务端校验通过后才建立会话——这也是 MCP Server 暴露到公网后的安全底线。4. 分布式 MCP 的几个硬骨头4.1 配置中心把工具开关和密钥动态化MCP Server 和 AI 应用都接入 Nacos 之后配置中心的能力自然也要用起来。我最常用的两个场景第一把模型密钥、数据库地址这类容易变的环境信息放到 Nacos 配置中心修改配置后服务自动感知省去重新发版的流程第二把工具的启停做成配置项实现“动态开关工具”。比如某个 MCP 工具因为数据源维护需要临时下线不需要改动代码只需在 Nacos 配置里把tools.deduct-stock.enabledfalse设置上服务端通过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.enabledtrue同时要设置身份识别的 key 和 value并修改 admin 的默认密码。从 Nacos 2.2 之后这一套配置是生产环境必须做的网上搜到的相关风险通告基本都是因为未开启鉴权导致配置泄露。MCP Server 这一侧同样要做连接校验。远程 MCP Server 暴露在公网时至少要在 HTTP 层校验Authorization头更稳妥的方案是在 Nginx 层做 TLS 终结把ws升级为wss然后通过自定义拦截器校验连接参数里的 token。我在部署里就是这样一个组合Nacos 开启鉴权MCP Server 的 HTTP 接口校验 tokenSSL 证书挂在网关层TLS 1.2 起步。工具调用链路还挂了限流器防止 AI 应用异常突发的调用打垮下游库存服务。5. 常见问题与排查实录现象可能原因解决方案Nacos 服务列表看不到 MCP Server注册中心地址配错、Nacos 未启动、网络不通检查spring.cloud.nacos.discovery.server-addr确认 8848 端口可达查看 Nacoslogs/start.outNacos 启动后控制台访问不了数据库连接失败、非 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-jURL 后追加serverTimezoneAsia/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 的底座打好后面加工具就是几行代码的事。