Croway opened a new pull request, #25270: URL: https://github.com/apache/camel/pull/25270
Fixes [CAMEL-23078](https://issues.apache.org/jira/browse/CAMEL-23078). Two independent MCP improvements to `camel-openai`, one per commit. ## 1. Optional parallel MCP tool execution A model can request several tool calls in a single response, and those calls are independent by design, but they were executed one after another in `OpenAIProducer.processNonStreamingAgentic` and `OpenAIToolExecutionProducer.process`, so a batch took as long as the sum of its tools. - New `parallelToolExecution` option (default `false`, so existing routes are unaffected) dispatches a batch concurrently. - New `parallelToolTimeout` option (milliseconds, default `0` = disabled) bounds the batch as a whole so one slow tool cannot block it. A call that exceeds it is cancelled and then handled according to `toolExecutionErrorStrategy`. Left disabled by default because `mcpTimeout` already bounds each individual MCP request. - Results are collected positionally and always fed back to the model in the order it requested the tools, since the OpenAI API pairs each `tool` message with its `tool_call_id`. - A batch of a single tool call still runs inline, with no thread hand-off. - MDC is carried onto the worker threads so tool-call logs stay correlated with the exchange. The thread pool comes from Camel's `ExecutorServiceManager` rather than being hand-rolled. That matters beyond tidiness: it follows the configured thread pool profile, is exposed over JMX, is shut down with the `CamelContext`, and — via `DefaultThreadPoolFactory` — automatically becomes a thread-per-task **virtual thread** executor when `camel.main.virtualThreadsEnabled=true`. The blocking done on those threads is `McpSyncClient.callTool`, which blocks on a Reactor `CountDownLatch` rather than inside a `synchronized` block, and the endpoint's shared state is guarded by `ReentrantLock`, so nothing pins a carrier thread. ### Refactor included in the same commit The per-batch execution logic was duplicated almost line for line between the two producers. It is extracted into a shared `McpToolCallExecutor`, which keeps hallucinated tool name handling, argument parsing, the error strategies and `returnDirect` detection from drifting between the two paths, and gives both the new option. The executor also snapshots the immutable `McpToolState` once per batch, so every call in a batch sees a consistent view. ### Behaviour change (documented in the upgrade guide) With `parallelToolExecution=true` **and** `toolExecutionErrorStrategy=failExchange`, the sibling calls already dispatched complete before the exchange fails, instead of being abandoned mid-loop. Routes whose tools have side effects and that rely on a failure preventing later calls in the same batch should keep the sequential default. ## 2. Refresh the MCP tool list on `tools/list_changed` MCP servers announce tool additions, removals and changes with a `tools/list_changed` notification, but the component ignored it: the advertised tool list was fixed at endpoint startup and was only ever re-listed as a side effect of a transport reconnect. A long-running route therefore kept offering the model a stale set of tools. - A `toolsChangeConsumer` is registered on each MCP client, and the tool state is republished when it fires. The MCP SDK already re-lists the tools before invoking the consumer, so there is no extra round trip. - The list the SDK passes is **unfiltered**, so the per-server `toolNames` include list is re-applied before publishing — a server cannot widen its exposed tool set by announcing new tools. - New `mcpToolRefresh` option, default `true`, since honouring the notification is what the MCP specification expects of a client. Set it to `false` to pin the tool list to endpoint startup for deployments that require a deterministic set of tools. Documented in the upgrade guide. - A notification arriving from a client that a reconnect has already superseded is discarded, so a stale in-flight notification cannot map tools back to a dead client. ### Refactor included in the same commit The per-server state rebuild was inlined in `doReconnectMcpServer`; it is extracted into `republishServerTools` and shared with the refresh path, so duplicate-name handling (CAMEL-23958), the `toolNames` filter (CAMEL-23964) and `returnDirect` detection cannot diverge between reconnect and refresh. ### Drive-by fix `returnDirect` flags set programmatically through `addReturnDirectTool` / `removeReturnDirectTool` were silently discarded whenever the tool state was rebuilt. They are now remembered and re-applied — except for tools that no longer exist, which stay fully pruned, preserving the guarantee asserted by the CAMEL-23957 reconnect tests. ## Notes on the original issue description The description predates CAMEL-23957, which replaced the individual `cachedMcpTools` / `toolClientMap` / `returnDirectTools` fields with the immutable `McpToolState` record guarded by a global lock. The refresh therefore publishes a new snapshot rather than mutating maps, as the description suggested. `openai:responses` passes hosted MCP tools straight through to the API and does not execute tools locally, so it is out of scope for both changes. ## Testing `mvn test -pl components/camel-ai/camel-openai` — 181 tests, all green, including the pre-existing MCP reconnect, tool-filtering and error-strategy suites. New tests: - `McpToolCallExecutorTest` (10) — ordering, `returnDirect` per call, both error strategies, hallucinated tool names, and the batch timeout. Concurrency is proven with a `CountDownLatch` rendezvous that only completes if the calls are dispatched in parallel, rather than by measuring wall-clock time, so there is no timing flakiness and no `Thread.sleep`. - `OpenAIParallelToolExecutionTest` (3) — the same, end to end through a route using `camel-test-infra-openai-mock` (`andInvokeTool` emits several tool calls in one response). - `OpenAIEndpointMcpToolRefreshTest` (13) — tools added, removed, `toolNames` filter re-applied, duplicates across servers, other servers untouched, `returnDirect` annotations and manual overrides, refresh disabled, superseded client, unknown server, and a notification arriving before the client is ready. One gap worth flagging for review: the two lines that register the consumer on the SDK builder are not covered by an automated test. The MCP Java SDK ships no in-memory transport, and there is no precedent in this repo for an embedded MCP server in tests, so covering it would mean adding a servlet container as a test dependency purely for that. The refresh logic itself — where all the behaviour lives — is unit tested directly through `onToolsChanged`. --- _Claude Code on behalf of Croway_ -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
