What is the difference between McpSyncClient and McpAsyncClient, and how do the transport choices relate to picking one?
answer
- sync = blocking, returns result on caller thread
- async = Reactor Mono/Flux, non-blocking
- sync usually wraps async
- spring.ai.mcp.client.type=SYNC|ASYNC
- ASYNC<->WebFlux, SYNC<->MVC; keep it consistent
basics
~20 sMcpSyncClient is blocking — calls return results directly. McpAsyncClient is reactive — calls return Reactor Mono/Flux and never block a thread. You pick via spring.ai.mcp.client.type=SYNC or ASYNC; async fits reactive/WebFlux stacks, sync fits imperative apps.
solid answer
~40 sBoth are MCP client abstractions from the MCP Java SDK, differing only in threading model. McpSyncClient exposes blocking methods (e.g. listTools, callTool) that return the result on the calling thread — natural for imperative Spring MVC apps. McpAsyncClient exposes the same operations returning Project Reactor types (Mono/Flux), so no thread blocks on I/O — natural for WebFlux/reactive pipelines. Under the hood the sync client typically wraps the async one. In Spring AI you rarely instantiate them directly: spring.ai.mcp.client.type=SYNC|ASYNC selects which the auto-config creates, and correspondingly whether the ToolCallbackProvider is Sync- or Async-based. Match the choice to your app: a blocking MVC ChatClient flow with SYNC and stdio/SSE; a reactive flow with ASYNC and the WebFlux transport. Mixing a reactive transport with blocking consumption (or vice-versa) invites thread-starvation or unnecessary complexity.
go deeper
Know sync = blocking, async = reactive, and you pick with a property.
Explain the return-type difference (direct value vs Mono/Flux) and that type selects the auto-configured client.
Tie the choice to the app's threading model/transport and articulate thread-starvation and error-handling differences.
Standardize the reactive-vs-imperative decision across services, including timeout/retry strategy and transport pairing, to avoid mixed-model foot-guns.
## Two clients, one protocol The MCP Java SDK offers two client faces over the same protocol: - **`McpSyncClient`** — **blocking** API. Methods like `listTools()` and `callTool(...)` execute and return the result on the caller's thread. Errors surface as thrown exceptions. This is the ergonomic fit for **imperative** code — Spring MVC controllers, ordinary services, a blocking `ChatClient` call. - **`McpAsyncClient`** — **reactive** API built on **Project Reactor**. The same operations return `Mono<...>`/`Flux<...>`; nothing blocks the calling thread, and composition/backpressure use the reactive operators. This fits **Spring WebFlux** and end-to-end non-blocking pipelines. Typically the **sync client is a thin blocking wrapper around the async client** — the async one is the primitive. ## How you actually choose it in Spring AI You usually don't `new` these. The client starter's auto-configuration reads `spring.ai.mcp.client.type`: ```properties spring.ai.mcp.client.type=SYNC # or ASYNC ``` That determines whether it builds `McpSyncClient`s and a `SyncMcpToolCallbackProvider`, or `McpAsyncClient`s and an `AsyncMcpToolCallbackProvider`. On the **server** side there's a parallel `spring.ai.mcp.server.type=SYNC|ASYNC`. ## Transport interaction Transport (stdio vs HTTP/SSE) and threading model are related but distinct: - **stdio** — server as child process over stdin/stdout; works with either type. - **HTTP/SSE** — networked; the **WebFlux** server/client variants are the reactive path and pair naturally with **ASYNC**; the **WebMVC** server variant is the blocking path pairing with **SYNC**. The practical rule: **keep the threading model consistent end to end.** A reactive WebFlux app should generally use ASYNC so tool calls stay non-blocking; a blocking MVC app should use SYNC so you don't drag Reactor types through imperative code for no benefit. ## Why it matters - **Thread economy:** in a reactive app, a blocking MCP call on an event-loop thread can cause **thread starvation**. Async avoids that. - **Simplicity:** in an imperative app, forcing async just adds Reactor ceremony with no throughput win. - **Error handling differs:** sync throws; async signals errors through the reactive stream (`onError`), so retry/timeout operators differ. ## Gotchas - Don't call a **blocking** McpSyncClient from a WebFlux event-loop thread. - Timeouts/retries are configured differently per model — plan for remote failures either way. - The choice cascades: client `type` also dictates which `ToolCallbackProvider` flavor you inject, so keep it aligned with how your `ChatClient` is invoked.
- Why is it risky to use McpSyncClient inside a WebFlux application?A blocking call on a Reactor event-loop thread can starve the small event-loop pool and stall unrelated requests. In WebFlux you want the async client so MCP I/O stays non-blocking.
saying these in an interview costs you the question
- Claiming sync and async differ in which MCP features they support
- Calling a blocking McpSyncClient from a WebFlux event-loop thread
- Thinking you instantiate the clients manually rather than selecting via spring.ai.mcp.client.type