This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 029a51c451d82598331c4bf6050656a2857424b4
Author: croway <[email protected]>
AuthorDate: Fri Jul 31 09:48:37 2026 +0200

    CAMEL-23078: camel-openai - 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 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) <[email protected]>
---
 .../apache/camel/catalog/components/openai.json    |  60 ++--
 .../component/openai/OpenAIEndpointConfigurer.java |  12 +
 .../component/openai/OpenAIEndpointUriFactory.java |   4 +-
 .../org/apache/camel/component/openai/openai.json  |  60 ++--
 .../camel-openai/src/main/docs/openai-mcp.adoc     |  69 ++++
 .../component/openai/McpToolCallExecutor.java      | 272 +++++++++++++++
 .../component/openai/OpenAIConfiguration.java      |  32 ++
 .../camel/component/openai/OpenAIProducer.java     | 112 ++-----
 .../openai/OpenAIToolExecutionProducer.java        |  97 ++----
 .../component/openai/McpToolCallExecutorTest.java  | 364 +++++++++++++++++++++
 .../openai/OpenAIParallelToolExecutionTest.java    | 168 ++++++++++
 .../OpenAIMcpParallelToolExecutionIT.java          | 132 ++++++++
 components/camel-ai/camel-openai/test_execution.md |  27 ++
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  12 +
 14 files changed, 1198 insertions(+), 223 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
index 75596ffafff6..c1c640d00adc 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/openai.json
@@ -122,34 +122,36 @@
     "mcpTimeout": { "index": 33, "kind": "parameter", "displayName": "Mcp 
Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 20, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Timeout in seconds for MCP tool call requests. 
Applies to all MCP operations inclu [...]
     "model": { "index": 34, "kind": "parameter", "displayName": "Model", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for chat completion" },
     "outputClass": { "index": 35, "kind": "parameter", "displayName": "Output 
Class", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Fully qualified class name for structured 
output using response format" },
-    "previousResponseId": { "index": 36, "kind": "parameter", "displayName": 
"Previous Response Id", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Previous response id for 
OpenAI server-side conversation state (Responses [...]
-    "requestTimeout": { "index": 37, "kind": "parameter", "displayName": 
"Request Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 0, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "HTTP request timeout in milliseconds for the 
OpenAI SDK client. When 0 or  [...]
-    "speechInstructions": { "index": 38, "kind": "parameter", "displayName": 
"Speech Instructions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Optional instructions to 
control the voice of the generated audio. Does no [...]
-    "speechModel": { "index": 39, "kind": "parameter", "displayName": "Speech 
Model", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for text-to-speech (e.g., 
gpt-4o-mini-tts, tts-1, tts-1-hd)" },
-    "speechResponseFormat": { "index": 40, "kind": "parameter", "displayName": 
"Speech Response Format", "group": "producer", "label": "", "required": false, 
"type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", 
"flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "mp3", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "descrip [...]
-    "speechSpeed": { "index": 41, "kind": "parameter", "displayName": "Speech 
Speed", "group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The speed of the generated audio, from 0.25 to 
4.0 where 1.0 is normal speed" },
-    "speechVoice": { "index": 42, "kind": "parameter", "displayName": "Speech 
Voice", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "alloy", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The voice to use for 
text-to-speech (e.g., alloy, echo, fable,  [...]
-    "storeFullResponse": { "index": 43, "kind": "parameter", "displayName": 
"Store Full Response", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Store the full SDK response in non-streaming 
mode: chat-comp [...]
-    "streaming": { "index": 44, "kind": "parameter", "displayName": 
"Streaming", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Enable streaming 
responses" },
-    "stripThinking": { "index": 45, "kind": "parameter", "displayName": "Strip 
Thinking", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Strip ... blocks from 
model responses (used by reasoning models like  [...]
-    "systemMessage": { "index": 46, "kind": "parameter", "displayName": 
"System Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "System message to 
prepend. When set and conversationMemory is enabled, the conversat [...]
-    "temperature": { "index": 47, "kind": "parameter", "displayName": 
"Temperature", "group": "producer", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Temperature for response 
generation (0.0 to 2.0)" },
-    "toolExecutionErrorStrategy": { "index": 48, "kind": "parameter", 
"displayName": "Tool Execution Error Strategy", "group": "producer", "label": 
"", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ 
"failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "failExchange", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...]
-    "topP": { "index": 49, "kind": "parameter", "displayName": "Top P", 
"group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Top P for response generation (0.0 to 1.0)" },
-    "userMessage": { "index": 50, "kind": "parameter", "displayName": "User 
Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Default user message 
text to use when no prompt is provided" },
-    "lazyStartProducer": { "index": 51, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
-    "oauthProfile": { "index": 52, "kind": "parameter", "displayName": "Oauth 
Profile", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "OAuth profile name for 
obtaining an access token via the OAuth 2.0 Client Cred [...]
-    "sslContextParameters": { "index": 53, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "SSLContextParameters to 
use [...]
-    "sslEndpointAlgorithm": { "index": 54, "kind": "parameter", "displayName": 
"Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "security": 
"insecure:ssl", "insecureValue": "none", "defaultValue": "https", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", [...]
-    "sslKeymanagerAlgorithm": { "index": 55, "kind": "parameter", 
"displayName": "Ssl Keymanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "SunX509", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the key  [...]
-    "sslKeyPassword": { "index": 56, "kind": "parameter", "displayName": "Ssl 
Key Password", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password of the private key in the key 
store file" },
-    "sslKeystoreLocation": { "index": 57, "kind": "parameter", "displayName": 
"Ssl Keystore Location", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The location of the key 
store file. This is optional and can be [...]
-    "sslKeystorePassword": { "index": 58, "kind": "parameter", "displayName": 
"Ssl Keystore Password", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The store password for the key store file" },
-    "sslKeystoreType": { "index": 59, "kind": "parameter", "displayName": "Ssl 
Keystore Type", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the key store file" },
-    "sslProtocol": { "index": 60, "kind": "parameter", "displayName": "Ssl 
Protocol", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"TLSv1.3", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The SSL protocol used to generate the 
SSLContext" },
-    "sslTrustmanagerAlgorithm": { "index": 61, "kind": "parameter", 
"displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "PKIX", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the tru [...]
-    "sslTruststoreLocation": { "index": 62, "kind": "parameter", 
"displayName": "Ssl Truststore Location", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The location of the trust store file, used to 
validate the  [...]
-    "sslTruststorePassword": { "index": 63, "kind": "parameter", 
"displayName": "Ssl Truststore Password", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password for the trust store file. [...]
-    "sslTruststoreType": { "index": 64, "kind": "parameter", "displayName": 
"Ssl Truststore Type", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the trust store file" }
+    "parallelToolExecution": { "index": 36, "kind": "parameter", 
"displayName": "Parallel Tool Execution", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "deprecationNote": "", "autowired": false, "secret": false, 
"defaultValue": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Execute the tool calls returned by the model 
in a si [...]
+    "parallelToolTimeout": { "index": 37, "kind": "parameter", "displayName": 
"Parallel Tool Timeout", "group": "producer", "label": "", "required": false, 
"type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "defaultValue": 0, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Timeout in milliseconds 
for a batch of parallel tool calls, so  [...]
+    "previousResponseId": { "index": 38, "kind": "parameter", "displayName": 
"Previous Response Id", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Previous response id for 
OpenAI server-side conversation state (Responses [...]
+    "requestTimeout": { "index": 39, "kind": "parameter", "displayName": 
"Request Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 0, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "HTTP request timeout in milliseconds for the 
OpenAI SDK client. When 0 or  [...]
+    "speechInstructions": { "index": 40, "kind": "parameter", "displayName": 
"Speech Instructions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Optional instructions to 
control the voice of the generated audio. Does no [...]
+    "speechModel": { "index": 41, "kind": "parameter", "displayName": "Speech 
Model", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for text-to-speech (e.g., 
gpt-4o-mini-tts, tts-1, tts-1-hd)" },
+    "speechResponseFormat": { "index": 42, "kind": "parameter", "displayName": 
"Speech Response Format", "group": "producer", "label": "", "required": false, 
"type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", 
"flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "mp3", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "descrip [...]
+    "speechSpeed": { "index": 43, "kind": "parameter", "displayName": "Speech 
Speed", "group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The speed of the generated audio, from 0.25 to 
4.0 where 1.0 is normal speed" },
+    "speechVoice": { "index": 44, "kind": "parameter", "displayName": "Speech 
Voice", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "alloy", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The voice to use for 
text-to-speech (e.g., alloy, echo, fable,  [...]
+    "storeFullResponse": { "index": 45, "kind": "parameter", "displayName": 
"Store Full Response", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Store the full SDK response in non-streaming 
mode: chat-comp [...]
+    "streaming": { "index": 46, "kind": "parameter", "displayName": 
"Streaming", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Enable streaming 
responses" },
+    "stripThinking": { "index": 47, "kind": "parameter", "displayName": "Strip 
Thinking", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Strip ... blocks from 
model responses (used by reasoning models like  [...]
+    "systemMessage": { "index": 48, "kind": "parameter", "displayName": 
"System Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "System message to 
prepend. When set and conversationMemory is enabled, the conversat [...]
+    "temperature": { "index": 49, "kind": "parameter", "displayName": 
"Temperature", "group": "producer", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Temperature for response 
generation (0.0 to 2.0)" },
+    "toolExecutionErrorStrategy": { "index": 50, "kind": "parameter", 
"displayName": "Tool Execution Error Strategy", "group": "producer", "label": 
"", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ 
"failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "failExchange", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...]
+    "topP": { "index": 51, "kind": "parameter", "displayName": "Top P", 
"group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Top P for response generation (0.0 to 1.0)" },
+    "userMessage": { "index": 52, "kind": "parameter", "displayName": "User 
Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Default user message 
text to use when no prompt is provided" },
+    "lazyStartProducer": { "index": 53, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
+    "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth 
Profile", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "OAuth profile name for 
obtaining an access token via the OAuth 2.0 Client Cred [...]
+    "sslContextParameters": { "index": 55, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "SSLContextParameters to 
use [...]
+    "sslEndpointAlgorithm": { "index": 56, "kind": "parameter", "displayName": 
"Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "security": 
"insecure:ssl", "insecureValue": "none", "defaultValue": "https", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", [...]
+    "sslKeymanagerAlgorithm": { "index": 57, "kind": "parameter", 
"displayName": "Ssl Keymanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "SunX509", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the key  [...]
+    "sslKeyPassword": { "index": 58, "kind": "parameter", "displayName": "Ssl 
Key Password", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password of the private key in the key 
store file" },
+    "sslKeystoreLocation": { "index": 59, "kind": "parameter", "displayName": 
"Ssl Keystore Location", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The location of the key 
store file. This is optional and can be [...]
+    "sslKeystorePassword": { "index": 60, "kind": "parameter", "displayName": 
"Ssl Keystore Password", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The store password for the key store file" },
+    "sslKeystoreType": { "index": 61, "kind": "parameter", "displayName": "Ssl 
Keystore Type", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the key store file" },
+    "sslProtocol": { "index": 62, "kind": "parameter", "displayName": "Ssl 
Protocol", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"TLSv1.3", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The SSL protocol used to generate the 
SSLContext" },
+    "sslTrustmanagerAlgorithm": { "index": 63, "kind": "parameter", 
"displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "PKIX", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the tru [...]
+    "sslTruststoreLocation": { "index": 64, "kind": "parameter", 
"displayName": "Ssl Truststore Location", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The location of the trust store file, used to 
validate the  [...]
+    "sslTruststorePassword": { "index": 65, "kind": "parameter", 
"displayName": "Ssl Truststore Password", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password for the trust store file. [...]
+    "sslTruststoreType": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Truststore Type", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the trust store file" }
   }
 }
diff --git 
a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java
 
b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java
index 825570b9a22b..9519713469f1 100644
--- 
a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java
+++ 
b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointConfigurer.java
@@ -95,6 +95,10 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "oauthProfile": 
target.getConfiguration().setOauthProfile(property(camelContext, 
java.lang.String.class, value)); return true;
         case "outputclass":
         case "outputClass": 
target.getConfiguration().setOutputClass(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "paralleltoolexecution":
+        case "parallelToolExecution": 
target.getConfiguration().setParallelToolExecution(property(camelContext, 
boolean.class, value)); return true;
+        case "paralleltooltimeout":
+        case "parallelToolTimeout": 
target.getConfiguration().setParallelToolTimeout(property(camelContext, 
long.class, value)); return true;
         case "previousresponseid":
         case "previousResponseId": 
target.getConfiguration().setPreviousResponseId(property(camelContext, 
java.lang.String.class, value)); return true;
         case "requesttimeout":
@@ -226,6 +230,10 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "oauthProfile": return java.lang.String.class;
         case "outputclass":
         case "outputClass": return java.lang.String.class;
+        case "paralleltoolexecution":
+        case "parallelToolExecution": return boolean.class;
+        case "paralleltooltimeout":
+        case "parallelToolTimeout": return long.class;
         case "previousresponseid":
         case "previousResponseId": return java.lang.String.class;
         case "requesttimeout":
@@ -358,6 +366,10 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "oauthProfile": return 
target.getConfiguration().getOauthProfile();
         case "outputclass":
         case "outputClass": return target.getConfiguration().getOutputClass();
+        case "paralleltoolexecution":
+        case "parallelToolExecution": return 
target.getConfiguration().isParallelToolExecution();
+        case "paralleltooltimeout":
+        case "parallelToolTimeout": return 
target.getConfiguration().getParallelToolTimeout();
         case "previousresponseid":
         case "previousResponseId": return 
target.getConfiguration().getPreviousResponseId();
         case "requesttimeout":
diff --git 
a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java
 
b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java
index 60bf0d2c653d..5e751f585b10 100644
--- 
a/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java
+++ 
b/components/camel-ai/camel-openai/src/generated/java/org/apache/camel/component/openai/OpenAIEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class OpenAIEndpointUriFactory extends 
org.apache.camel.support.component
     private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(65);
+        Set<String> props = new HashSet<>(67);
         props.add("additionalBodyProperty");
         props.add("additionalHeader");
         props.add("additionalResponseHeader");
@@ -63,6 +63,8 @@ public class OpenAIEndpointUriFactory extends 
org.apache.camel.support.component
         props.add("oauthProfile");
         props.add("operation");
         props.add("outputClass");
+        props.add("parallelToolExecution");
+        props.add("parallelToolTimeout");
         props.add("previousResponseId");
         props.add("requestTimeout");
         props.add("speechInstructions");
diff --git 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
index 75596ffafff6..c1c640d00adc 100644
--- 
a/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
+++ 
b/components/camel-ai/camel-openai/src/generated/resources/META-INF/org/apache/camel/component/openai/openai.json
@@ -122,34 +122,36 @@
     "mcpTimeout": { "index": 33, "kind": "parameter", "displayName": "Mcp 
Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "int", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 20, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Timeout in seconds for MCP tool call requests. 
Applies to all MCP operations inclu [...]
     "model": { "index": 34, "kind": "parameter", "displayName": "Model", 
"group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for chat completion" },
     "outputClass": { "index": 35, "kind": "parameter", "displayName": "Output 
Class", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Fully qualified class name for structured 
output using response format" },
-    "previousResponseId": { "index": 36, "kind": "parameter", "displayName": 
"Previous Response Id", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Previous response id for 
OpenAI server-side conversation state (Responses [...]
-    "requestTimeout": { "index": 37, "kind": "parameter", "displayName": 
"Request Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 0, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "HTTP request timeout in milliseconds for the 
OpenAI SDK client. When 0 or  [...]
-    "speechInstructions": { "index": 38, "kind": "parameter", "displayName": 
"Speech Instructions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Optional instructions to 
control the voice of the generated audio. Does no [...]
-    "speechModel": { "index": 39, "kind": "parameter", "displayName": "Speech 
Model", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for text-to-speech (e.g., 
gpt-4o-mini-tts, tts-1, tts-1-hd)" },
-    "speechResponseFormat": { "index": 40, "kind": "parameter", "displayName": 
"Speech Response Format", "group": "producer", "label": "", "required": false, 
"type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", 
"flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "mp3", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "descrip [...]
-    "speechSpeed": { "index": 41, "kind": "parameter", "displayName": "Speech 
Speed", "group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The speed of the generated audio, from 0.25 to 
4.0 where 1.0 is normal speed" },
-    "speechVoice": { "index": 42, "kind": "parameter", "displayName": "Speech 
Voice", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "alloy", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The voice to use for 
text-to-speech (e.g., alloy, echo, fable,  [...]
-    "storeFullResponse": { "index": 43, "kind": "parameter", "displayName": 
"Store Full Response", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Store the full SDK response in non-streaming 
mode: chat-comp [...]
-    "streaming": { "index": 44, "kind": "parameter", "displayName": 
"Streaming", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Enable streaming 
responses" },
-    "stripThinking": { "index": 45, "kind": "parameter", "displayName": "Strip 
Thinking", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Strip ... blocks from 
model responses (used by reasoning models like  [...]
-    "systemMessage": { "index": 46, "kind": "parameter", "displayName": 
"System Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "System message to 
prepend. When set and conversationMemory is enabled, the conversat [...]
-    "temperature": { "index": 47, "kind": "parameter", "displayName": 
"Temperature", "group": "producer", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Temperature for response 
generation (0.0 to 2.0)" },
-    "toolExecutionErrorStrategy": { "index": 48, "kind": "parameter", 
"displayName": "Tool Execution Error Strategy", "group": "producer", "label": 
"", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ 
"failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "failExchange", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...]
-    "topP": { "index": 49, "kind": "parameter", "displayName": "Top P", 
"group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Top P for response generation (0.0 to 1.0)" },
-    "userMessage": { "index": 50, "kind": "parameter", "displayName": "User 
Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Default user message 
text to use when no prompt is provided" },
-    "lazyStartProducer": { "index": 51, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
-    "oauthProfile": { "index": 52, "kind": "parameter", "displayName": "Oauth 
Profile", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "OAuth profile name for 
obtaining an access token via the OAuth 2.0 Client Cred [...]
-    "sslContextParameters": { "index": 53, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "SSLContextParameters to 
use [...]
-    "sslEndpointAlgorithm": { "index": 54, "kind": "parameter", "displayName": 
"Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "security": 
"insecure:ssl", "insecureValue": "none", "defaultValue": "https", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", [...]
-    "sslKeymanagerAlgorithm": { "index": 55, "kind": "parameter", 
"displayName": "Ssl Keymanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "SunX509", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the key  [...]
-    "sslKeyPassword": { "index": 56, "kind": "parameter", "displayName": "Ssl 
Key Password", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password of the private key in the key 
store file" },
-    "sslKeystoreLocation": { "index": 57, "kind": "parameter", "displayName": 
"Ssl Keystore Location", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The location of the key 
store file. This is optional and can be [...]
-    "sslKeystorePassword": { "index": 58, "kind": "parameter", "displayName": 
"Ssl Keystore Password", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The store password for the key store file" },
-    "sslKeystoreType": { "index": 59, "kind": "parameter", "displayName": "Ssl 
Keystore Type", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the key store file" },
-    "sslProtocol": { "index": 60, "kind": "parameter", "displayName": "Ssl 
Protocol", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"TLSv1.3", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The SSL protocol used to generate the 
SSLContext" },
-    "sslTrustmanagerAlgorithm": { "index": 61, "kind": "parameter", 
"displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "PKIX", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the tru [...]
-    "sslTruststoreLocation": { "index": 62, "kind": "parameter", 
"displayName": "Ssl Truststore Location", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The location of the trust store file, used to 
validate the  [...]
-    "sslTruststorePassword": { "index": 63, "kind": "parameter", 
"displayName": "Ssl Truststore Password", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password for the trust store file. [...]
-    "sslTruststoreType": { "index": 64, "kind": "parameter", "displayName": 
"Ssl Truststore Type", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the trust store file" }
+    "parallelToolExecution": { "index": 36, "kind": "parameter", 
"displayName": "Parallel Tool Execution", "group": "producer", "label": "", 
"required": false, "type": "boolean", "javaType": "boolean", "deprecated": 
false, "deprecationNote": "", "autowired": false, "secret": false, 
"defaultValue": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Execute the tool calls returned by the model 
in a si [...]
+    "parallelToolTimeout": { "index": 37, "kind": "parameter", "displayName": 
"Parallel Tool Timeout", "group": "producer", "label": "", "required": false, 
"type": "integer", "javaType": "long", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "defaultValue": 0, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Timeout in milliseconds 
for a batch of parallel tool calls, so  [...]
+    "previousResponseId": { "index": 38, "kind": "parameter", "displayName": 
"Previous Response Id", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Previous response id for 
OpenAI server-side conversation state (Responses [...]
+    "requestTimeout": { "index": 39, "kind": "parameter", "displayName": 
"Request Timeout", "group": "producer", "label": "", "required": false, "type": 
"integer", "javaType": "long", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": 0, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "HTTP request timeout in milliseconds for the 
OpenAI SDK client. When 0 or  [...]
+    "speechInstructions": { "index": 40, "kind": "parameter", "displayName": 
"Speech Instructions", "group": "producer", "label": "", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Optional instructions to 
control the voice of the generated audio. Does no [...]
+    "speechModel": { "index": 41, "kind": "parameter", "displayName": "Speech 
Model", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The model to use for text-to-speech (e.g., 
gpt-4o-mini-tts, tts-1, tts-1-hd)" },
+    "speechResponseFormat": { "index": 42, "kind": "parameter", "displayName": 
"Speech Response Format", "group": "producer", "label": "", "required": false, 
"type": "enum", "javaType": "java.lang.String", "enum": [ "mp3", "opus", "aac", 
"flac", "wav", "pcm" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "mp3", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "descrip [...]
+    "speechSpeed": { "index": 43, "kind": "parameter", "displayName": "Speech 
Speed", "group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The speed of the generated audio, from 0.25 to 
4.0 where 1.0 is normal speed" },
+    "speechVoice": { "index": 44, "kind": "parameter", "displayName": "Speech 
Voice", "group": "producer", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "alloy", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The voice to use for 
text-to-speech (e.g., alloy, echo, fable,  [...]
+    "storeFullResponse": { "index": 45, "kind": "parameter", "displayName": 
"Store Full Response", "group": "producer", "label": "", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Store the full SDK response in non-streaming 
mode: chat-comp [...]
+    "streaming": { "index": 46, "kind": "parameter", "displayName": 
"Streaming", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Enable streaming 
responses" },
+    "stripThinking": { "index": 47, "kind": "parameter", "displayName": "Strip 
Thinking", "group": "producer", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Strip ... blocks from 
model responses (used by reasoning models like  [...]
+    "systemMessage": { "index": 48, "kind": "parameter", "displayName": 
"System Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "System message to 
prepend. When set and conversationMemory is enabled, the conversat [...]
+    "temperature": { "index": 49, "kind": "parameter", "displayName": 
"Temperature", "group": "producer", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Temperature for response 
generation (0.0 to 2.0)" },
+    "toolExecutionErrorStrategy": { "index": 50, "kind": "parameter", 
"displayName": "Tool Execution Error Strategy", "group": "producer", "label": 
"", "required": false, "type": "enum", "javaType": 
"org.apache.camel.component.openai.ToolExecutionErrorStrategy", "enum": [ 
"failExchange", "repromptModel" ], "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "defaultValue": "failExchange", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfigur [...]
+    "topP": { "index": 51, "kind": "parameter", "displayName": "Top P", 
"group": "producer", "label": "", "required": false, "type": "number", 
"javaType": "java.lang.Double", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "Top P for response generation (0.0 to 1.0)" },
+    "userMessage": { "index": 52, "kind": "parameter", "displayName": "User 
Message", "group": "producer", "label": "", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "Default user message 
text to use when no prompt is provided" },
+    "lazyStartProducer": { "index": 53, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
+    "oauthProfile": { "index": 54, "kind": "parameter", "displayName": "Oauth 
Profile", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "OAuth profile name for 
obtaining an access token via the OAuth 2.0 Client Cred [...]
+    "sslContextParameters": { "index": 55, "kind": "parameter", "displayName": 
"Ssl Context Parameters", "group": "security", "label": "security", "required": 
false, "type": "object", "javaType": 
"org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "SSLContextParameters to 
use [...]
+    "sslEndpointAlgorithm": { "index": 56, "kind": "parameter", "displayName": 
"Ssl Endpoint Algorithm", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "security": 
"insecure:ssl", "insecureValue": "none", "defaultValue": "https", 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", [...]
+    "sslKeymanagerAlgorithm": { "index": 57, "kind": "parameter", 
"displayName": "Ssl Keymanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "SunX509", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the key  [...]
+    "sslKeyPassword": { "index": 58, "kind": "parameter", "displayName": "Ssl 
Key Password", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password of the private key in the key 
store file" },
+    "sslKeystoreLocation": { "index": 59, "kind": "parameter", "displayName": 
"Ssl Keystore Location", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.openai.OpenAIConfiguration", 
"configurationField": "configuration", "description": "The location of the key 
store file. This is optional and can be [...]
+    "sslKeystorePassword": { "index": 60, "kind": "parameter", "displayName": 
"Ssl Keystore Password", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": true, "security": 
"secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The store password for the key store file" },
+    "sslKeystoreType": { "index": 61, "kind": "parameter", "displayName": "Ssl 
Keystore Type", "group": "security", "label": "security", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the key store file" },
+    "sslProtocol": { "index": 62, "kind": "parameter", "displayName": "Ssl 
Protocol", "group": "security", "label": "security", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"TLSv1.3", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The SSL protocol used to generate the 
SSLContext" },
+    "sslTrustmanagerAlgorithm": { "index": 63, "kind": "parameter", 
"displayName": "Ssl Trustmanager Algorithm", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "defaultValue": "PKIX", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The algorithm used by the tru [...]
+    "sslTruststoreLocation": { "index": 64, "kind": "parameter", 
"displayName": "Ssl Truststore Location", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The location of the trust store file, used to 
validate the  [...]
+    "sslTruststorePassword": { "index": 65, "kind": "parameter", 
"displayName": "Ssl Truststore Password", "group": "security", "label": 
"security", "required": false, "type": "string", "javaType": 
"java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The password for the trust store file. [...]
+    "sslTruststoreType": { "index": 66, "kind": "parameter", "displayName": 
"Ssl Truststore Type", "group": "security", "label": "security", "required": 
false, "type": "string", "javaType": "java.lang.String", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
"JKS", "configurationClass": 
"org.apache.camel.component.openai.OpenAIConfiguration", "configurationField": 
"configuration", "description": "The file format of the trust store file" }
   }
 }
diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc 
b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
index 4b8160b3bf2e..8f5fb2e6accb 100644
--- a/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
+++ b/components/camel-ai/camel-openai/src/main/docs/openai-mcp.adoc
@@ -300,6 +300,75 @@ YAML::
 ----
 ====
 
+=== Parallel Tool Execution
+
+A model can request several tool calls in a single response. By default, the 
component executes them one after another,
+so the batch takes as long as the sum of its tool calls. Because tool calls 
emitted in the same assistant message are
+independent by design, `parallelToolExecution=true` dispatches them 
concurrently instead, reducing the batch to the
+duration of its slowest tool:
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+from("direct:chat")
+    .to("openai:chat-completion?model=gpt-4"
+        + "&parallelToolExecution=true"
+        + "&parallelToolTimeout=30000"
+        + "&mcpServer.api.transportType=streamableHttp"
+        + "&mcpServer.api.url=http://localhost:9090/mcp";);
+----
+
+XML::
++
+[source,xml]
+----
+<route>
+  <from uri="direct:chat"/>
+  <to 
uri="openai:chat-completion?model=gpt-4&amp;parallelToolExecution=true&amp;parallelToolTimeout=30000&amp;mcpServer.api.transportType=streamableHttp&amp;mcpServer.api.url=http://localhost:9090/mcp"/>
+</route>
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: direct:chat
+      steps:
+        - to:
+            uri: openai:chat-completion
+            parameters:
+              model: gpt-4
+              parallelToolExecution: true
+              parallelToolTimeout: 30000
+              mcpServer.api.transportType: streamableHttp
+              mcpServer.api.url: http://localhost:9090/mcp
+----
+====
+
+The option applies to both the agentic loop and the `openai:tool-execution` 
operation.
+
+Notes:
+
+* Results are always fed back to the model in the order the model requested 
the tools, as required by the OpenAI API,
+  regardless of the order in which the tool calls complete.
+* A batch containing a single tool call is executed inline, without a thread 
hand-off.
+* `parallelToolTimeout` (milliseconds, default `0` = disabled) bounds the 
batch as a whole, so that one slow tool cannot
+  block it. A tool call that exceeds it is cancelled and then handled 
according to `toolExecutionErrorStrategy`. Leaving
+  it disabled is reasonable, because `mcpTimeout` already bounds each 
individual MCP request.
+* With `toolExecutionErrorStrategy=failExchange`, the sibling tool calls that 
were already dispatched are allowed to
+  complete before the exchange fails. Sequential execution instead abandons 
the remaining calls at the first failure.
+* The thread pool comes from Camel's `ExecutorServiceManager` and follows the 
default thread pool profile, so it is
+  visible over JMX and shut down with the `CamelContext`. When virtual threads 
are enabled
+  (`camel.main.virtualThreadsEnabled=true`), the pool automatically becomes a 
thread-per-task virtual thread executor and
+  the profile's pool sizes no longer apply — concurrency is then bounded by 
the profile's `maxQueueSize` instead.
+* `stdio` MCP servers multiplex all calls over a single child process, so the 
achievable parallelism there depends on
+  the server implementation. HTTP-based transports (`streamableHttp`, `sse`) 
benefit the most.
+
 === Manual Tool Loop with `tool-execution` Operation
 
 When `autoToolExecution=false`, you can implement your own tool loop using the 
`openai:tool-execution` operation and Camel's `loopDoWhile` EIP. This gives you 
full control to add logging, filtering, retry logic, or custom routing between 
tool calls — without writing any Java code.
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java
new file mode 100644
index 000000000000..bcb5c1f8480f
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/McpToolCallExecutor.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Collectors;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.support.service.ServiceSupport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+/**
+ * Executes a batch of MCP tool calls returned by the model in a single 
response.
+ *
+ * <p>
+ * Shared by the agentic loop in {@link OpenAIProducer} and by the manual tool 
loop in
+ * {@link OpenAIToolExecutionProducer} so that hallucinated tool name 
handling, argument parsing, error strategies and
+ * {@code returnDirect} detection cannot drift between the two.
+ *
+ * <p>
+ * When {@code parallelToolExecution=true} the calls in a batch are dispatched 
concurrently, since tool calls emitted in
+ * the same assistant message are independent by design. Results are always 
returned in the original tool call order,
+ * because the OpenAI API pairs each {@code tool} message with its {@code 
tool_call_id}.
+ */
+class McpToolCallExecutor extends ServiceSupport {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(McpToolCallExecutor.class);
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private final OpenAIEndpoint endpoint;
+
+    private ExecutorService executorService;
+
+    McpToolCallExecutor(OpenAIEndpoint endpoint) {
+        this.endpoint = endpoint;
+    }
+
+    /**
+     * The outcome of a single tool call.
+     *
+     * @param toolCallId   the id the model assigned to the call, used to pair 
the result back to the request
+     * @param toolName     the name of the tool that was called
+     * @param content      the textual result to feed back to the model
+     * @param returnDirect whether the call succeeded and the tool is 
annotated with {@code returnDirect}
+     */
+    record ToolResult(String toolCallId, String toolName, String content, 
boolean returnDirect) {
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        if (endpoint.getConfiguration().isParallelToolExecution()) {
+            // Deliberately goes through Camel's ExecutorServiceManager rather 
than creating a raw
+            // ThreadPoolExecutor: the pool then honours 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.
+            executorService = 
endpoint.getCamelContext().getExecutorServiceManager()
+                    .newDefaultThreadPool(this, "OpenAIMcpToolCall");
+        }
+        super.doStart();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (executorService != null) {
+            
endpoint.getCamelContext().getExecutorServiceManager().shutdownNow(executorService);
+            executorService = null;
+        }
+        super.doStop();
+    }
+
+    /**
+     * Executes every tool call in the batch and returns the results in the 
original order.
+     *
+     * @param  toolCalls the tool calls requested by the model
+     * @return           one result per tool call, in the same order
+     * @throws Exception when a tool call fails and the configured strategy is 
to fail the exchange
+     */
+    List<ToolResult> execute(List<ChatCompletionMessageToolCall> toolCalls) 
throws Exception {
+        if (toolCalls.isEmpty()) {
+            return List.of();
+        }
+
+        // Snapshot the (immutable) tool state once so the whole batch sees a 
consistent view even if an
+        // MCP server publishes a tool list change or a sibling call triggers 
a reconnect while it runs
+        McpToolState toolState = endpoint.getMcpToolState();
+
+        // A single tool call gains nothing from a thread hand-off
+        if (executorService == null || toolCalls.size() == 1) {
+            List<ToolResult> results = new ArrayList<>(toolCalls.size());
+            for (ChatCompletionMessageToolCall toolCall : toolCalls) {
+                results.add(executeOne(toolCall, toolState));
+            }
+            return results;
+        }
+
+        return executeParallel(toolCalls, toolState);
+    }
+
+    private List<ToolResult> 
executeParallel(List<ChatCompletionMessageToolCall> toolCalls, McpToolState 
toolState)
+            throws Exception {
+        LOG.debug("Executing {} tool call(s) in parallel", toolCalls.size());
+
+        // Carry the caller's MDC onto the worker threads so tool call logs 
stay correlated with the exchange
+        Map<String, String> mdc = MDC.getCopyOfContextMap();
+
+        List<Future<ToolResult>> futures = new ArrayList<>(toolCalls.size());
+        for (ChatCompletionMessageToolCall toolCall : toolCalls) {
+            futures.add(executorService.submit(withMdc(mdc, () -> 
executeOne(toolCall, toolState))));
+        }
+
+        long timeout = endpoint.getConfiguration().getParallelToolTimeout();
+        long deadline = timeout > 0 ? System.nanoTime() + 
TimeUnit.MILLISECONDS.toNanos(timeout) : 0;
+
+        ToolResult[] results = new ToolResult[toolCalls.size()];
+        Exception failure = null;
+
+        for (int i = 0; i < futures.size(); i++) {
+            ChatCompletionMessageToolCall toolCall = toolCalls.get(i);
+            try {
+                results[i] = deadline > 0
+                        ? futures.get(i).get(Math.max(0, deadline - 
System.nanoTime()), TimeUnit.NANOSECONDS)
+                        : futures.get(i).get();
+            } catch (TimeoutException e) {
+                futures.get(i).cancel(true);
+                // Do not abandon the siblings: keep collecting so that every 
dispatched call is accounted for
+                Exception timedOut = timeoutFailure(toolCall, timeout);
+                if (timedOut != null) {
+                    failure = failure != null ? failure : timedOut;
+                } else {
+                    results[i] = errorResult(toolCall, "Error: tool execution 
timed out after " + timeout + " ms");
+                }
+            } catch (ExecutionException e) {
+                // executeOne only throws when the configured strategy is to 
fail the exchange
+                failure = failure != null ? failure : 
asException(e.getCause());
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                futures.forEach(f -> f.cancel(true));
+                throw e;
+            }
+        }
+
+        if (failure != null) {
+            throw failure;
+        }
+        return Arrays.asList(results);
+    }
+
+    /**
+     * Builds the exception for a timed out call, or returns {@code null} when 
the configured strategy is to report the
+     * timeout back to the model instead of failing the exchange.
+     */
+    private Exception timeoutFailure(ChatCompletionMessageToolCall toolCall, 
long timeout) {
+        String toolName = toolCall.asFunction().function().name();
+        if (endpoint.getConfiguration().getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+            return new TimeoutException(
+                    "MCP tool '" + toolName + "' did not complete within 
parallelToolTimeout of " + timeout + " ms");
+        }
+        LOG.warn("MCP tool '{}' timed out after {} ms, sending the timeout 
back to the model", toolName, timeout);
+        return null;
+    }
+
+    private static Exception asException(Throwable cause) {
+        return cause instanceof Exception e ? e : new 
IllegalStateException(cause);
+    }
+
+    private static Callable<ToolResult> withMdc(Map<String, String> mdc, 
Callable<ToolResult> task) {
+        if (mdc == null || mdc.isEmpty()) {
+            return task;
+        }
+        return () -> {
+            MDC.setContextMap(mdc);
+            try {
+                return task.call();
+            } finally {
+                MDC.clear();
+            }
+        };
+    }
+
+    private ToolResult executeOne(ChatCompletionMessageToolCall toolCall, 
McpToolState toolState) throws Exception {
+        OpenAIConfiguration config = endpoint.getConfiguration();
+        String toolName = toolCall.asFunction().function().name();
+        String argsJson = toolCall.asFunction().function().arguments();
+
+        McpSyncClient mcpClient = toolState.toolClientMap().get(toolName);
+        if (mcpClient == null) {
+            if (config.getHallucinatedToolNameStrategy() == 
HallucinatedToolNameStrategy.FAIL_EXCHANGE) {
+                throw new IllegalStateException("Tool '" + toolName + "' not 
found in any configured MCP server");
+            }
+            // repromptModel: send a corrective tool result listing available 
tools
+            String available = String.join(", ", 
toolState.toolClientMap().keySet());
+            LOG.warn("Hallucinated tool name '{}', sending corrective result 
to model", toolName);
+            return errorResult(toolCall,
+                    "Error: tool '" + toolName + "' does not exist. Available 
tools: " + available);
+        }
+
+        LOG.debug("Executing MCP tool '{}' with args: {}", toolName, argsJson);
+
+        try {
+            Map<String, Object> argsMap = OBJECT_MAPPER.readValue(argsJson, 
Map.class);
+            McpSchema.CallToolResult toolResult = endpoint.callTool(mcpClient, 
toolName, argsMap);
+
+            if (Boolean.TRUE.equals(toolResult.isError())) {
+                String content = "Error: " + 
extractTextContent(toolResult.content());
+                LOG.warn("MCP tool '{}' returned error: {}", toolName, 
content);
+                return errorResult(toolCall, content);
+            }
+
+            String content = extractTextContent(toolResult.content());
+            LOG.debug("Tool '{}' result: {}", toolName, content);
+            return new ToolResult(
+                    toolCall.asFunction().id(), toolName, content, 
toolState.returnDirectTools().contains(toolName));
+        } catch (JsonProcessingException e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("Invalid tool arguments for '{}': {}", toolName, 
argsJson, e);
+            return errorResult(toolCall, "Error: invalid tool arguments: " + 
e.getMessage());
+        } catch (Exception e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("MCP tool '{}' execution failed: {}", toolName, 
e.getMessage(), e);
+            return errorResult(toolCall, "Error: Tool execution failed: " + 
e.getMessage());
+        }
+    }
+
+    private static ToolResult errorResult(ChatCompletionMessageToolCall 
toolCall, String content) {
+        return new ToolResult(
+                toolCall.asFunction().id(), 
toolCall.asFunction().function().name(), content, false);
+    }
+
+    private static String extractTextContent(List<McpSchema.Content> contents) 
{
+        if (contents == null || contents.isEmpty()) {
+            return "";
+        }
+        return contents.stream()
+                .filter(McpSchema.TextContent.class::isInstance)
+                .map(McpSchema.TextContent.class::cast)
+                .map(McpSchema.TextContent::text)
+                .collect(Collectors.joining());
+    }
+}
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
index 0c866ce17662..9df4fc72c21a 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIConfiguration.java
@@ -226,6 +226,22 @@ public class OpenAIConfiguration implements Cloneable {
                             + "can self-correct and retry. The 
maxToolIterations option bounds retries.")
     private HallucinatedToolNameStrategy hallucinatedToolNameStrategy = 
HallucinatedToolNameStrategy.FAIL_EXCHANGE;
 
+    @UriParam(defaultValue = "false")
+    @Metadata(description = "Execute the tool calls returned by the model in a 
single response concurrently instead of "
+                            + "sequentially. Tool calls in the same batch are 
independent by design, so this reduces the "
+                            + "latency of a batch to that of its slowest tool. 
Results are always fed back to the model in "
+                            + "the original tool call order. Note that with 
toolExecutionErrorStrategy=failExchange the "
+                            + "sibling tool calls already dispatched complete 
before the exchange fails.")
+    private boolean parallelToolExecution;
+
+    @UriParam(defaultValue = "0")
+    @Metadata(description = "Timeout in milliseconds for a batch of parallel 
tool calls, so that one slow tool cannot "
+                            + "block the whole batch. The timeout applies to 
the batch as a whole, not per tool call. "
+                            + "A tool call that exceeds it is cancelled and 
handled according to toolExecutionErrorStrategy. "
+                            + "The default of 0 disables the batch timeout and 
relies on mcpTimeout, which already bounds "
+                            + "each individual MCP request. Only used when 
parallelToolExecution=true.")
+    private long parallelToolTimeout;
+
     // ========== EMBEDDINGS CONFIGURATION ==========
 
     @UriParam
@@ -754,6 +770,22 @@ public class OpenAIConfiguration implements Cloneable {
         this.hallucinatedToolNameStrategy = hallucinatedToolNameStrategy;
     }
 
+    public boolean isParallelToolExecution() {
+        return parallelToolExecution;
+    }
+
+    public void setParallelToolExecution(boolean parallelToolExecution) {
+        this.parallelToolExecution = parallelToolExecution;
+    }
+
+    public long getParallelToolTimeout() {
+        return parallelToolTimeout;
+    }
+
+    public void setParallelToolTimeout(long parallelToolTimeout) {
+        this.parallelToolTimeout = parallelToolTimeout;
+    }
+
     public SSLContextParameters getSslContextParameters() {
         return sslContextParameters;
     }
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
index d36c7d6e48e2..5820cbfbee7c 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIProducer.java
@@ -30,7 +30,6 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.openai.core.JsonField;
 import com.openai.core.JsonValue;
@@ -52,8 +51,6 @@ import 
com.openai.models.chat.completions.ChatCompletionSystemMessageParam;
 import com.openai.models.chat.completions.ChatCompletionToolMessageParam;
 import com.openai.models.chat.completions.ChatCompletionUserMessageParam;
 import com.openai.models.completions.CompletionUsage;
-import io.modelcontextprotocol.client.McpSyncClient;
-import io.modelcontextprotocol.spec.McpSchema;
 import org.apache.camel.AsyncCallback;
 import org.apache.camel.CamelExchangeException;
 import org.apache.camel.Exchange;
@@ -62,6 +59,7 @@ import org.apache.camel.WrappedFile;
 import org.apache.camel.spi.Synchronization;
 import org.apache.camel.support.DefaultAsyncProducer;
 import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -77,6 +75,7 @@ public class OpenAIProducer extends DefaultAsyncProducer {
     private static final String PENDING_USER_MESSAGE = 
"CamelOpenAIPendingUserMessage";
 
     private Class<?> outputClassResolved;
+    private McpToolCallExecutor toolCallExecutor;
 
     public OpenAIProducer(OpenAIEndpoint endpoint) {
         super(endpoint);
@@ -86,6 +85,9 @@ public class OpenAIProducer extends DefaultAsyncProducer {
     protected void doStart() throws Exception {
         OpenAIConfiguration config = getEndpoint().getConfiguration();
 
+        toolCallExecutor = new McpToolCallExecutor(getEndpoint());
+        ServiceHelper.startService(toolCallExecutor);
+
         if (ObjectHelper.isNotEmpty(config.getOutputClass())) {
             outputClassResolved = 
getEndpoint().getCamelContext().getClassResolver()
                     .resolveMandatoryClass(config.getOutputClass());
@@ -104,6 +106,12 @@ public class OpenAIProducer extends DefaultAsyncProducer {
         super.doStart();
     }
 
+    @Override
+    protected void doStop() throws Exception {
+        ServiceHelper.stopService(toolCallExecutor);
+        super.doStop();
+    }
+
     private String resolveResourceContent(String property) {
         try (InputStream is = 
ResourceHelper.resolveResourceAsInputStream(getEndpoint().getCamelContext(), 
property)) {
             if (is != null) {
@@ -532,93 +540,35 @@ public class OpenAIProducer extends DefaultAsyncProducer {
             paramsBuilder.addMessage(assistantParam);
             agenticMessages.add(assistantParam);
 
-            // Execute all tool calls in this batch
-            boolean allReturnDirect = true;
-            List<ToolResultEntry> batchResults = new ArrayList<>();
-
+            // Record the requested tools up front so the log keeps the 
model's ordering regardless of
+            // whether the batch is executed sequentially or in parallel
             for (ChatCompletionMessageToolCall toolCall : toolCalls) {
-                String toolName = toolCall.asFunction().function().name();
-                String argsJson = toolCall.asFunction().function().arguments();
-                String toolCallId = toolCall.asFunction().id();
-                toolCallsLog.add(toolName);
-
-                McpToolState mcpToolState = getEndpoint().getMcpToolState();
-                McpSyncClient mcpClient = 
mcpToolState.toolClientMap().get(toolName);
-                if (mcpClient == null) {
-                    if (config.getHallucinatedToolNameStrategy() == 
HallucinatedToolNameStrategy.FAIL_EXCHANGE) {
-                        throw new IllegalStateException(
-                                "Tool '" + toolName + "' not found in any 
configured MCP server");
-                    }
-                    // repromptModel: send a corrective tool result listing 
available tools
-                    String available = String.join(", ", 
mcpToolState.toolClientMap().keySet());
-                    String errorMsg = "Error: tool '" + toolName
-                                      + "' does not exist. Available tools: " 
+ available;
-                    LOG.warn("Hallucinated tool name '{}', sending corrective 
result to model", toolName);
-                    batchResults.add(new ToolResultEntry(toolCallId, 
errorMsg));
-                    allReturnDirect = false;
-                    continue;
-                }
-
-                LOG.debug("Executing MCP tool '{}' with args: {}", toolName, 
argsJson);
-                String resultContent;
-
-                try {
-                    Map<String, Object> argsMap = 
OBJECT_MAPPER.readValue(argsJson, Map.class);
-                    McpSchema.CallToolResult toolResult
-                            = getEndpoint().callTool(mcpClient, toolName, 
argsMap);
-
-                    if (Boolean.TRUE.equals(toolResult.isError())) {
-                        resultContent = "Error: " + 
extractTextContent(toolResult.content());
-                        allReturnDirect = false;
-                    } else {
-                        resultContent = 
extractTextContent(toolResult.content());
-                        if 
(!mcpToolState.returnDirectTools().contains(toolName)) {
-                            allReturnDirect = false;
-                        }
-                    }
-                } catch (JsonProcessingException e) {
-                    if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
-                        throw e;
-                    }
-                    LOG.warn("Invalid tool arguments for '{}': {}", toolName, 
argsJson, e);
-                    resultContent = "Error: invalid tool arguments: " + 
e.getMessage();
-                    allReturnDirect = false;
-                } catch (Exception e) {
-                    if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
-                        throw e;
-                    }
-                    LOG.warn("MCP tool '{}' execution failed: {}", toolName, 
e.getMessage(), e);
-                    resultContent = "Error: Tool execution failed: " + 
e.getMessage();
-                    allReturnDirect = false;
-                }
-
-                LOG.debug("Tool '{}' result: {}", toolName, resultContent);
-                batchResults.add(new ToolResultEntry(toolCallId, 
resultContent));
+                toolCallsLog.add(toolCall.asFunction().function().name());
             }
 
+            // Execute all tool calls in this batch
+            List<McpToolCallExecutor.ToolResult> batchResults = 
toolCallExecutor.execute(toolCalls);
+            boolean allReturnDirect = 
batchResults.stream().allMatch(McpToolCallExecutor.ToolResult::returnDirect);
+
             // returnDirect check: if ALL tools in this batch are 
returnDirect, short-circuit
             if (allReturnDirect && !batchResults.isEmpty()) {
                 LOG.debug("All tools in batch have returnDirect=true, 
short-circuiting agentic loop");
-                StringBuilder directResult = new StringBuilder();
-                for (ToolResultEntry entry : batchResults) {
-                    if (!directResult.isEmpty()) {
-                        directResult.append("\n");
-                    }
-                    directResult.append(entry.content());
-                }
+                String directResult = batchResults.stream()
+                        .map(McpToolCallExecutor.ToolResult::content)
+                        .collect(Collectors.joining("\n"));
 
-                exchange.getMessage().setBody(directResult.toString());
+                exchange.getMessage().setBody(directResult);
                 setResponseHeaders(exchange.getMessage(), response);
                 
exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, iteration);
                 
exchange.getMessage().setHeader(OpenAIConstants.MCP_TOOL_CALLS, toolCallsLog);
                 
exchange.getMessage().setHeader(OpenAIConstants.MCP_RETURN_DIRECT, true);
-                updateConversationHistory(exchange, agenticMessages, 
directResult.toString());
+                updateConversationHistory(exchange, agenticMessages, 
directResult);
                 return;
             }
 
             // Normal path: feed tool results back to LLM
             LOG.debug("Feeding {} tool result(s) back to the model", 
batchResults.size());
-            for (ToolResultEntry entry : batchResults) {
+            for (McpToolCallExecutor.ToolResult entry : batchResults) {
                 ChatCompletionMessageParam toolMsg = 
ChatCompletionMessageParam.ofTool(
                         ChatCompletionToolMessageParam.builder()
                                 .toolCallId(entry.toolCallId())
@@ -651,20 +601,6 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                                 tokenTracker.getCompletionTokens(), 
tokenTracker.getTotalTokens()));
     }
 
-    private String extractTextContent(List<McpSchema.Content> contents) {
-        if (contents == null || contents.isEmpty()) {
-            return "";
-        }
-        return contents.stream()
-                .filter(McpSchema.TextContent.class::isInstance)
-                .map(McpSchema.TextContent.class::cast)
-                .map(McpSchema.TextContent::text)
-                .collect(Collectors.joining());
-    }
-
-    private record ToolResultEntry(String toolCallId, String content) {
-    }
-
     private void processStreaming(Exchange exchange, 
ChatCompletionCreateParams params) {
         // NOTE: the stream is going to be closed after the exchange completes.
         StreamResponse<ChatCompletionChunk> streamResponse = 
getEndpoint().getClient().chat().completions() // NOSONAR
diff --git 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java
index a5313f0d7089..b6021011deaa 100644
--- 
a/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/OpenAIToolExecutionProducer.java
@@ -18,21 +18,16 @@ package org.apache.camel.component.openai;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.openai.models.chat.completions.ChatCompletion;
 import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam;
 import com.openai.models.chat.completions.ChatCompletionMessageParam;
 import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
 import com.openai.models.chat.completions.ChatCompletionToolMessageParam;
 import com.openai.models.chat.completions.ChatCompletionUserMessageParam;
-import io.modelcontextprotocol.client.McpSyncClient;
-import io.modelcontextprotocol.spec.McpSchema;
 import org.apache.camel.Exchange;
 import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.service.ServiceHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -60,7 +55,8 @@ import org.slf4j.LoggerFactory;
 public class OpenAIToolExecutionProducer extends DefaultProducer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(OpenAIToolExecutionProducer.class);
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+    private McpToolCallExecutor toolCallExecutor;
 
     public OpenAIToolExecutionProducer(OpenAIEndpoint endpoint) {
         super(endpoint);
@@ -71,6 +67,19 @@ public class OpenAIToolExecutionProducer extends 
DefaultProducer {
         return (OpenAIEndpoint) super.getEndpoint();
     }
 
+    @Override
+    protected void doStart() throws Exception {
+        toolCallExecutor = new McpToolCallExecutor(getEndpoint());
+        ServiceHelper.startService(toolCallExecutor);
+        super.doStart();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        ServiceHelper.stopService(toolCallExecutor);
+        super.doStop();
+    }
+
     @Override
     public void process(Exchange exchange) throws Exception {
         // Get the full ChatCompletion response (stored by 
storeFullResponse=true on chat-completion)
@@ -130,82 +139,18 @@ public class OpenAIToolExecutionProducer extends 
DefaultProducer {
                     "No MCP tool clients configured on the endpoint. Configure 
mcpServer.* parameters.");
         }
 
-        int executedCount = 0;
-        for (ChatCompletionMessageToolCall toolCall : toolCalls) {
-            String toolName = toolCall.asFunction().function().name();
-            String argsJson = toolCall.asFunction().function().arguments();
-            String toolCallId = toolCall.asFunction().id();
-
-            McpToolState mcpToolState = getEndpoint().getMcpToolState();
-            McpSyncClient mcpClient = 
mcpToolState.toolClientMap().get(toolName);
-            if (mcpClient == null) {
-                if (config.getHallucinatedToolNameStrategy() == 
HallucinatedToolNameStrategy.FAIL_EXCHANGE) {
-                    throw new IllegalStateException(
-                            "Tool '" + toolName + "' not found in any 
configured MCP server");
-                }
-                // repromptModel: send a corrective tool result listing 
available tools
-                String available = String.join(", ", 
mcpToolState.toolClientMap().keySet());
-                String errorMsg = "Error: tool '" + toolName
-                                  + "' does not exist. Available tools: " + 
available;
-                LOG.warn("Hallucinated tool name '{}', sending corrective 
result to model", toolName);
-                history.add(ChatCompletionMessageParam.ofTool(
-                        ChatCompletionToolMessageParam.builder()
-                                .toolCallId(toolCallId)
-                                .content(errorMsg)
-                                .build()));
-                executedCount++;
-                continue;
-            }
-
-            String resultContent;
-
-            try {
-                Map<String, Object> argsMap = 
OBJECT_MAPPER.readValue(argsJson, Map.class);
-                McpSchema.CallToolResult toolResult
-                        = getEndpoint().callTool(mcpClient, toolName, argsMap);
-
-                if (Boolean.TRUE.equals(toolResult.isError())) {
-                    resultContent = "Error: " + 
extractTextContent(toolResult.content());
-                    LOG.warn("MCP tool '{}' returned error: {}", toolName, 
resultContent);
-                } else {
-                    resultContent = extractTextContent(toolResult.content());
-                }
-            } catch (JsonProcessingException e) {
-                if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
-                    throw e;
-                }
-                LOG.warn("Invalid tool arguments for '{}': {}", toolName, 
argsJson, e);
-                resultContent = "Error: invalid tool arguments: " + 
e.getMessage();
-            } catch (Exception e) {
-                if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
-                    throw e;
-                }
-                LOG.warn("MCP tool '{}' execution failed: {}", toolName, 
e.getMessage(), e);
-                resultContent = "Error: Tool execution failed: " + 
e.getMessage();
-            }
-
+        List<McpToolCallExecutor.ToolResult> results = 
toolCallExecutor.execute(toolCalls);
+        for (McpToolCallExecutor.ToolResult result : results) {
             history.add(ChatCompletionMessageParam.ofTool(
                     ChatCompletionToolMessageParam.builder()
-                            .toolCallId(toolCallId)
-                            .content(resultContent)
+                            .toolCallId(result.toolCallId())
+                            .content(result.content())
                             .build()));
-            executedCount++;
         }
 
         // Update conversation history and clear body for the next 
chat-completion call
         exchange.setProperty(historyProperty, 
OpenAIConversationHistoryTrimmer.trim(history, config));
         exchange.getMessage().setBody(null);
-        exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, 
executedCount);
-    }
-
-    private String extractTextContent(List<McpSchema.Content> contents) {
-        if (contents == null || contents.isEmpty()) {
-            return "";
-        }
-        return contents.stream()
-                .filter(McpSchema.TextContent.class::isInstance)
-                .map(McpSchema.TextContent.class::cast)
-                .map(McpSchema.TextContent::text)
-                .collect(Collectors.joining());
+        exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, 
results.size());
     }
 }
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java
new file mode 100644
index 000000000000..4d9d975dbd13
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/McpToolCallExecutorTest.java
@@ -0,0 +1,364 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import 
com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
+import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for CAMEL-23078: the shared MCP tool call batch executor, covering 
both the sequential and the parallel
+ * ({@code parallelToolExecution=true}) execution paths.
+ */
+class McpToolCallExecutorTest {
+
+    private static final long AWAIT_SECONDS = 10;
+
+    private final DefaultCamelContext context = new DefaultCamelContext();
+
+    private McpToolCallExecutor executor;
+
+    @BeforeEach
+    void startContext() {
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        if (executor != null) {
+            executor.stop();
+        }
+        context.stop();
+    }
+
+    // ------------------------------------------------------------------
+    // Ordering
+    // ------------------------------------------------------------------
+
+    @Test
+    void parallelResultsKeepToolCallOrder() throws Exception {
+        // Every tool blocks until all of them have started, so the batch can 
only complete when it runs
+        // concurrently. The results must still come back in the order the 
model requested them.
+        CountDownLatch rendezvous = new CountDownLatch(3);
+
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of(
+                "slow_a", rendezvousClient(rendezvous, "A"),
+                "slow_b", rendezvousClient(rendezvous, "B"),
+                "slow_c", rendezvousClient(rendezvous, "C")),
+                Set.of());
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results
+                = executor.execute(List.of(toolCall("id-a", "slow_a"), 
toolCall("id-b", "slow_b"),
+                        toolCall("id-c", "slow_c")));
+
+        
assertThat(results).extracting(McpToolCallExecutor.ToolResult::toolCallId)
+                .containsExactly("id-a", "id-b", "id-c");
+        assertThat(results).extracting(McpToolCallExecutor.ToolResult::content)
+                .containsExactly("A", "B", "C");
+    }
+
+    @Test
+    void sequentialExecutionProducesTheSameResults() throws Exception {
+        OpenAIEndpoint endpoint = newEndpoint(false, 0, Map.of(
+                "tool_a", staticClient("A"),
+                "tool_b", staticClient("B")),
+                Set.of());
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results
+                = executor.execute(List.of(toolCall("id-a", "tool_a"), 
toolCall("id-b", "tool_b")));
+
+        
assertThat(results).extracting(McpToolCallExecutor.ToolResult::toolCallId)
+                .containsExactly("id-a", "id-b");
+        
assertThat(results).extracting(McpToolCallExecutor.ToolResult::content).containsExactly("A",
 "B");
+    }
+
+    @Test
+    void emptyBatchReturnsNoResults() throws Exception {
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of("tool_a", 
staticClient("A")), Set.of());
+        executor = startExecutor(endpoint);
+
+        assertThat(executor.execute(List.of())).isEmpty();
+    }
+
+    // ------------------------------------------------------------------
+    // returnDirect
+    // ------------------------------------------------------------------
+
+    @Test
+    void returnDirectIsReportedPerToolCall() throws Exception {
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of(
+                "direct_tool", staticClient("A"),
+                "normal_tool", staticClient("B")),
+                Set.of("direct_tool"));
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results
+                = executor.execute(List.of(toolCall("id-a", "direct_tool"), 
toolCall("id-b", "normal_tool")));
+
+        
assertThat(results).extracting(McpToolCallExecutor.ToolResult::returnDirect).containsExactly(true,
 false);
+    }
+
+    @Test
+    void returnDirectIsNotReportedForAToolThatReturnedAnError() throws 
Exception {
+        McpSyncClient failing = mock(McpSyncClient.class);
+        
when(failing.callTool(any(McpSchema.CallToolRequest.class))).thenReturn(
+                McpSchema.CallToolResult.builder()
+                        .content(List.of(new McpSchema.TextContent(null, 
"boom", null)))
+                        .isError(true)
+                        .build());
+
+        OpenAIEndpoint endpoint
+                = newEndpoint(true, 0, Map.of("direct_tool", failing), 
Set.of("direct_tool"));
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results = 
executor.execute(List.of(toolCall("id-a", "direct_tool")));
+
+        assertThat(results).singleElement()
+                .satisfies(r -> {
+                    assertThat(r.returnDirect()).isFalse();
+                    assertThat(r.content()).isEqualTo("Error: boom");
+                });
+    }
+
+    // ------------------------------------------------------------------
+    // Error handling
+    // ------------------------------------------------------------------
+
+    @Test
+    void parallelFailureLetsSiblingToolsCompleteBeforeFailingTheExchange() 
throws Exception {
+        AtomicInteger siblingCalls = new AtomicInteger();
+        McpSyncClient sibling = mock(McpSyncClient.class);
+        
when(sibling.callTool(any(McpSchema.CallToolRequest.class))).thenAnswer(invocation
 -> {
+            siblingCalls.incrementAndGet();
+            return textResult("ok");
+        });
+
+        McpSyncClient failing = mock(McpSyncClient.class);
+        when(failing.callTool(any(McpSchema.CallToolRequest.class)))
+                .thenThrow(new IllegalStateException("tool blew up"));
+
+        // failing tool first, so a mid-loop abort would skip the sibling 
entirely
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of(
+                "failing_tool", failing,
+                "sibling_tool", sibling),
+                Set.of());
+        executor = startExecutor(endpoint);
+
+        assertThatThrownBy(
+                () -> executor.execute(List.of(toolCall("id-a", 
"failing_tool"), toolCall("id-b", "sibling_tool"))))
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessage("tool blew up");
+
+        assertThat(siblingCalls).hasValue(1);
+    }
+
+    @Test
+    void toolFailureIsSentBackToTheModelWhenRepromptingIsConfigured() throws 
Exception {
+        McpSyncClient failing = mock(McpSyncClient.class);
+        when(failing.callTool(any(McpSchema.CallToolRequest.class)))
+                .thenThrow(new IllegalStateException("tool blew up"));
+
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of(
+                "failing_tool", failing,
+                "ok_tool", staticClient("fine")),
+                Set.of());
+        
endpoint.getConfiguration().setToolExecutionErrorStrategy(ToolExecutionErrorStrategy.REPROMPT_MODEL);
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results
+                = executor.execute(List.of(toolCall("id-a", "failing_tool"), 
toolCall("id-b", "ok_tool")));
+
+        assertThat(results).extracting(McpToolCallExecutor.ToolResult::content)
+                .containsExactly("Error: Tool execution failed: tool blew up", 
"fine");
+    }
+
+    @Test
+    void hallucinatedToolNameFailsTheExchangeByDefault() throws Exception {
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of("known_tool", 
staticClient("A")), Set.of());
+        executor = startExecutor(endpoint);
+
+        assertThatThrownBy(
+                () -> executor.execute(List.of(toolCall("id-a", "known_tool"), 
toolCall("id-b", "made_up_tool"))))
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining("made_up_tool");
+    }
+
+    @Test
+    void hallucinatedToolNameIsCorrectedWhenRepromptingIsConfigured() throws 
Exception {
+        OpenAIEndpoint endpoint = newEndpoint(true, 0, Map.of("known_tool", 
staticClient("A")), Set.of());
+        
endpoint.getConfiguration().setHallucinatedToolNameStrategy(HallucinatedToolNameStrategy.REPROMPT_MODEL);
+        executor = startExecutor(endpoint);
+
+        List<McpToolCallExecutor.ToolResult> results
+                = executor.execute(List.of(toolCall("id-a", "made_up_tool"), 
toolCall("id-b", "known_tool")));
+
+        assertThat(results.get(0).content()).contains("made_up_tool", 
"known_tool");
+        assertThat(results.get(0).returnDirect()).isFalse();
+        assertThat(results.get(1).content()).isEqualTo("A");
+    }
+
+    // ------------------------------------------------------------------
+    // Timeout
+    // ------------------------------------------------------------------
+
+    @Test
+    void batchTimeoutFailsTheExchangeButLetsFastToolsFinish() throws Exception 
{
+        CountDownLatch release = new CountDownLatch(1);
+        AtomicInteger fastCalls = new AtomicInteger();
+        try {
+            McpSyncClient fast = mock(McpSyncClient.class);
+            
when(fast.callTool(any(McpSchema.CallToolRequest.class))).thenAnswer(invocation 
-> {
+                fastCalls.incrementAndGet();
+                return textResult("fast");
+            });
+
+            OpenAIEndpoint endpoint = newEndpoint(true, 200, Map.of(
+                    "blocking_tool", blockingClient(release),
+                    "fast_tool", fast),
+                    Set.of());
+            executor = startExecutor(endpoint);
+
+            assertThatThrownBy(
+                    () -> executor.execute(List.of(toolCall("id-a", 
"blocking_tool"), toolCall("id-b", "fast_tool"))))
+                    .isInstanceOf(TimeoutException.class)
+                    .hasMessageContaining("blocking_tool")
+                    .hasMessageContaining("parallelToolTimeout");
+
+            // the slow tool must not have cancelled its sibling
+            assertThat(fastCalls).hasValue(1);
+        } finally {
+            release.countDown();
+        }
+    }
+
+    @Test
+    void batchTimeoutIsSentBackToTheModelWhenRepromptingIsConfigured() throws 
Exception {
+        CountDownLatch release = new CountDownLatch(1);
+        try {
+            OpenAIEndpoint endpoint = newEndpoint(true, 200, Map.of(
+                    "blocking_tool", blockingClient(release)),
+                    Set.of());
+            
endpoint.getConfiguration().setToolExecutionErrorStrategy(ToolExecutionErrorStrategy.REPROMPT_MODEL);
+            executor = startExecutor(endpoint);
+
+            // two calls, so the batch is dispatched in parallel rather than 
run inline
+            List<McpToolCallExecutor.ToolResult> results
+                    = executor.execute(List.of(toolCall("id-a", 
"blocking_tool"), toolCall("id-b", "blocking_tool")));
+
+            
assertThat(results).extracting(McpToolCallExecutor.ToolResult::content)
+                    .allSatisfy(content -> assertThat(content).contains("timed 
out after 200 ms"));
+        } finally {
+            release.countDown();
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // Helpers
+    // ------------------------------------------------------------------
+
+    private McpToolCallExecutor startExecutor(OpenAIEndpoint endpoint) throws 
Exception {
+        McpToolCallExecutor answer = new McpToolCallExecutor(endpoint);
+        answer.start();
+        return answer;
+    }
+
+    private OpenAIEndpoint newEndpoint(
+            boolean parallel, long timeout, Map<String, McpSyncClient> 
toolClients, Set<String> returnDirectTools) {
+        OpenAIConfiguration configuration = new OpenAIConfiguration();
+        configuration.setParallelToolExecution(parallel);
+        configuration.setParallelToolTimeout(timeout);
+
+        OpenAIComponent component = new OpenAIComponent();
+        component.setCamelContext(context);
+
+        OpenAIEndpoint endpoint = new OpenAIEndpoint("openai:chat-completion", 
component, configuration);
+        endpoint.setCamelContext(context);
+        endpoint.setMcpToolState(new McpToolState(
+                List.of(), toolClients, Map.of(), returnDirectTools));
+        return endpoint;
+    }
+
+    private static ChatCompletionMessageToolCall toolCall(String id, String 
toolName) {
+        return ChatCompletionMessageToolCall.ofFunction(
+                ChatCompletionMessageFunctionToolCall.builder()
+                        .id(id)
+                        
.function(ChatCompletionMessageFunctionToolCall.Function.builder()
+                                .name(toolName)
+                                .arguments("{}")
+                                .build())
+                        .build());
+    }
+
+    private static McpSyncClient staticClient(String resultText) {
+        McpSyncClient client = mock(McpSyncClient.class);
+        
when(client.callTool(any(McpSchema.CallToolRequest.class))).thenReturn(textResult(resultText));
+        return client;
+    }
+
+    /**
+     * A client that only returns once every tool in the batch has entered its 
call, so the batch can complete only when
+     * the calls are dispatched concurrently.
+     */
+    private static McpSyncClient rendezvousClient(CountDownLatch rendezvous, 
String resultText) {
+        McpSyncClient client = mock(McpSyncClient.class);
+        
when(client.callTool(any(McpSchema.CallToolRequest.class))).thenAnswer(invocation
 -> {
+            rendezvous.countDown();
+            if (!rendezvous.await(AWAIT_SECONDS, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("tool calls were not 
dispatched concurrently");
+            }
+            return textResult(resultText);
+        });
+        return client;
+    }
+
+    private static McpSyncClient blockingClient(CountDownLatch release) {
+        McpSyncClient client = mock(McpSyncClient.class);
+        
when(client.callTool(any(McpSchema.CallToolRequest.class))).thenAnswer(invocation
 -> {
+            release.await(AWAIT_SECONDS, TimeUnit.SECONDS);
+            return textResult("released");
+        });
+        return client;
+    }
+
+    private static McpSchema.CallToolResult textResult(String text) {
+        return McpSchema.CallToolResult.builder()
+                .content(List.of(new McpSchema.TextContent(null, text, null)))
+                .isError(false)
+                .build();
+    }
+}
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIParallelToolExecutionTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIParallelToolExecutionTest.java
new file mode 100644
index 000000000000..9f2bf52dd646
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIParallelToolExecutionTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.infra.openai.mock.OpenAIMock;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * End-to-end tests for CAMEL-23078: {@code parallelToolExecution=true} in the 
agentic loop.
+ */
+class OpenAIParallelToolExecutionTest extends CamelTestSupport {
+
+    private static final long AWAIT_SECONDS = 10;
+
+    /**
+     * Every tool blocks until both have started, so the agentic loop can only 
make progress when the batch is
+     * dispatched concurrently.
+     */
+    private final CountDownLatch rendezvous = new CountDownLatch(2);
+
+    @RegisterExtension
+    public OpenAIMock openAIMock = new OpenAIMock().builder()
+            .when("call both tools")
+            .invokeTool("get_weather")
+            .withParam("city", "London")
+            .andInvokeTool("get_traffic")
+            .withParam("city", "London")
+            .replyWith("London is sunny with light traffic.")
+            .end()
+            .build();
+
+    private String endpointUri(boolean parallel) {
+        return 
"openai:chat-completion?model=gpt-5&apiKey=dummy&autoToolExecution=true"
+               + "&parallelToolExecution=" + parallel
+               + "&baseUrl=" + openAIMock.getBaseUrl() + "/v1";
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:parallel").to(endpointUri(true));
+                from("direct:sequential").to(endpointUri(false));
+            }
+        };
+    }
+
+    @Test
+    void parallelBatchIsDispatchedConcurrently() {
+        injectMcpTools(endpointUri(true), Set.of());
+
+        Exchange result = template.request("direct:parallel", e -> 
e.getIn().setBody("call both tools"));
+
+        assertThat(result.getException()).isNull();
+        
assertThat(result.getMessage().getBody(String.class)).isEqualTo("London is 
sunny with light traffic.");
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.TOOL_ITERATIONS, 
Integer.class)).isEqualTo(1);
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MCP_TOOL_CALLS, 
List.class))
+                .containsExactly("get_weather", "get_traffic");
+        assertThat(rendezvous.getCount()).isZero();
+    }
+
+    @Test
+    void parallelBatchOfReturnDirectToolsShortCircuitsInOrder() {
+        injectMcpTools(endpointUri(true), Set.of("get_weather", 
"get_traffic"));
+
+        Exchange result = template.request("direct:parallel", e -> 
e.getIn().setBody("call both tools"));
+
+        assertThat(result.getException()).isNull();
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MCP_RETURN_DIRECT, 
Boolean.class)).isTrue();
+        // results are joined in the order the model requested the tools, not 
in completion order
+        
assertThat(result.getMessage().getBody(String.class)).isEqualTo("Sunny, 
22C\nLight traffic");
+    }
+
+    @Test
+    void sequentialExecutionRemainsTheDefaultBehavior() {
+        // no rendezvous: the sequential path would deadlock on it, so use 
independent clients
+        Map<String, McpSyncClient> toolClients = new LinkedHashMap<>();
+        toolClients.put("get_weather", staticClient("Sunny, 22C"));
+        toolClients.put("get_traffic", staticClient("Light traffic"));
+        injectMcpTools(endpointUri(false), toolClients, Set.of());
+
+        Exchange result = template.request("direct:sequential", e -> 
e.getIn().setBody("call both tools"));
+
+        assertThat(result.getException()).isNull();
+        
assertThat(result.getMessage().getBody(String.class)).isEqualTo("London is 
sunny with light traffic.");
+        
assertThat(result.getMessage().getHeader(OpenAIConstants.MCP_TOOL_CALLS, 
List.class))
+                .containsExactly("get_weather", "get_traffic");
+    }
+
+    private void injectMcpTools(String endpointKey, Set<String> 
returnDirectToolNames) {
+        Map<String, McpSyncClient> toolClients = new LinkedHashMap<>();
+        toolClients.put("get_weather", rendezvousClient("Sunny, 22C"));
+        toolClients.put("get_traffic", rendezvousClient("Light traffic"));
+        injectMcpTools(endpointKey, toolClients, returnDirectToolNames);
+    }
+
+    private void injectMcpTools(
+            String endpointKey, Map<String, McpSyncClient> toolClients, 
Set<String> returnDirectToolNames) {
+        OpenAIEndpoint endpoint = context.getEndpoint(endpointKey, 
OpenAIEndpoint.class);
+
+        List<McpSchema.Tool> mcpTools = toolClients.keySet().stream()
+                .map(name -> McpSchema.Tool.builder(name, Map.of("type", 
"object"))
+                        .description("Mock tool: " + name)
+                        .build())
+                .toList();
+
+        endpoint.setMcpToolState(new McpToolState(
+                McpToolConverter.convert(mcpTools), toolClients, Map.of(), 
returnDirectToolNames));
+    }
+
+    private McpSyncClient rendezvousClient(String resultText) {
+        McpSyncClient client = mock(McpSyncClient.class);
+        
when(client.callTool(any(McpSchema.CallToolRequest.class))).thenAnswer(invocation
 -> {
+            rendezvous.countDown();
+            if (!rendezvous.await(AWAIT_SECONDS, TimeUnit.SECONDS)) {
+                throw new IllegalStateException("tool calls were not 
dispatched concurrently");
+            }
+            return textResult(resultText);
+        });
+        return client;
+    }
+
+    private static McpSyncClient staticClient(String resultText) {
+        McpSyncClient client = mock(McpSyncClient.class);
+        
when(client.callTool(any(McpSchema.CallToolRequest.class))).thenReturn(textResult(resultText));
+        return client;
+    }
+
+    private static McpSchema.CallToolResult textResult(String text) {
+        return McpSchema.CallToolResult.builder()
+                .content(List.of(new McpSchema.TextContent(null, text, null)))
+                .isError(false)
+                .build();
+    }
+}
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIMcpParallelToolExecutionIT.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIMcpParallelToolExecutionIT.java
new file mode 100644
index 000000000000..df82b75800f8
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/integration/OpenAIMcpParallelToolExecutionIT.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.openai.integration;
+
+import java.util.List;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.openai.OpenAIConstants;
+import 
org.apache.camel.test.infra.mcp.everything.services.McpEverythingService;
+import 
org.apache.camel.test.infra.mcp.everything.services.McpEverythingServiceFactory;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for CAMEL-23078: the agentic loop with {@code 
parallelToolExecution=true} against a real MCP server
+ * and a real LLM backend.
+ *
+ * <p>
+ * How many tool calls the model emits per response is up to the model, and a 
batch of one is executed inline rather
+ * than dispatched to the pool, so reaching the parallel path at all depends 
on the backend in use. With
+ * {@code qwen3.5:2b-mlx} the multi-tool prompt below reliably produces 
two-call batches and does exercise it; smaller
+ * or older models often serialise the same prompt into one tool call per 
iteration instead. These tests are therefore
+ * written to assert that enabling the option yields the same correct outcome 
as the sequential default whatever the
+ * model chooses to do, rather than asserting a particular batch size, which 
would be flaky. The deterministic proof
+ * that a batch really is dispatched concurrently lives in {@code 
McpToolCallExecutorTest} and
+ * {@code OpenAIParallelToolExecutionTest}, which rendezvous the tool calls on 
a latch that can only be released if they
+ * run at the same time.
+ *
+ * <p>
+ * Uses the MCP Everything Server (Streamable HTTP) and Ollama as the LLM 
backend. See {@code test_execution.md} for how
+ * to run these against a local Ollama.
+ */
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*", 
disabledReason = "Requires too much network resources")
+public class OpenAIMcpParallelToolExecutionIT extends OpenAITestSupport {
+
+    private static final String MCP_PROTOCOL_VERSIONS = 
"2024-11-05,2025-03-26,2025-06-18";
+
+    /**
+     * Models call tools one at a time unless told otherwise. This gives a 
capable model the chance to emit a real
+     * batch, which is what reaches the parallel dispatch path.
+     */
+    private static final String PARALLEL_SYSTEM_MESSAGE
+            = "You must call all the tools you need at once, in a single 
response, in parallel.";
+
+    private static final String MULTI_TOOL_PROMPT
+            = "Add 3 and 4 using the add tool, and echo the word hello using 
the echo tool. Call both tools now, together.";
+
+    @RegisterExtension
+    static McpEverythingService MCP_EVERYTHING = 
McpEverythingServiceFactory.createService();
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        String mcpConfig = "&mcpServer.everything.transportType=streamableHttp"
+                           + "&mcpServer.everything.url=" + 
MCP_EVERYTHING.url()
+                           + "&mcpProtocolVersions=" + MCP_PROTOCOL_VERSIONS;
+
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:parallel")
+                        
.toF("openai:chat-completion?parallelToolExecution=true%s", mcpConfig);
+
+                from("direct:parallel-timeout")
+                        
.toF("openai:chat-completion?parallelToolExecution=true&parallelToolTimeout=60000%s",
 mcpConfig);
+
+                from("direct:sequential")
+                        .toF("openai:chat-completion?%s", 
mcpConfig.substring(1));
+            }
+        };
+    }
+
+    @Test
+    void parallelToolExecutionCompletesTheAgenticLoop() {
+        Exchange result = template.request("direct:parallel", 
this::multiToolRequest);
+
+        assertThat(result.getException()).isNull();
+        
assertThat(result.getMessage().getBody(String.class)).contains("7").containsIgnoringCase("hello");
+        assertThat(toolCalls(result)).contains("add", "echo");
+    }
+
+    @Test
+    void parallelToolExecutionHonorsABatchTimeout() {
+        // a generous timeout must not disturb tool calls that complete well 
within it
+        Exchange result = template.request("direct:parallel-timeout", 
this::multiToolRequest);
+
+        assertThat(result.getException()).isNull();
+        assertThat(result.getMessage().getBody(String.class)).contains("7");
+        assertThat(toolCalls(result)).contains("add", "echo");
+    }
+
+    @Test
+    void parallelAndSequentialExecutionAgreeOnTheOutcome() {
+        Exchange sequential = template.request("direct:sequential",
+                e -> e.getIn().setBody("Use the add tool to add 15 and 27. 
What is the result?"));
+        Exchange parallel = template.request("direct:parallel",
+                e -> e.getIn().setBody("Use the add tool to add 15 and 27. 
What is the result?"));
+
+        assertThat(sequential.getException()).isNull();
+        assertThat(parallel.getException()).isNull();
+        
assertThat(sequential.getMessage().getBody(String.class)).contains("42");
+        assertThat(parallel.getMessage().getBody(String.class)).contains("42");
+        assertThat(toolCalls(parallel)).isEqualTo(toolCalls(sequential));
+    }
+
+    private void multiToolRequest(Exchange exchange) {
+        exchange.getIn().setHeader(OpenAIConstants.SYSTEM_MESSAGE, 
PARALLEL_SYSTEM_MESSAGE);
+        exchange.getIn().setBody(MULTI_TOOL_PROMPT);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static List<String> toolCalls(Exchange exchange) {
+        return exchange.getMessage().getHeader(OpenAIConstants.MCP_TOOL_CALLS, 
List.class);
+    }
+}
diff --git a/components/camel-ai/camel-openai/test_execution.md 
b/components/camel-ai/camel-openai/test_execution.md
index c57033033d43..696bf1454899 100644
--- a/components/camel-ai/camel-openai/test_execution.md
+++ b/components/camel-ai/camel-openai/test_execution.md
@@ -32,6 +32,33 @@ Make sure the embedding model is pulled in Ollama before 
running the tests:
 ollama pull granite-embedding:30m
 ```
 
+### Parallel MCP tool execution tests
+
+`OpenAIMcpParallelToolExecutionIT` covers `parallelToolExecution=true` end to 
end. The parallel dispatch path is
+only reached when the model emits more than one tool call in a single 
response, since a batch of one is executed
+inline. That is the model's decision, not the component's, so the backend 
matters:
+
+* `qwen3.5:2b-mlx` reliably emits two-call batches for these prompts, and the 
MLX build is noticeably faster on
+  Apple Silicon.
+* Smaller or older models frequently serialise the same prompt into one tool 
call per iteration, in which case the
+  tests still pass but only cover the inline path.
+
+```bash
+ollama pull qwen3.5:2b-mlx
+mvn verify -Dollama.endpoint=http://localhost:11434/ 
-Dollama.model=qwen3.5:2b-mlx -Dollama.instance.type=remote \
+  -Dit.test=OpenAIMcpParallelToolExecutionIT
+```
+
+To confirm the parallel path was actually exercised, raise
+`org.apache.camel.component.openai.McpToolCallExecutor` to `DEBUG` and look for
+`Executing N tool call(s) in parallel` in `target/camel-openai-tests.log`.
+
+The deterministic proof that a batch is dispatched concurrently is in the unit 
tests
+(`McpToolCallExecutorTest`, `OpenAIParallelToolExecutionTest`), which do not 
need Ollama at all.
+
+Note that agentic ITs asserting the model called a tool at all can flake with 
small models, which sometimes answer
+directly instead of calling a tool.
+
 ### Running with OpenAI
 
 To run tests against OpenAI API instead of Ollama:
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 0b12177f387c..35b8848e616b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -429,6 +429,18 @@ Hosted tools: `builtinTools` (`web_search`, `file_search`, 
`code_interpreter`; `
 on this operation. The full SDK response is stored in exchange property 
`CamelOpenAIResponsesResponse` when
 `storeFullResponse=true`.
 
+==== Optional parallel MCP tool execution
+
+The new `parallelToolExecution` option (default `false`) executes the tool 
calls the model requests in a
+single response concurrently rather than sequentially, and 
`parallelToolTimeout` (milliseconds, default `0`
+= disabled) bounds such a batch as a whole. Both apply to the agentic loop and 
to the `openai:tool-execution`
+operation. Existing routes are unaffected unless they opt in.
+
+When `parallelToolExecution=true` is combined with 
`toolExecutionErrorStrategy=failExchange`, the sibling
+tool calls that were already dispatched complete before the exchange fails, 
whereas sequential execution
+abandons the remaining calls at the first failure. Routes whose tools have 
side effects and that rely on
+a failure preventing later tool calls in the same batch should keep the 
sequential default.
+
 === camel-management - Throughput MBean attribute uses EWMA smoothing
 
 The `Throughput` attribute on the `ManagedPerformanceCounter` JMX MBean now 
reports an EWMA

Reply via email to