Gateway and Multi-service
只要接入的 MCP 服务不止一个,真正重要的问题就不再是“单个 client 会不会调”,而是:
- 谁来统一持有这些连接
- 谁来维护工具目录
- 谁来区分全局服务和用户服务
- 谁来处理配置源变更
在 AI4J 里,这个角色就是 McpGateway。
1. McpGateway 的职责比“服务列表”大得多
从实现看,McpGateway 至少负责:
- 管理
serviceId -> McpClient - 管理工具映射和缓存
- 从配置文件或配置源加载服务
- 为请求层提供统一的可用工具查询
- 为执行层提供统一的工具调用入口
- 区分全局服务和用户级服务
这意味着 gateway 不是“一个 Map 包装器”,而是 MCP 的运行时控制面。
2. 初始化时到底发生什么
McpGateway.initialize(...) 当前有两条路径:
使用默认配置文件
如果没有显式设置 configSource,它会:
- 加载
mcp-servers-config.json - 遍历已启用服务
- 为每个服务创建
McpClient - 调用
addMcpClient(...) - 启动完后把自己设成全局实例
使用动态配置源
如果先 setConfigSource(...),则会:
- 把 gateway 绑定到
McpConfigSource - 初始化时执行
loadAll(configSource) - 后续继续监听配置增删改事件
所以 gateway 不只是“启动时扫一遍配置”,而是已经具备长期运行的配置治理入口。
3. addMcpClient(...) 真正做了什么
添加客户端不是简单 put 进 Map。addMcpClientInternal(...) 当前会:
- 把 client 放入
mcpClients - 立刻执行
client.connect().join() - 再执行
toolRegistry.refresh(mcpClients).join() - 如果之前已有旧 client,则断开旧连接
- 失败时回滚映射并清理新 client
这说明 gateway 在设计上追求的是:
- client 连接成功后才算接入完成
- 工具目录刷新与连接状态联动
- 替换失败时尽量恢复旧状态
4. gateway 如何管理工具目录
McpGatewayToolRegistry 做了两件核心工作:
- 聚合所有已连接 client 的
tools/list - 构建
tool -> clientId映射
刷新逻辑是:
- 遍历所有
mcpClients - 只处理
client.isConnected()的客户端 - 并发拉取每个 client 的
getAvailableTools() - 转成 OpenAI
Tool.Function - 覆盖写入新的 cache 和映射
也就是说,gateway 的可见工具目录并不是静态配置,而是“当前连接状态下的聚合快照”。
5. 全局服务和用户服务是怎么区分的
AI4J 对多租户不是只停留在概念层。
McpGatewayKeySupport 当前明确约定:
- 用户服务 key:
user_{userId}_service_{serviceId} - 用户工具 key:
user_{userId}_tool_{toolName}
这让 gateway 能同时处理:
- 全局共享服务
- 用户专属服务
而且两者不会共用同一套 key 空间。
6. getAvailableTools(...) 和 getUserAvailableTools(...) 的区别
getAvailableTools()
返回全局聚合缓存;如果缓存为空,会触发一次 toolRegistry.refresh(...)。
getAvailableTools(serviceIds)
如果传入 serviceIds,就只对这些全局服务做过滤并转换。
getUserAvailableTools(serviceIds, userId)
会同时返回:
- 该用户前缀下的专属服务工具
- 过滤后的全局服务工具
这意味着用户级工具不是“覆盖全局工具”,而是与全局工具合并暴露。
7. 一个必须明确写出来的现实约束
当前远端全局工具映射规则是:
- 全局工具:
toolName -> clientId - 用户工具:
user_{userId}_tool_{toolName} -> clientId
这带来一个工程后果:
- 用户工具命名空间有隔离
- 全局远端工具命名空间没有服务前缀隔离
也就是说,如果两个全局 MCP 服务都暴露 search,后写入的映射会覆盖前者。McpGatewayToolRegistry 不会自动改名,也不会为远端服务拼 serviceName_toolName。
这是当前多服务设计里最需要在文档中讲清楚的限制之一。
8. gateway 和请求白名单是什么关系
McpGateway 管的是“系统里已接入哪些服务”,但最终本次请求开放哪些服务,仍然由:
ChatCompletion.mcpServices(...)ResponseRequest.mcpServices(...)
决定。
两者关系可以记成:
- gateway:目录和连接控制面
mcpServices:请求级暴露控制面
有 gateway 不代表默认开放;有请求白名单也必须先有 gateway 才有服务可选。
9. 配置源动态变更时会发生什么
McpGatewayConfigSourceBinding 已经把配置源事件和 gateway 操作接起来了:
onConfigAdded-> 创建 client 并接入onConfigUpdated-> 创建新 client 并替换onConfigRemoved->removeMcpClient(...)
这说明 AI4J 已经考虑到 MCP 服务不是永远静态的。
如果你把 gateway 只当成“应用启动阶段初始化一次”的组件,会错过这层动态治理能力。
10. gateway 不负责什么
它虽然很核心,但它不负责:
- agent loop
- tool approval
- prompt 策略
- provider 响应编排
这些属于更上层运行时。
gateway 的边界是:
- 接入服务
- 管连接
- 管目录
- 管路由
把审批、权限解释或 agent 任务状态直接塞进 gateway,会把层次做乱。
11. 调试多服务问题时该先看哪里
建议按这个顺序排查:
McpGateway.getStatus()看 client 数量、连接数、工具数toolRegistry.snapshotMappings()看工具映射是否符合预期- 检查请求里的
mcpServices是否真的包含目标服务 - 检查是否出现远端全局同名工具覆盖
- 再回头看具体 transport 日志
如果一开始就只盯 provider 的 tool call,很容易把问题误判成模型行为异常。
12. 这一页的结论
McpGateway是 AI4J 的 MCP 控制面:它管理多服务连接、配置源、工具目录、用户级隔离和调用路由。它解决的是“服务如何被统一治理”,而不是“本次请求默认开放什么”。同时要注意,当前远端全局工具没有服务名前缀隔离,同名工具在多服务场景下会互相覆盖。