Skip to main content

MCP Client Integration (Single-Server Mode)

This page covers only "single-server mode".

In other words, the questions in front of you right now are:

  • Connect to a single MCP server
  • Determine whether it speaks modern stateless HTTP or the session-era protocol
  • Get tool / resource / prompt, the three high-level APIs, working end to end

If you have already moved on to multiple services or per-user isolation, you should read the gateway docs instead of staying on this page.

1. The real lifecycle of McpClient

McpClient is not something you can callTool() on the moment it is constructed.

Its real lifecycle is:

  1. Construct the transport
  2. new McpClient(...)
  3. connect()
  4. Become ready per transport/profile: the default AUTO does limited probing first; modern HTTP does no handshake; legacy transports complete the initialization handshake
  5. Read tools / resources / prompts
  6. Invoke capabilities
  7. disconnect()

Missing any step in between means it is not a stable client.

2. What connect() actually does

connect() always runs transport.start() first, with a 30s timeout, and then takes different paths depending on transport/profile:

  • The default StreamableHttpTransport uses AUTO. It only sends the modern server/discover first; a valid modern discovery result selects 2026-07-28 and does not send initialize or notifications/initialized.
  • AUTO falls back to initialization-era Streamable HTTP only when that probe receives an unrecognized HTTP 400, 404, or 405, and then runs initialize -> notifications/initialized. Authentication failures, recognizable modern JSON-RPC errors, and invalid discovery responses do not trigger the downgrade.
  • STDIO, HTTP+SSE with explicit type: "sse", and clients that explicitly select the legacy Streamable HTTP profile keep the initialization-era flow.
  • If the transport needs a heartbeat, the client starts the heartbeat after it is ready.

Therefore isConnected() and isInitialized() are still pre-call checks, but for modern HTTP isInitialized() means "client ready", not "a protocol handshake happened".

3. McpClient capability boundary

AI4J's high-level APIs cover Tool, Resource, and Prompt. The capabilities declared per request on modern HTTP are conservative: it does not claim support for sampling, roots, elicitation, MRTR, or subscriptions until these multi-turn protocol capabilities are actually available.

Do not infer from old session-era initialize examples that a modern peer also supports these optional capabilities. Trust the target server's protocol profile and its actual capability catalog.

4. Minimal integration example: STDIO

If you are connecting to a local subprocess MCP server, the shortest path is usually 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();

This path validates:

  • Whether the subprocess can start
  • Whether the stdio transport can handshake
  • Whether tools/list and tools/call work

5. Minimal integration example: Streamable HTTP / SSE

If you are integrating a service-style MCP, you will usually use HTTP or 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();

This example uses AUTO by default. For unknown or in-migration Streamable HTTP peers, it only does limited compatibility probing via server/discover; it does not auto-select HTTP+SSE. For a known session-handshake server, set it explicitly:

config.withProtocolProfile(McpProtocolProfile.LEGACY_2025_03_26);

For full request headers, server profiles, and the upgrade path, see Streamable HTTP.

SSE

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

The difference between these two paths is not in McpClient but in the transport connection model. HTTP+SSE is the explicit sse transport; do not treat it as the fallback target of Streamable HTTP AUTO.

6. The high-level APIs on McpClient are more than just Tool

The current high-level APIs cover at least:

  • 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 example

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

6.2 Prompt example

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

This shows that MCP inside AI4J is not simply a "remote function call protocol" — it covers all three capability types: tool / resource / prompt.

7. Understand the caching semantics first

McpClient caches:

  • availableTools
  • availableResources
  • availablePrompts

This has two direct consequences:

7.1 Benefits

  • No need to repeat the list call every time
  • Lower per-session call overhead

7.2 Boundary

After a disconnect or reconnect, the cache may be invalid, so the client clears these caches on disconnect.

This is also why disconnect() is not just "close the connection" — it also resets the state.

8. Heartbeat and auto-reconnect are not optional details

8.1 Heartbeat

If the transport's needsHeartbeat() returns true, the client starts a low-frequency heartbeat check.

The current implementation is:

  • Run a getAvailableTools() check once every 10 minutes

This is more of a last-resort liveness check than a high-frequency keepalive.

8.2 Auto-reconnect

McpClient defaults to:

  • autoReconnect = true

On disconnect it will:

  • Clear the cache
  • Stop the heartbeat
  • Stop the transport
  • Cancel pending requests
  • Attempt to reconnect after 5 seconds

This means it already has basic session-recovery capability, but it is not a sophisticated connection pool.

8.3 Disabling auto-reconnect

The default 3-argument constructor pins autoReconnect to true:

new McpClient("demo-client", "1.0.0", transport)
// equivalent to new McpClient("demo-client", "1.0.0", transport, true)

If you want to own the connection lifecycle yourself (for example, letting an upper-layer gateway or orchestrator schedule reconnects uniformly, or doing one-shot short-lived calls), disable it explicitly with the 4-argument constructor:

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

Behavioral differences once disabled:

  • On disconnect it still clears the cache, stops the heartbeat, stops the transport, and cancels pending requests
  • But it no longer schedules a reconnect (the log will print 自动重连已禁用,跳过MCP重连)
  • Whether to reconnect afterwards is entirely up to the caller; the usual approach is to new McpClient(...) again and then connect()
Gateway-created clients have reconnect on by default

McpGateway uses the default 3-argument constructor when creating a client via McpGatewayClientFactory, i.e. autoReconnect = true. There is currently no config option to inject autoReconnect from the configuration file into the gateway creation path (see Configuration & Gateway Reference - autoReconnect field). To disable it, construct the client outside the gateway yourself and register it via addMcpClient(...).

9. Look at the failure semantics of callTool() in two layers

This is an easy place to get wrong.

9.1 Connection-state failure

If the client is not connected or not initialized, callTool() returns an exceptional future directly.

9.2 Protocol-layer failure

If the server returns an MCP error response, the current implementation usually flattens the error into a string instead of necessarily throwing an exception.

Protocol-layer failure does not always throw

Therefore the caller must not only catch exceptions — it must also check whether the returned content is failure text.

The most stable way to use single-server mode is:

  1. Construct the transport
  2. connect().join()
  3. Call getAvailableTools() first to see the real exposed names
  4. Then callTool(...)
  5. disconnect().join() in a finally

This significantly lowers troubleshooting cost, because tool names, permissions, and connection issues surface at earlier steps.

11. Common troubleshooting paths

11.1 not connected or not initialized

Check first:

  • Did you call connect() first
  • Whether the transport/profile matches the target peer; whether AUTO's server/discover received a response that allows fallback
  • Only the legacy profile checks whether the initialization handshake truly completed; for modern HTTP, check whether request metadata and HTTP headers are preserved by the proxy

11.2 tool not found

Check first:

  • Whether getAvailableTools() can see that name
  • Whether you are calling the MCP-exposed name, not your own alias

11.3 resource not found / prompt not found

Check first:

  • getAvailableResources()
  • getAvailablePrompts()

11.4 HTTP 401 / 403

Check first:

  • TransportConfig.headers
  • Whether the token is actually being sent in the request

12. When to leave this page

Once you have:

  • Connected more than one MCP
  • A need for per-user isolation
  • A need for tool-source governance

you should no longer stay in single-server mode — switch to: