CAMEL-23078: camel-openai - Parallel MCP tool execution and runtime tool refresh - #25270
Open
Croway wants to merge 2 commits into
Open
CAMEL-23078: camel-openai - Parallel MCP tool execution and runtime tool refresh#25270Croway wants to merge 2 commits into
Croway wants to merge 2 commits into
Conversation
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 so a batch took as long as the sum of its tools. Add a `parallelToolExecution` option (default false, so existing routes are unaffected) that dispatches a batch concurrently, and a `parallelToolTimeout` option that bounds the batch as a whole so one slow tool cannot block it. Results are collected positionally and always fed back to the model in the order it requested the tools, as the OpenAI API pairs each tool message with its tool_call_id. A batch of a single tool call still runs inline. The thread pool is obtained from Camel's ExecutorServiceManager rather than being hand-rolled, so it follows the configured thread pool profile, is exposed over JMX, is shut down with the CamelContext, and automatically becomes a thread-per-task virtual thread executor when virtual threads are enabled. The per-batch execution logic was duplicated between the agentic loop in OpenAIProducer and the manual loop in OpenAIToolExecutionProducer, so it is extracted into a shared McpToolCallExecutor. That keeps hallucinated tool name handling, argument parsing, the error strategies and returnDirect detection from drifting between the two paths, and gives both the parallel option. The executor also snapshots the immutable McpToolState once per batch, so all calls in a batch see a consistent view of the tools. With toolExecutionErrorStrategy=failExchange the sibling calls already dispatched now complete before the exchange fails, instead of being abandoned mid-loop; this is documented in the upgrade guide. Concurrency is covered deterministically by unit tests that rendezvous the tool calls on a latch which can only be released if they run at the same time. OpenAIMcpParallelToolExecutionIT additionally covers the option end to end against the MCP Everything Server; reaching the parallel path there depends on the model emitting multi-call batches, which test_execution.md documents. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Croway
force-pushed
the
CAMEL-23078-openai-mcp-parallel-tools-and-refresh
branch
from
July 31, 2026 08:17
e87bd35 to
57a4e9d
Compare
Contributor
|
🌟 Thank you for your contribution to the Apache Camel project! 🌟 🐫 Apache Camel Committers, please review the following items:
|
Croway
force-pushed
the
CAMEL-23078-openai-mcp-parallel-tools-and-refresh
branch
from
July 31, 2026 09:09
57a4e9d to
14ff372
Compare
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 to whatever was listed when the endpoint started, 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. Register a toolsChangeConsumer on each MCP client and republish the tool state when it fires. The MCP SDK already re-lists the tools before invoking the consumer, so no extra round trip is needed, but the list it passes is unfiltered and the per-server `toolNames` include list is re-applied before publishing, so a server cannot widen its exposed tool set by announcing new tools. The new `mcpToolRefresh` option (default true, matching what the MCP specification expects of a client) pins the tool list to endpoint startup when set to false. The per-server state rebuild was inlined in doReconnectMcpServer; it is extracted into republishServerTools and shared with the refresh path so that duplicate name handling, the toolNames filter and returnDirect detection cannot drift between the two. 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. Also fixes returnDirect flags set programmatically through addReturnDirectTool and removeReturnDirectTool being 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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Croway
force-pushed
the
CAMEL-23078-openai-mcp-parallel-tools-and-refresh
branch
from
July 31, 2026 09:38
14ff372 to
0f5a79d
Compare
oscerd
approved these changes
Jul 31, 2026
Contributor
|
🧪 CI tested the following changed modules:
🔬 Scalpel shadow comparison — Scalpel: 12 tested, 26 compile-only — current: 9 all testedMaveniverse Scalpel detected 38 affected modules (current approach: 9).
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes 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.processNonStreamingAgenticandOpenAIToolExecutionProducer.process, so a batch took as long as the sum of its tools.parallelToolExecutionoption (defaultfalse, so existing routes are unaffected) dispatches a batch concurrently.parallelToolTimeoutoption (milliseconds, default0= 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 totoolExecutionErrorStrategy. Left disabled by default becausemcpTimeoutalready bounds each individual MCP request.toolmessage with itstool_call_id.The thread pool comes from Camel's
ExecutorServiceManagerrather than being hand-rolled. That matters beyond tidiness: it follows the configured thread pool profile, is exposed over JMX, is shut down with theCamelContext, and — viaDefaultThreadPoolFactory— automatically becomes a thread-per-task virtual thread executor whencamel.main.virtualThreadsEnabled=true. The blocking done on those threads isMcpSyncClient.callTool, which blocks on a ReactorCountDownLatchrather than inside asynchronizedblock, and the endpoint's shared state is guarded byReentrantLock, 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 andreturnDirectdetection from drifting between the two paths, and gives both the new option. The executor also snapshots the immutableMcpToolStateonce per batch, so every call in a batch sees a consistent view.Behaviour change (documented in the upgrade guide)
With
parallelToolExecution=trueandtoolExecutionErrorStrategy=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_changedMCP servers announce tool additions, removals and changes with a
tools/list_changednotification, 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.toolsChangeConsumeris 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.toolNamesinclude list is re-applied before publishing — a server cannot widen its exposed tool set by announcing new tools.mcpToolRefreshoption, defaulttrue, since honouring the notification is what the MCP specification expects of a client. Set it tofalseto pin the tool list to endpoint startup for deployments that require a deterministic set of tools. Documented in the upgrade guide.Refactor included in the same commit
The per-server state rebuild was inlined in
doReconnectMcpServer; it is extracted intorepublishServerToolsand shared with the refresh path, so duplicate-name handling (CAMEL-23958), thetoolNamesfilter (CAMEL-23964) andreturnDirectdetection cannot diverge between reconnect and refresh.Drive-by fix
returnDirectflags set programmatically throughaddReturnDirectTool/removeReturnDirectToolwere 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/returnDirectToolsfields with the immutableMcpToolStaterecord guarded by a global lock. The refresh therefore publishes a new snapshot rather than mutating maps, as the description suggested.openai:responsespasses hosted MCP tools straight through to the API and does not execute tools locally, so it is out of scope for both changes.Testing
Unit tests:
mvn test -pl components/camel-ai/camel-openai— 182 tests, all green, including the pre-existing MCP reconnect, tool-filtering and error-strategy suites.McpToolCallExecutorTest(10) — ordering,returnDirectper call, both error strategies, hallucinated tool names, and the batch timeout. Concurrency is proven with aCountDownLatchrendezvous that only completes if the calls are dispatched in parallel, rather than by measuring wall-clock time, so there is no timing flakiness and noThread.sleep.OpenAIParallelToolExecutionTest(3) — the same, end to end through a route usingcamel-test-infra-openai-mock(andInvokeToolemits several tool calls in one response).OpenAIEndpointMcpToolRefreshTest(13) — tools added, removed,toolNamesfilter re-applied, duplicates across servers, other servers untouched,returnDirectannotations and manual overrides, refresh disabled, superseded client, unknown server, and a notification arriving before the client is ready.Integration tests: run locally against Ollama and the dockerised MCP Everything Server — 21 ITs green across the whole suite (
OpenAIChatCompletionIT,OpenAIEmbeddingsIT,OpenAIMcpToolsIT,OpenAIMcpAdvancedIT,OpenAIMcpManualToolLoopIT,OpenAIMcpConversationStoreIT, and the newOpenAIMcpParallelToolExecutionIT).OpenAIMcpParallelToolExecutionITis new and coversparallelToolExecution=trueend to end, including the batch timeout and a comparison against the sequential default. Reaching the parallel dispatch path depends on the model emitting more than one tool call in a single response, since a batch of one runs inline — so it is asserted on outcome rather than on batch size, which would be flaky. Verified withqwen3.5:2b-mlx, which reliably produces two-call batches for these prompts; theMcpToolCallExecutordebug log confirmsExecuting 2 tool call(s) in parallel, so the concurrent path really is exercised against a real MCP server.test_execution.mdrecords the model choice and how to confirm it.This also closes the gap noted in the earlier revision of this description: the
toolsChangeConsumerregistration is now exercised against a real MCP server over Streamable HTTP by every MCP IT, sincemcpToolRefreshdefaults totrue. What remains uncovered by automation is only the firing of the callback, which would need an MCP server that mutates its tool list on demand; the handler behaviour itself is unit tested directly throughonToolsChanged.One unrelated observation from the IT runs:
OpenAIMcpToolsIT.testAgenticLoopHeadersasserts that at least one tool iteration occurred, which flakes with small models that sometimes answer without calling a tool. It is pre-existing and passes on retry.Claude Code on behalf of Croway