跳到主要内容

MCP Client 接入(单服务模式)

这一页只讲“单服务模式”。

也就是说,你当前的问题是:

  • 先连上一个 MCP server
  • 先确定它使用现代无状态 HTTP 还是 session-era 协议
  • 先跑通 tool / resource / prompt 三类高层 API

如果你已经要接多个服务或做用户隔离,就应该去读 gateway,而不是继续停在这页。

1. McpClient 的真实生命周期

McpClient 不是创建完就能直接 callTool()

它的真实生命周期是:

  1. 构造 transport
  2. new McpClient(...)
  3. connect()
  4. 按 transport/profile 完成就绪:默认 AUTO 先做有限探测;现代 HTTP 不握手;legacy transport 完成初始化握手
  5. 读取 tools / resources / prompts
  6. 调用能力
  7. disconnect()

中间少任何一步,都不能算一个稳定 client。

2. connect() 到底做了什么

connect() 总会先执行 transport.start(),超时 30s,然后按 transport/profile 进入不同路径:

  • 默认的 StreamableHttpTransport 使用 AUTO。它只先发送现代 server/discover;有效的现代发现结果会选择 2026-07-28,不发送 initializenotifications/initialized
  • AUTO 只在该探测收到未识别的 HTTP 400404405 时回退到 initialization-era Streamable HTTP,并执行 initialize -> notifications/initialized。认证失败、可识别的现代 JSON-RPC error、以及无效发现响应都不会触发降级。
  • STDIO、显式 type: "sse" 的 HTTP+SSE,以及显式选择 legacy Streamable HTTP profile 的 client,保留初始化时代流程。
  • 若 transport 需要心跳,client 会在准备就绪后启动 heartbeat。

因此 isConnected()isInitialized() 仍是调用前检查,但现代 HTTP 的 isInitialized() 表示 client ready,不表示发生过协议握手。

3. McpClient capability 边界

AI4J 的高层 API 覆盖 Tool、Resource 和 Prompt。现代 HTTP 每次请求声明的 capabilities 是保守的:它不会声称支持 sampling、roots、elicitation、MRTR 或 subscriptions,直到这些多轮协议能力真正可用。

不要根据旧 session-era initialize 示例推断现代 peer 也支持这些可选 capability。应以目标 server 的协议 profile 和实际能力目录为准。

4. 最小接入示例:STDIO

如果你连接的是本地子进程 MCP server,最短路径通常是 stdio。

McpTransport transport = new StdioTransport(
"npx",
Arrays.asList("-y", "@modelcontextprotocol/server-filesystem", "D:/workspace"),
null
);

McpClient client = new McpClient("demo-client", "1.0.0", transport);
client.connect().join();

List<McpToolDefinition> tools = client.getAvailableTools().join();
String result = client.callTool("read_file", Collections.singletonMap("path", "README.md")).join();

client.disconnect().join();

这一条路径验证的是:

  • 子进程是否能启动
  • stdio transport 是否能握手
  • tools/list 和 tools/call 是否能通

5. 最小接入示例:Streamable HTTP / SSE

如果你接的是服务化 MCP,通常会用 HTTP 或 SSE。

Streamable HTTP

TransportConfig config = TransportConfig.streamableHttp("https://example.com/mcp");
config.setHeaders(Collections.singletonMap("Authorization", "Bearer your-token"));

McpTransport transport = McpTransportFactory.createTransport("streamable_http", config);
McpClient client = new McpClient("demo-client", "1.0.0", transport);
client.connect().join();

这个示例默认使用 AUTO。对未知或迁移中的 Streamable HTTP peer,它只通过 server/discover 做有限兼容探测;它不会自动选择 HTTP+SSE。对已知的 session-handshake server,显式设置:

config.withProtocolProfile(McpProtocolProfile.LEGACY_2025_03_26);

完整请求头、server profile 和升级路线见 Streamable HTTP 传输

SSE

McpTransport transport = new SseTransport("https://example.com/sse");
McpClient client = new McpClient("demo-client", "1.0.0", transport);
client.connect().join();

这两条路径的区别,不在 McpClient,而在 transport 连接模型。HTTP+SSE 是 sse 的显式 transport;不要把它当作 Streamable HTTP AUTO 的回退目标。

6. McpClient 提供的高层 API 不止 Tool

当前高层 API 至少覆盖:

  • getAvailableTools() -> tools/list
  • callTool(name, args) -> tools/call
  • getAvailableResources() -> resources/list
  • readResource(uri) -> resources/read
  • getAvailablePrompts() -> prompts/list
  • getPrompt(name, arguments) -> prompts/get

6.1 Resource 示例

List<McpResource> resources = client.getAvailableResources().join();
McpResourceContent resource = client.readResource("file://docs/README.md").join();

6.2 Prompt 示例

List<McpPrompt> prompts = client.getAvailablePrompts().join();
McpPromptResult prompt = client.getPrompt(
"code_review_prompt",
Collections.<String, Object>singletonMap("language", "java")
).join();

这说明 MCP 在 AI4J 里不是单纯“远程 function call 协议”,而是覆盖 tool / resource / prompt 三类能力。

7. 缓存语义要先理解

McpClient 会缓存:

  • availableTools
  • availableResources
  • availablePrompts

这有两个直接后果:

7.1 好处

  • 不必每次都重复 list
  • 会话级调用开销更低

7.2 边界

断线或重连后,缓存可能失效,因此 client 在断开时会清掉这些缓存。

这也是为什么 disconnect() 不只是“关连接”,还会清空状态。

8. 心跳和自动重连不是可有可无的细节

8.1 心跳

若 transport needsHeartbeat() 返回 true,client 会启动低频 heartbeat 检查。

当前实现是:

  • 每 10 分钟做一次 getAvailableTools() 检查

这更像保底存活检测,而不是高频 keepalive。

8.2 自动重连

McpClient 默认:

  • autoReconnect = true

断线后会:

  • 清缓存
  • 停 heartbeat
  • 停 transport
  • 取消 pending requests
  • 5 秒后尝试重连

这意味着它已经具备基础会话恢复能力,但不是复杂的连接池。

8.3 关闭自动重连

默认的 3 参构造器把 autoReconnect 固定为 true

new McpClient("demo-client", "1.0.0", transport)
// 等价于 new McpClient("demo-client", "1.0.0", transport, true)

如果你希望自己掌控连接生命周期(例如由上层 gateway、编排器统一调度重连,或短生命周期一次性调用),用 4 参构造器显式关闭:

McpClient client = new McpClient("demo-client", "1.0.0", transport, false);

关闭后的行为差异:

  • 断线时仍会清缓存、停 heartbeat、停 transport、取消 pending requests
  • 不再调度重连(日志会打印 自动重连已禁用,跳过MCP重连
  • 后续是否重连完全由调用方决定,通常做法是重新 new McpClient(...)connect()
Gateway 创建的 client 默认开启重连

McpGateway 经由 McpGatewayClientFactory 创建 client 时使用默认的 3 参构造器,即 autoReconnect = true。当前没有配置项把 autoReconnect 从配置文件注入到 gateway 创建链路(见 配置与网关参考 - autoReconnect 字段)。需要关闭时,应在 gateway 外自行构造 client 再用 addMcpClient(...) 接入。

9. callTool() 的失败语义要分两层看

这里是一个容易写错的点。

9.1 连接态失败

如果 client 没连接或没初始化,callTool() 会直接返回 exceptional future。

9.2 协议层失败

如果服务端返回 MCP error response,当前实现通常会把错误压成字符串返回,而不是一定抛异常。

协议层失败不一定抛异常

因此调用方不要只抓异常,也要检查返回内容是否是失败文本。

10. 推荐的接入姿势

单服务模式最稳的使用方式是:

  1. 构造 transport
  2. connect().join()
  3. getAvailableTools() 看真实暴露名
  4. callTool(...)
  5. finally 中 disconnect().join()

这样可以显著降低排障成本,因为工具名、权限、连接问题会在更靠前的步骤暴露出来。

11. 常见排障路径

11.1 not connected or not initialized

先检查:

  • 有没有先 connect()
  • transport/profile 是否与目标 peer 匹配;AUTO 的 server/discover 是否收到允许回退的响应
  • 只有 legacy profile 才检查初始化握手是否真正完成;现代 HTTP 则检查请求 metadata 和 HTTP headers 是否被代理保留

11.2 tool not found

先检查:

  • getAvailableTools() 是否能看到这个名字
  • 你调用的是 MCP 暴露名,而不是自己的别名

11.3 resource not found / prompt not found

先检查:

  • getAvailableResources()
  • getAvailablePrompts()

11.4 HTTP 401 / 403

先检查:

  • TransportConfig.headers
  • token 是否真的打进请求

12. 什么时候应该离开这页

一旦你已经:

  • 接了不止一个 MCP
  • 需要用户级隔离
  • 需要工具来源治理

就不该继续停留在单服务模式,应切到: