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]

Reply via email to