MCP Gateway 管理(多服务聚合与治理)
McpGateway 不是“多建几个 McpClient 的包装类”,而是 AI4J 里真正的多服务 MCP 运行时。
它同时负责 4 件事:
- 保存所有已接入的
McpClient - 维护
tool -> client的目录映射 - 统一管理全局服务和用户级服务
- 把配置源、动态增删和上层 Agent 调用收敛到一个入口
如果你只接一个 MCP,McpClient 就够了;如果你要做服务治理、租户隔离、动态启停,核心入口就是 mcp/gateway/McpGateway.java。
1. 先看它内部到底管理什么
McpGateway 内部有 3 个关键状态:
mcpClients保存clientKey -> McpClienttoolRegistry由McpGatewayToolRegistry维护toolKey -> clientKeyconfigSource绑定McpConfigSource,负责把配置系统变更翻译成网关增删改
这意味着 gateway 管理的不是“工具列表快照”,而是:
- 连接状态
- 工具目录
- 配置源绑定关系
- 生命周期入口
2. key 规则就是它的多租户边界
AI4J 没把用户隔离做成注释约定,而是直接写进 key 规则里。
全局服务
- client key:
serviceId - tool key:
toolName
用户级服务
- client key:
user_{userId}_service_{serviceId} - tool key:
user_{userId}_tool_{toolName}
对应实现入口:
McpGatewayKeySupport.buildUserClientKey(...)McpGatewayKeySupport.buildUserToolKey(...)McpGatewayKeySupport.extractUserIdFromClientKey(...)
这带来两个直接结果:
callUserTool(...)可以先查用户专属工具,再回退到全局工具getUserAvailableTools(...)可以把用户工具和全局工具在一个结果里合并
3. 初始化不是“读个 JSON”这么简单
模式 A:配置文件初始化
McpGateway gateway = new McpGateway();
gateway.initialize("mcp-servers-config.json").join();
当 configSource == null 时,initialize(...) 的执行链是:
loadServerConfig(configFile)startConfiguredServers()- 对每个启用服务调用
clientFactory.create(...) addMcpClient(...)client.connect().join()toolRegistry.refresh(mcpClients).join()
这里有一个很重要的真实语义:
- 配置文件里只有
enabled == true的服务会被启动 - 单个服务启动失败会被记录日志并吞掉
- 整个 gateway 仍然可能被标记为
initialized = true
所以 initialize() 成功,不等于“所有服务都连上了”,它只表示网关初始化流程完成了。
模式 B:配置源初始化
McpConfigSource source = new FileMcpConfigSource("mcp-servers-config.json");
McpGateway gateway = new McpGateway();
gateway.setConfigSource(source);
gateway.initialize().join();
当你设置了 configSource 后,initialize() 不再走本地配置文件解析,而是走:
configSourceBinding.loadAll(configSource)- 从配置源取回全部配置
- 为每个配置创建 client 并接入 gateway
这条路径更适合:
- 数据库配置中心
- 后台管理台
- 多租户动态接入平台
4. 动态增删是“重建目录”,不是局部打补丁
addMcpClientInternal(...) 的真实顺序是:
- 先把新 client 放进
mcpClients client.connect().join()toolRegistry.refresh(mcpClients).join()- 成功后再断开旧 client
- 如果失败,回滚
mcpClients,再断开新 client
removeMcpClientInternal(...) 的顺序是:
- 从
mcpClients删除 client.disconnect().join()toolRegistry.refresh(mcpClients).join()
这说明当前实现的目录更新策略是:
- 每次增删改后,重新向所有已连接 client 拉一遍工具清单
- 然后整体重建
tool -> client映射缓存
优点是实现简单、状态一致性强。缺点是:
- 服务很多时,刷新成本会线性放大
- 不是增量更新
5. McpGatewayToolRegistry 真正做了什么
McpGatewayToolRegistry.refresh(...) 会:
- 遍历所有
mcpClients - 对每个已连接 client 调
getAvailableTools() - 把
McpToolDefinition转成 OpenAI 风格Tool.Function - 同步生成
toolKey -> clientKey映射 - 刷新
availableTools缓存
这里还有一个很容易被忽略但非常关键的事实:
如果两个全局服务暴露了同名工具,映射会互相覆盖:
- 当前实现没有为全局同名工具做命名空间隔离
- 最终命中哪个 client,取决于 refresh 时最后写入的那一个
所以在多服务平台里,toolName 命名规范不是建议,而是硬要求。
6. 调用路径怎么走
全局工具
String result = gateway.callTool("query_weather", arguments).join();
调用链是:
toolRegistry.getClientId(toolName)- 找到对应
clientKey mcpClients.get(clientKey)client.callTool(toolName, arguments)
如果找不到映射,会直接抛:
IllegalArgumentException("工具不存在: ...")
如果 client 不存在或未连接,会抛:
IllegalStateException("MCP客户端不可用: ...")
用户工具
String result = gateway.callUserTool("u1001", "query_weather", arguments).join();
调用链是:
- 构造
user_{userId}_tool_{toolName} - 先查用户级映射
- 命中则走对应用户 client
- 未命中则回退到全局
callTool(...)
这个回退策略很实用,但也要意识到它的含义:
- 用户级未命中,不代表该工具不可用
- 可能只是落回了全局共享服务
如果你的业务要求“租户强隔离,不允许回退”,就不能直接照搬默认策略。
7. getAvailableTools(...) 和 getUserAvailableTools(...) 的边界
getAvailableTools()
- 返回所有全局工具缓存
- 如果缓存为空,会先 refresh 一次
getAvailableTools(serviceIds)
- 只过滤全局
clientKey - 本质上是按
serviceId取指定 client 的工具集合
getUserAvailableTools(serviceIds, userId)
- 先枚举用户级 client
- 再合并全局 client
serviceIds只对全局 client 过滤生效
所以它不是“只返回用户级工具”,而是“用户级工具 + 可见的全局工具”。
8. 配置源热更新到底怎么生效
McpGatewayConfigSourceBinding 是配置源和网关之间的桥。
它会把 McpConfigSource 的 3 类事件翻译成实际动作:
onConfigAdded-> 创建 client ->gateway.addMcpClient(...)onConfigUpdated-> 重新创建 client ->gateway.addMcpClient(...)onConfigRemoved->gateway.removeMcpClient(...)
这意味着 AI4J 现在已经具备了“配置变更驱动运行时服务切换”的基础骨架。
但也要注意边界:
FileMcpConfigSource只提供reloadConfigs(),不会自动监控文件系统- MySQL / Redis / 配置中心要靠你自己实现
McpConfigSource
9. getGatewayStatus() 能看什么,不能看什么
getGatewayStatus() 当前能提供:
totalClientsglobalClientsuserClientsconnectedClientstotalTools- 每个 client 的
connected / initialized / type
它适合:
- 管理台状态页
- 启动后自检
- 回归测试断言
但它还不是完整观测面,因为里面没有:
- 每个 tool 的调用量
- 延迟分布
- 失败率
- 最近一次异常原因
这类指标仍然需要你在网关外围补监控和日志。
工具映射快照:getToolToClientMap()
如果要看“当前每个工具名落在哪个 client”,用 getToolToClientMap():
Map<String, String> mapping = gateway.getToolToClientMap();
// 形如:
// "search_repositories" -> "github"
// "user_123_tool_query_weather" -> "user_123_service_weather"
它返回的是 McpGatewayToolRegistry.snapshotMappings() 的一份快照(toolKey -> clientKey),包含全局工具和用户级工具两类 key。它和 getGatewayStatus() 里的 totalTools 对应同一份目录,适合做调试、管理台展示或回归断言。注意它是一次性快照,不是实时视图。
用户级批量清理:clearUserMcpClients(userId)
对应 addUserMcpClient(...) / removeUserMcpClient(...) 的生命周期收尾,clearUserMcpClients(userId) 会一次性断开某个用户的全部 MCP 客户端:
gateway.clearUserMcpClients("u1001").join();
// 等价于:找到所有以 "user_u1001_service_" 开头的 clientKey,
// 逐个 removeMcpClientInternal(...)(断开连接 + refresh 目录)
它的真实行为是:
- 按
user_{userId}_service_前缀过滤出该用户的全部 clientKey - 逐个走
removeMcpClientInternal(...):从mcpClients移除 →client.disconnect()→toolRegistry.refresh(...) - 返回清理数量日志
适用场景:用户登出、租户注销、会话过期回收。单个 client 移除失败只记日志不中断整批清理。和单个 removeUserMcpClient(...) 一样,它只清理用户专属 client,全局共享服务不受影响。
10. 与 Agent 集成时真正的分层关系
推荐结构是:
- 宿主启动时初始化
McpGateway - Agent 构建时显式声明
mcpServices - 运行时由
ToolUtil通过 gateway 完成远端调用
Agent agent = Agents.react()
.toolRegistry(Collections.<String>emptyList(), Arrays.asList("github", "filesystem"))
.build();
要点只有一句:
- gateway 解决“服务怎么管理”
toolRegistry(..., mcpServices)解决“这次给模型看哪些服务”
两者不是一回事。
11. 什么时候该用它,什么时候不该用它
适合上 McpGateway 的场景:
- 服务数量超过一个
- 需要统一管理多种 transport
- 需要用户级隔离
- 需要动态启停和配置源治理
不必一开始就上 gateway 的场景:
- 只接一个 MCP
- 只是验证 transport 是否能连通
- 还没跑通最基本的
connect -> tools/list -> tools/call
12. 这页最该记住的结论
McpGateway 在 AI4J 里不是“辅助类”,而是 MCP 多服务治理层本身。
它把:
- 连接生命周期
- 工具目录
- 用户隔离
- 配置源热更新
收敛成一个正式运行时;但它不负责解决所有问题,例如命名冲突治理、失败熔断、审计指标,仍然需要在平台层继续补齐。