MCP 与 Agent 一体化实战(端到端)
这一页不是“天气助手 demo 文案”,而是把 MCP 真正进入 Agent 运行时后的执行链拆开。
要回答的问题只有一个:
一个第三方 MCP 服务,是怎样从配置文件一路进入 Agent 推理循环,并最终被模型调用的?
1. 先看完整执行链
在 AI4J 里,MCP 接入 Agent 至少会经过 7 层:
mcp-servers-config.jsonMcpGateway.initialize(...)McpClient.connect()McpGatewayToolRegistry.refresh(...)ToolUtil.getAllTools(functionList, mcpServerIds)- Agent runtime 把工具 schema 暴露给模型
ToolUtil.invoke(...)再把调用路由回 gateway/client
如果不把这 7 层拆开,很多文档都会把“Agent 能调用 MCP”写成一句空话。
2. 场景设定
假设我们接一个第三方天气服务:
- serviceId:
weather-http - transport:
streamable_http - tool:
query_weather - Agent runtime: ReAct
- 目标:模型根据用户问题决定是否调用天气工具
配置如下:
{
"mcpServers": {
"weather-http": {
"type": "streamable_http",
"url": "http://127.0.0.1:8000/mcp",
"protocolProfile": "AUTO",
"enabled": true
}
}
}
这里的 weather-http 不是备注,而是后续 Agent 白名单要用到的真实 serviceId。protocolProfile 由 Gateway 配置模型读取;默认 AUTO 只对 Streamable HTTP 做 server/discover 的有限兼容探测,不会自动把 endpoint 切换为 HTTP+SSE。
3. 第 1 段:把第三方服务接进宿主
McpGateway gateway = McpGateway.getInstance();
gateway.initialize("mcp-servers-config.json").join();
这一步之后,真正发生的不是“网关准备好了”这么简单,而是:
- gateway 读入配置
McpGatewayClientFactory根据type创建 transport- 为
weather-http创建McpClient client.connect()执行与 profile 对应的生命周期:AUTO 发现现代 peer 或进入 legacy 初始化握手toolRegistry.refresh(...)拉取工具清单
建议你在这一步就先做一次显式检查:
List<Tool.Function> gatewayTools = gateway.getAvailableTools().join();
System.out.println(gatewayTools);
如果这里拿不到工具,后面 Agent 再怎么调也不会成功。
4. 第 2 段:把 MCP 工具投影到 Agent 可见面
Agent 不是直接读取 gateway 内部状态,而是通过 ToolUtil.getAllTools(...) 获取本次请求的工具集合。
构建 Agent 时通常是:
Agent agent = Agents.react()
.modelClient(modelClient)
.model("doubao-seed-1-8-251228")
.systemPrompt("你是天气助手,必要时必须调用工具后再回答。")
.toolRegistry(Collections.<String>emptyList(), Arrays.asList("weather-http"))
.options(AgentOptions.builder().maxSteps(4).build())
.build();
这句最重要的不是语法,而是白名单语义:
functionList为空,说明本次不暴露本地 Function 工具mcpServices = ["weather-http"],说明只暴露这一个 MCP 服务的工具
当前 AI4J 不会因为 gateway 里接了很多服务,就自动把它们全部给模型看见。
5. 第 3 段:模型发起 tool call 时真正走哪条路
模型在推理中决定调用工具后,不是直接碰 McpClient,而是先回到 ToolUtil.invoke(...)。
在 MCP 场景里,关键分支是:
ToolUtil.invoke(functionName, argument)- 如果命中用户工具前缀,先尝试
gateway.callUserTool(...) - 否则进入本地 MCP / Function / 全局 gateway 工具分发
- 远程第三方 MCP 最终落到
gateway.callTool(...) - gateway 根据
tool -> client映射找到weather-http McpClient.callTool(...)发起tools/call
因此,Agent 看到的是工具 schema,但真正执行时依然由 Core SDK 的 MCP 运行时兜底。
6. 第 4 段:结果怎样回到模型
McpClient.callTool(...) 拿到 MCP 响应后,会:
- 解析
tools/call的返回内容 - 转成字符串结果
- 返回给
ToolUtil - 再交回 Agent runtime
- Agent 把 tool result 放回模型上下文
- 模型继续生成最终回答
也就是说,MCP 工具在 Agent 里的角色是:
- 先作为 schema 暴露给模型
- 再作为 tool result 回填给模型
这与本地 Function 工具在 Agent 里的角色是对齐的,只是中间多了一段 MCP 协议链。
7. 一份最小可运行示例
McpGateway gateway = McpGateway.getInstance();
gateway.initialize("mcp-servers-config.json").join();
Agent agent = Agents.react()
.modelClient(modelClient)
.model("doubao-seed-1-8-251228")
.systemPrompt("你是天气助手,必须先调用工具再回答。")
.toolRegistry(Collections.<String>emptyList(), Arrays.asList("weather-http"))
.options(AgentOptions.builder().maxSteps(4).build())
.build();
AgentResult result = agent.run(AgentRequest.builder()
.input("请查询北京今天天气,并给出穿衣建议")
.build());
System.out.println(result.getOutputText());
这段代码验证的是整条闭环,而不是某个局部 API。
8. 为什么有时 gateway 有工具,但 Agent 看不到
这是最常见的集成误判。
排查顺序应该是:
gateway.isInitialized()是否为truegateway.getAvailableTools()是否能列出工具toolRegistry(..., mcpServices)是否传了正确的serviceId- 是否把 serviceId 和 toolName 混用了
mcpServices 传的是服务 ID,不是工具名。
9. 为什么模型有时不触发工具
如果 gateway 和 Agent 注册都没问题,但模型仍然不用工具,排查顺序应该是:
- tool description 是否让模型看懂它能做什么
systemPrompt是否明确要求必要时调用工具maxSteps是否过小- 用户问题是否真的需要该工具
这已经不是 MCP 连接问题,而是 runtime 提示与推理策略问题。
10. 多租户场景如何进入这条链
如果同一个 Agent 宿主要面向多个用户,而每个用户绑定不同第三方 MCP,可以这样接:
gateway.addUserMcpClient("u1001", "weather-http", userClient).join();
之后调用链变成:
- Agent 会话绑定
userId ToolUtil优先尝试用户级工具- gateway 查
user_{userId}_tool_{toolName} - 命中则走用户专属 client
- 未命中再回退全局工具
这里要自己想清楚权限边界:
- 默认实现允许回退
- 强隔离场景通常不应该回退
11. Trace 和诊断应该加在哪里
MCP + Agent 问题很多时候不是“错”,而是“不知道卡在哪”。
推荐至少观察这几个点:
- gateway 初始化是否成功
- 工具是否出现在 Agent 暴露列表
- 模型是否真的发出 tool call
- tool call 是否成功返回
如果 Agent runtime 已启用 trace,重点看:
- RUN
- MODEL
- TOOL
这样能很快分清:
- 是模型没决定调用
- 还是 MCP 工具执行失败
12. 一份更像回归测试的最小断言
@Test
public void test_mcp_agent_e2e() {
McpGateway gateway = McpGateway.getInstance();
gateway.initialize("mcp-servers-config.json").join();
List<Tool.Function> tools = gateway.getAvailableTools().join();
Assert.assertFalse(tools.isEmpty());
Agent agent = Agents.react()
.modelClient(modelClient)
.model("doubao-seed-1-8-251228")
.toolRegistry(Collections.<String>emptyList(), Arrays.asList("weather-http"))
.options(AgentOptions.builder().maxSteps(4).build())
.build();
AgentResult result = agent.run(AgentRequest.builder().input("北京天气").build());
Assert.assertNotNull(result);
Assert.assertNotNull(result.getOutputText());
}
重点不是断言输出具体文案,而是断言:
- gateway 有工具
- Agent 能执行
- 最终回答非空
13. 这页最该记住的结论
MCP 进入 Agent 不是“把一个第三方工具名塞给模型”。
它实际是一条跨 3 层的链:
- Core SDK 的 MCP 连接与治理层
- ToolUtil 的工具投影与调用分发层
- Agent runtime 的推理与工具消费层
把这 3 层分开理解,端到端问题就会容易定位很多。