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

gnodet pushed a commit to branch fix/CAMEL-24322
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 474bf98d631bf1a3dc17b4b673b2a12ba650e373
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Aug 3 08:19:36 2026 +0200

    CAMEL-24322: Add tool-calling support via AiToolRegistry
    
    Extend the camel-openai component to discover and execute Camel route
    tools registered via the shared AiToolRegistry, alongside existing MCP
    tools. This implements Step 6 of the unified AI tool abstraction design
    (CAMEL-23382).
    
    Changes:
    - Add camel-ai-tool compile dependency to camel-openai
    - Add 'tags' configuration parameter to OpenAIConfiguration for
      filtering tools by tag from the shared AiToolRegistry
    - Create AiToolSpecToOpenAI converter that transforms AiToolSpec
      into OpenAI ChatCompletionFunctionTool using parametersJsonSchema
    - Extend OpenAIProducer to discover Camel route tools and dispatch
      them via AiToolExecutor in the agentic loop, with exchange isolation
    - Extend OpenAIToolExecutionProducer similarly for manual tool loops
    - Add AiToolSpecToOpenAITest with 8 test cases covering full spec,
      no params, no description, default type, required arrays, empty
      schema, invalid schema, and additionalProperties:false
    - Update error messages to reference generic "tool source" instead
      of MCP-specific wording
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 components/camel-ai/camel-openai/pom.xml           |   5 +
 .../component/openai/OpenAIEndpointConfigurer.java |   3 +
 .../component/openai/OpenAIEndpointUriFactory.java |   3 +-
 .../org/apache/camel/component/openai/openai.json  |  37 +++--
 .../camel/component/openai/AiToolSpecToOpenAI.java |  82 +++++++++
 .../component/openai/OpenAIConfiguration.java      |  15 ++
 .../camel/component/openai/OpenAIProducer.java     | 147 ++++++++++++++--
 .../openai/OpenAIToolExecutionProducer.java        | 184 ++++++++++++++++-----
 .../component/openai/AiToolSpecToOpenAITest.java   | 137 +++++++++++++++
 .../openai/OpenAIToolErrorStrategyTest.java        |   2 +-
 10 files changed, 537 insertions(+), 78 deletions(-)

diff --git a/components/camel-ai/camel-openai/pom.xml 
b/components/camel-ai/camel-openai/pom.xml
index 9bafddb02bd2..251a9ed5e275 100644
--- a/components/camel-ai/camel-openai/pom.xml
+++ b/components/camel-ai/camel-openai/pom.xml
@@ -43,6 +43,11 @@
             <artifactId>camel-support</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-ai-tool</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>com.openai</groupId>
             <artifactId>openai-java</artifactId>
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..8448d69b9bcd 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
@@ -140,6 +140,7 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "stripThinking": 
target.getConfiguration().setStripThinking(property(camelContext, 
boolean.class, value)); return true;
         case "systemmessage":
         case "systemMessage": 
target.getConfiguration().setSystemMessage(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "tags": target.getConfiguration().setTags(property(camelContext, 
java.lang.String.class, value)); return true;
         case "temperature": 
target.getConfiguration().setTemperature(property(camelContext, 
java.lang.Double.class, value)); return true;
         case "toolexecutionerrorstrategy":
         case "toolExecutionErrorStrategy": 
target.getConfiguration().setToolExecutionErrorStrategy(property(camelContext, 
org.apache.camel.component.openai.ToolExecutionErrorStrategy.class, value)); 
return true;
@@ -271,6 +272,7 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "stripThinking": return boolean.class;
         case "systemmessage":
         case "systemMessage": return java.lang.String.class;
+        case "tags": return java.lang.String.class;
         case "temperature": return java.lang.Double.class;
         case "toolexecutionerrorstrategy":
         case "toolExecutionErrorStrategy": return 
org.apache.camel.component.openai.ToolExecutionErrorStrategy.class;
@@ -403,6 +405,7 @@ public class OpenAIEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         case "stripThinking": return 
target.getConfiguration().isStripThinking();
         case "systemmessage":
         case "systemMessage": return 
target.getConfiguration().getSystemMessage();
+        case "tags": return target.getConfiguration().getTags();
         case "temperature": return target.getConfiguration().getTemperature();
         case "toolexecutionerrorstrategy":
         case "toolExecutionErrorStrategy": return 
target.getConfiguration().getToolExecutionErrorStrategy();
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..b0ecf1364fa6 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<>(66);
         props.add("additionalBodyProperty");
         props.add("additionalHeader");
         props.add("additionalResponseHeader");
@@ -86,6 +86,7 @@ public class OpenAIEndpointUriFactory extends 
org.apache.camel.support.component
         props.add("streaming");
         props.add("stripThinking");
         props.add("systemMessage");
+        props.add("tags");
         props.add("temperature");
         props.add("toolExecutionErrorStrategy");
         props.add("topP");
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..7bddb08018fd 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
@@ -133,23 +133,24 @@
     "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" }
+    "tags": { "index": 47, "kind": "parameter", "displayName": "Tags", 
"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": "Comma-separated tags to filter tools from the 
shared AiToolRegistry. Tools registered via ai-tool: cons [...]
+    "temperature": { "index": 48, "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": 49, "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": 50, "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": 51, "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": 52, "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": 53, "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": 54, "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": 55, "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": 56, "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": 57, "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": 58, "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": 59, "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": 60, "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": 61, "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": 62, "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": 63, "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": 64, "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": 65, "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/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java
 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java
new file mode 100644
index 000000000000..bb29b9c2e0ed
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/main/java/org/apache/camel/component/openai/AiToolSpecToOpenAI.java
@@ -0,0 +1,82 @@
+/*
+ * 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.Map;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.openai.core.JsonValue;
+import com.openai.models.FunctionDefinition;
+import com.openai.models.FunctionParameters;
+import com.openai.models.chat.completions.ChatCompletionFunctionTool;
+import org.apache.camel.component.ai.tool.AiToolSpec;
+
+/**
+ * Converts {@link AiToolSpec} instances to OpenAI {@link 
ChatCompletionFunctionTool} objects.
+ * <p>
+ * Uses the pre-built JSON Schema string from {@link 
AiToolSpec#getParametersJsonSchema()} and parses it into the OpenAI
+ * SDK's {@link FunctionParameters} format via {@link JsonValue#from(Object)}.
+ */
+final class AiToolSpecToOpenAI {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final TypeReference<Map<String, Object>> MAP_TYPE = new 
TypeReference<>() {
+    };
+
+    private AiToolSpecToOpenAI() {
+    }
+
+    /**
+     * Converts an {@link AiToolSpec} to an OpenAI {@link 
ChatCompletionFunctionTool}.
+     *
+     * @param  spec the tool specification to convert
+     * @return      the OpenAI function tool definition
+     */
+    static ChatCompletionFunctionTool toFunctionTool(AiToolSpec spec) {
+        FunctionDefinition.Builder funcBuilder = FunctionDefinition.builder()
+                .name(spec.getName());
+
+        if (spec.getDescription() != null) {
+            funcBuilder.description(spec.getDescription());
+        }
+
+        String jsonSchema = spec.getParametersJsonSchema();
+        if (jsonSchema != null && !jsonSchema.isEmpty()) {
+            try {
+                Map<String, Object> schemaMap = 
OBJECT_MAPPER.readValue(jsonSchema, MAP_TYPE);
+                FunctionParameters.Builder paramsBuilder = 
FunctionParameters.builder();
+
+                if (!schemaMap.containsKey("type")) {
+                    paramsBuilder.putAdditionalProperty("type", 
JsonValue.from("object"));
+                }
+                for (Map.Entry<String, Object> entry : schemaMap.entrySet()) {
+                    paramsBuilder.putAdditionalProperty(entry.getKey(), 
JsonValue.from(entry.getValue()));
+                }
+
+                funcBuilder.parameters(paramsBuilder.build());
+            } catch (Exception e) {
+                throw new IllegalArgumentException(
+                        "Failed to parse JSON Schema for tool '" + 
spec.getName() + "': " + e.getMessage(), e);
+            }
+        }
+
+        return ChatCompletionFunctionTool.builder()
+                .function(funcBuilder.build())
+                .build();
+    }
+}
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..f5080e90aa90 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
@@ -167,6 +167,13 @@ public class OpenAIConfiguration implements Cloneable {
                             + "(e.g. 
additionalResponseHeader.reasoning_content=CamelMyReasoningHeader)")
     private Map<String, Object> additionalResponseHeader;
 
+    @UriParam
+    @Metadata(description = "Comma-separated tags to filter tools from the 
shared AiToolRegistry. "
+                            + "Tools registered via ai-tool: consumer 
endpoints with matching tags "
+                            + "are discovered and exposed as OpenAI 
function-calling tools alongside "
+                            + "any MCP tools. Tools with no tags (default 
pool) are always included.")
+    private String tags;
+
     @UriParam(prefix = "mcpServer.", multiValue = true)
     @Metadata(description = "MCP (Model Context Protocol) server 
configurations. "
                             + "Define servers using prefix notation: 
mcpServer.<name>.transportType=stdio|sse|streamableHttp, (Note that sse is 
deprecated) "
@@ -682,6 +689,14 @@ public class OpenAIConfiguration implements Cloneable {
         this.speechInstructions = speechInstructions;
     }
 
+    public String getTags() {
+        return tags;
+    }
+
+    public void setTags(String tags) {
+        this.tags = tags;
+    }
+
     public Map<String, Object> getMcpServer() {
         return mcpServer;
     }
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..9240a3bf9418 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
@@ -24,8 +24,10 @@ import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -59,8 +61,14 @@ import org.apache.camel.CamelExchangeException;
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.WrappedFile;
+import org.apache.camel.component.ai.tool.AiToolExecutor;
+import org.apache.camel.component.ai.tool.AiToolParameterHelper;
+import org.apache.camel.component.ai.tool.AiToolRegistry;
+import org.apache.camel.component.ai.tool.AiToolResult;
+import org.apache.camel.component.ai.tool.AiToolSpec;
 import org.apache.camel.spi.Synchronization;
 import org.apache.camel.support.DefaultAsyncProducer;
+import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.ResourceHelper;
 import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
@@ -204,15 +212,26 @@ public class OpenAIProducer extends DefaultAsyncProducer {
             }
         }
 
+        // Discover Camel route tools from AiToolRegistry by tags
+        Map<String, AiToolSpec> camelRouteTools = 
discoverCamelRouteTools(config);
+        boolean hasCamelRouteTools = !camelRouteTools.isEmpty();
+        if (hasCamelRouteTools) {
+            for (AiToolSpec spec : camelRouteTools.values()) {
+                paramsBuilder.addTool(AiToolSpecToOpenAI.toFunctionTool(spec));
+            }
+        }
+
+        boolean hasAnyTools = hasMcpTools || hasCamelRouteTools;
+
         ChatCompletionCreateParams params = paramsBuilder.build();
 
-        if (Boolean.TRUE.equals(streaming) && hasMcpTools && 
config.isAutoToolExecution()) {
-            LOG.info("Streaming with MCP tools is not supported; falling back 
to non-streaming for the agentic loop");
-            processNonStreaming(exchange, params, config);
+        if (Boolean.TRUE.equals(streaming) && hasAnyTools && 
config.isAutoToolExecution()) {
+            LOG.info("Streaming with tools is not supported; falling back to 
non-streaming for the agentic loop");
+            processNonStreaming(exchange, params, config, camelRouteTools);
         } else if (Boolean.TRUE.equals(streaming)) {
             processStreaming(exchange, params);
         } else {
-            processNonStreaming(exchange, params, config);
+            processNonStreaming(exchange, params, config, camelRouteTools);
         }
     }
 
@@ -437,17 +456,20 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                         .build());
     }
 
-    private void processNonStreaming(Exchange exchange, 
ChatCompletionCreateParams params, OpenAIConfiguration config)
+    private void processNonStreaming(
+            Exchange exchange, ChatCompletionCreateParams params, 
OpenAIConfiguration config,
+            Map<String, AiToolSpec> camelRouteTools)
             throws Exception {
         List<ChatCompletionFunctionTool> mcpTools = 
getEndpoint().getMcpToolState().tools();
         boolean hasMcpTools = mcpTools != null && !mcpTools.isEmpty();
+        boolean hasAnyTools = hasMcpTools || !camelRouteTools.isEmpty();
 
-        if (!hasMcpTools || !config.isAutoToolExecution()) {
-            // Path A: No MCP tools or auto-execution disabled -- existing 
behavior
+        if (!hasAnyTools || !config.isAutoToolExecution()) {
+            // Path A: No tools or auto-execution disabled -- existing behavior
             processNonStreamingSimple(exchange, params, config);
         } else {
-            // Path B: MCP tools with agentic loop
-            processNonStreamingAgentic(exchange, params, config);
+            // Path B: Tools with agentic loop (MCP and/or Camel route tools)
+            processNonStreamingAgentic(exchange, params, config, 
camelRouteTools);
         }
     }
 
@@ -474,12 +496,17 @@ public class OpenAIProducer extends DefaultAsyncProducer {
     }
 
     private void processNonStreamingAgentic(
-            Exchange exchange, ChatCompletionCreateParams params, 
OpenAIConfiguration config)
+            Exchange exchange, ChatCompletionCreateParams params, 
OpenAIConfiguration config,
+            Map<String, AiToolSpec> camelRouteTools)
             throws Exception {
 
         int maxIterations = config.getMaxToolIterations();
+
+        Set<String> availableToolNames = new java.util.LinkedHashSet<>();
+        
availableToolNames.addAll(getEndpoint().getMcpToolState().toolClientMap().keySet());
+        availableToolNames.addAll(camelRouteTools.keySet());
         LOG.debug("Starting agentic loop with maxToolIterations={}, available 
tools: {}", maxIterations,
-                getEndpoint().getMcpToolState().toolClientMap().keySet());
+                availableToolNames);
 
         // Rebuild the builder from the immutable params so we can accumulate 
messages
         ChatCompletionCreateParams.Builder paramsBuilder = params.toBuilder();
@@ -542,15 +569,26 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                 String toolCallId = toolCall.asFunction().id();
                 toolCallsLog.add(toolName);
 
+                // Check if the tool is a Camel route tool first
+                AiToolSpec camelSpec = camelRouteTools.get(toolName);
+                if (camelSpec != null) {
+                    String resultContent = executeCamelRouteTool(camelSpec, 
argsJson, exchange, config);
+                    LOG.debug("Camel route tool '{}' result: {}", toolName, 
resultContent);
+                    batchResults.add(new ToolResultEntry(toolCallId, 
resultContent));
+                    allReturnDirect = false; // Camel route tools do not 
support returnDirect
+                    continue;
+                }
+
+                // Fall back to MCP tool dispatch
                 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");
+                                "Tool '" + toolName + "' not found in any 
configured tool source");
                     }
                     // repromptModel: send a corrective tool result listing 
available tools
-                    String available = String.join(", ", 
mcpToolState.toolClientMap().keySet());
+                    String available = String.join(", ", availableToolNames);
                     String errorMsg = "Error: tool '" + toolName
                                       + "' does not exist. Available tools: " 
+ available;
                     LOG.warn("Hallucinated tool name '{}', sending corrective 
result to model", toolName);
@@ -633,6 +671,89 @@ public class OpenAIProducer extends DefaultAsyncProducer {
                 "Max tool iterations (%d) exceeded. Tools called: 
%s".formatted(maxIterations, toolCallsLog));
     }
 
+    /**
+     * Executes a Camel route tool via {@link AiToolExecutor}, handling the 
exchange lifecycle and error strategies.
+     */
+    private String executeCamelRouteTool(
+            AiToolSpec spec, String argsJson, Exchange exchange, 
OpenAIConfiguration config)
+            throws Exception {
+        LOG.debug("Executing Camel route tool '{}' with args: {}", 
spec.getName(), argsJson);
+
+        Map<String, Object> argsMap;
+        try {
+            if (argsJson == null || argsJson.trim().isEmpty()) {
+                argsMap = Map.of();
+            } else {
+                argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class);
+            }
+        } catch (JsonProcessingException e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("Invalid tool arguments for Camel route tool '{}': {}", 
spec.getName(), argsJson, e);
+            return "Error: invalid tool arguments: " + e.getMessage();
+        }
+
+        // Isolate tool execution in its own exchange copy
+        Exchange toolExchange = ExchangeHelper.createCopy(exchange, true);
+        try {
+            AiToolResult result = AiToolExecutor.execute(spec, argsMap, 
toolExchange);
+            return handleCamelToolResult(spec.getName(), result, config);
+        } catch (Exception e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("Camel route tool '{}' execution failed: {}", 
spec.getName(), e.getMessage(), e);
+            return "Error: Tool execution failed: " + e.getMessage();
+        }
+    }
+
+    /**
+     * Converts an {@link AiToolResult} to a string for the LLM, respecting 
the configured error strategy.
+     */
+    private String handleCamelToolResult(String toolName, AiToolResult result, 
OpenAIConfiguration config)
+            throws Exception {
+        if (result instanceof AiToolResult.Success success) {
+            return success.value();
+        } else if (result instanceof AiToolResult.ArgumentError error) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw error.cause();
+            }
+            LOG.warn("Camel route tool '{}' argument error: {}", toolName, 
error.message(), error.cause());
+            return "Error: invalid tool arguments: " + error.message();
+        } else if (result instanceof AiToolResult.ExecutionError error) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw error.cause();
+            }
+            LOG.warn("Camel route tool '{}' execution error: {}", toolName, 
error.message(), error.cause());
+            return "Error: Tool execution failed";
+        }
+        return "Tool execution failed";
+    }
+
+    /**
+     * Discovers Camel route tools from the shared {@link AiToolRegistry} 
based on the configured tags.
+     */
+    private Map<String, AiToolSpec> 
discoverCamelRouteTools(OpenAIConfiguration config) {
+        String tags = config.getTags();
+        if (ObjectHelper.isEmpty(tags)) {
+            return Map.of();
+        }
+
+        AiToolRegistry registry = 
AiToolRegistry.getOrCreate(getEndpoint().getCamelContext());
+        String[] tagArray = AiToolParameterHelper.splitTags(tags);
+
+        Map<String, AiToolSpec> toolsByName = new LinkedHashMap<>();
+        for (String tag : tagArray) {
+            for (AiToolSpec spec : registry.getToolsByTag(tag.trim())) {
+                toolsByName.putIfAbsent(spec.getName(), spec);
+            }
+        }
+
+        LOG.debug("Discovered {} Camel route tools for tags: {}", 
toolsByName.size(), tags);
+        return toolsByName;
+    }
+
     private void setAgenticTokenHeaders(Message message, 
OpenAIAgenticTokenTracker tokenTracker) {
         message.setHeader(OpenAIConstants.AGENTIC_PROMPT_TOKENS, 
tokenTracker.getPromptTokens());
         message.setHeader(OpenAIConstants.AGENTIC_COMPLETION_TOKENS, 
tokenTracker.getCompletionTokens());
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..ca305c5c66ca 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
@@ -17,8 +17,11 @@
 package org.apache.camel.component.openai;
 
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -32,7 +35,14 @@ 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.component.ai.tool.AiToolExecutor;
+import org.apache.camel.component.ai.tool.AiToolParameterHelper;
+import org.apache.camel.component.ai.tool.AiToolRegistry;
+import org.apache.camel.component.ai.tool.AiToolResult;
+import org.apache.camel.component.ai.tool.AiToolSpec;
 import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.util.ObjectHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -124,64 +134,80 @@ public class OpenAIToolExecutionProducer extends 
DefaultProducer {
                         .toolCalls(toolCalls)
                         .build()));
 
-        // Execute each tool call via MCP and add tool result messages
-        if (getEndpoint().getMcpToolState().toolClientMap().isEmpty()) {
+        // Discover Camel route tools from AiToolRegistry
+        Map<String, AiToolSpec> camelRouteTools = 
discoverCamelRouteTools(config);
+        boolean hasMcpTools = 
!getEndpoint().getMcpToolState().toolClientMap().isEmpty();
+        boolean hasCamelRouteTools = !camelRouteTools.isEmpty();
+
+        if (!hasMcpTools && !hasCamelRouteTools) {
             throw new IllegalStateException(
-                    "No MCP tool clients configured on the endpoint. Configure 
mcpServer.* parameters.");
+                    "No tool sources configured on the endpoint. Configure 
mcpServer.* parameters or tags for Camel route tools.");
         }
 
+        // Build the available tool name set for hallucinated tool handling
+        Set<String> availableToolNames = new LinkedHashSet<>();
+        
availableToolNames.addAll(getEndpoint().getMcpToolState().toolClientMap().keySet());
+        availableToolNames.addAll(camelRouteTools.keySet());
+
         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;
+            // Check if the tool is a Camel route tool first
+            AiToolSpec camelSpec = camelRouteTools.get(toolName);
+            if (camelSpec != null) {
+                resultContent = executeCamelRouteTool(camelSpec, argsJson, 
exchange, config);
+            } else {
+                // Fall back to MCP tool dispatch
+                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 tool source");
+                    }
+                    // repromptModel: send a corrective tool result listing 
available tools
+                    String available = String.join(", ", availableToolNames);
+                    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;
                 }
-                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;
+
+                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();
                 }
-                LOG.warn("MCP tool '{}' execution failed: {}", toolName, 
e.getMessage(), e);
-                resultContent = "Error: Tool execution failed: " + 
e.getMessage();
             }
 
             history.add(ChatCompletionMessageParam.ofTool(
@@ -198,6 +224,74 @@ public class OpenAIToolExecutionProducer extends 
DefaultProducer {
         exchange.getMessage().setHeader(OpenAIConstants.TOOL_ITERATIONS, 
executedCount);
     }
 
+    private Map<String, AiToolSpec> 
discoverCamelRouteTools(OpenAIConfiguration config) {
+        String tags = config.getTags();
+        if (ObjectHelper.isEmpty(tags)) {
+            return Map.of();
+        }
+
+        AiToolRegistry registry = 
AiToolRegistry.getOrCreate(getEndpoint().getCamelContext());
+        String[] tagArray = AiToolParameterHelper.splitTags(tags);
+
+        Map<String, AiToolSpec> toolsByName = new LinkedHashMap<>();
+        for (String tag : tagArray) {
+            for (AiToolSpec spec : registry.getToolsByTag(tag.trim())) {
+                toolsByName.putIfAbsent(spec.getName(), spec);
+            }
+        }
+
+        LOG.debug("Discovered {} Camel route tools for tags: {}", 
toolsByName.size(), tags);
+        return toolsByName;
+    }
+
+    private String executeCamelRouteTool(
+            AiToolSpec spec, String argsJson, Exchange exchange, 
OpenAIConfiguration config)
+            throws Exception {
+        LOG.debug("Executing Camel route tool '{}' with args: {}", 
spec.getName(), argsJson);
+
+        Map<String, Object> argsMap;
+        try {
+            if (argsJson == null || argsJson.trim().isEmpty()) {
+                argsMap = Map.of();
+            } else {
+                argsMap = OBJECT_MAPPER.readValue(argsJson, Map.class);
+            }
+        } catch (JsonProcessingException e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("Invalid tool arguments for Camel route tool '{}': {}", 
spec.getName(), argsJson, e);
+            return "Error: invalid tool arguments: " + e.getMessage();
+        }
+
+        Exchange toolExchange = ExchangeHelper.createCopy(exchange, true);
+        try {
+            AiToolResult result = AiToolExecutor.execute(spec, argsMap, 
toolExchange);
+            if (result instanceof AiToolResult.Success success) {
+                return success.value();
+            } else if (result instanceof AiToolResult.ArgumentError error) {
+                if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                    throw error.cause();
+                }
+                LOG.warn("Camel route tool '{}' argument error: {}", 
spec.getName(), error.message(), error.cause());
+                return "Error: invalid tool arguments: " + error.message();
+            } else if (result instanceof AiToolResult.ExecutionError error) {
+                if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                    throw error.cause();
+                }
+                LOG.warn("Camel route tool '{}' execution error: {}", 
spec.getName(), error.message(), error.cause());
+                return "Error: Tool execution failed";
+            }
+            return "Tool execution failed";
+        } catch (Exception e) {
+            if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
+                throw e;
+            }
+            LOG.warn("Camel route tool '{}' execution failed: {}", 
spec.getName(), e.getMessage(), e);
+            return "Error: Tool execution failed: " + e.getMessage();
+        }
+    }
+
     private String extractTextContent(List<McpSchema.Content> contents) {
         if (contents == null || contents.isEmpty()) {
             return "";
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java
new file mode 100644
index 000000000000..1afdaa1a788a
--- /dev/null
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/AiToolSpecToOpenAITest.java
@@ -0,0 +1,137 @@
+/*
+ * 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.Map;
+
+import com.openai.core.JsonValue;
+import com.openai.models.FunctionParameters;
+import com.openai.models.chat.completions.ChatCompletionFunctionTool;
+import org.apache.camel.component.ai.tool.AiToolParameterHelper;
+import org.apache.camel.component.ai.tool.AiToolSpec;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class AiToolSpecToOpenAITest {
+
+    @Test
+    void convertFullSpec() {
+        Map<String, String> params = Map.of(
+                "city", "string",
+                "city.description", "The city name",
+                "city.required", "true",
+                "unit", "string",
+                "unit.enum", "celsius,fahrenheit",
+                "unit.description", "Temperature unit");
+
+        Map<String, AiToolParameterHelper.ParameterDef> defs = 
AiToolParameterHelper.parseParameterMetadata(params);
+        String jsonSchema = AiToolParameterHelper.buildJsonSchema(params);
+
+        AiToolSpec spec = new AiToolSpec("getWeather", "Get current weather", 
defs, jsonSchema, null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        assertThat(result.function().name()).isEqualTo("getWeather");
+        assertThat(result.function().description()).hasValue("Get current 
weather");
+        assertThat(result.function().parameters()).isPresent();
+
+        FunctionParameters parameters = result.function().parameters().get();
+        Map<String, JsonValue> props = parameters._additionalProperties();
+
+        assertThat(props.get("type").asString()).contains("object");
+        assertThat(props).containsKey("properties");
+    }
+
+    @Test
+    void convertSpecWithoutParameters() {
+        AiToolSpec spec = new AiToolSpec("noParams", "A tool with no 
parameters", Map.of(), null, null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        assertThat(result.function().name()).isEqualTo("noParams");
+        assertThat(result.function().description()).hasValue("A tool with no 
parameters");
+        assertThat(result.function().parameters()).isEmpty();
+    }
+
+    @Test
+    void convertSpecWithoutDescription() {
+        AiToolSpec spec = new AiToolSpec("bareTool", null, Map.of(), null, 
null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        assertThat(result.function().name()).isEqualTo("bareTool");
+        assertThat(result.function().description()).isEmpty();
+    }
+
+    @Test
+    void convertSpecDefaultsTypeToObject() {
+        // JSON Schema without "type" key should get "object" defaulted
+        String jsonSchema = "{\"properties\":{\"x\":{\"type\":\"string\"}}}";
+        AiToolSpec spec = new AiToolSpec("testTool", "Test", Map.of(), 
jsonSchema, null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        assertThat(result.function().parameters()).isPresent();
+        FunctionParameters parameters = result.function().parameters().get();
+        
assertThat(parameters._additionalProperties().get("type").asString()).contains("object");
+    }
+
+    @Test
+    void convertSpecPreservesRequiredArray() {
+        String jsonSchema = 
"{\"type\":\"object\",\"properties\":{\"city\":{\"type\":\"string\"}},\"required\":[\"city\"]}";
+        AiToolSpec spec = new AiToolSpec("withRequired", "Has required", 
Map.of(), jsonSchema, null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        FunctionParameters parameters = result.function().parameters().get();
+        assertThat(parameters._additionalProperties()).containsKey("required");
+        
assertThat(parameters._additionalProperties().get("required").asArray()).isNotEmpty();
+    }
+
+    @Test
+    void convertSpecWithEmptyJsonSchema() {
+        AiToolSpec spec = new AiToolSpec("emptySchema", "Empty", Map.of(), "", 
null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        assertThat(result.function().name()).isEqualTo("emptySchema");
+        assertThat(result.function().parameters()).isEmpty();
+    }
+
+    @Test
+    void convertSpecWithInvalidJsonSchemaThrows() {
+        AiToolSpec spec = new AiToolSpec("badSchema", "Bad", Map.of(), "not 
valid json", null);
+
+        assertThatThrownBy(() -> AiToolSpecToOpenAI.toFunctionTool(spec))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Failed to parse JSON Schema for tool 
'badSchema'");
+    }
+
+    @Test
+    void convertSpecPreservesAdditionalPropertiesFalse() {
+        String jsonSchema = 
"{\"type\":\"object\",\"properties\":{\"q\":{\"type\":\"string\"}},\"additionalProperties\":false}";
+        AiToolSpec spec = new AiToolSpec("strict", "Strict tool", Map.of(), 
jsonSchema, null);
+
+        ChatCompletionFunctionTool result = 
AiToolSpecToOpenAI.toFunctionTool(spec);
+
+        FunctionParameters parameters = result.function().parameters().get();
+        
assertThat(parameters._additionalProperties()).containsKey("additionalProperties");
+        
assertThat(parameters._additionalProperties().get("additionalProperties").asBoolean()).contains(false);
+    }
+}
diff --git 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java
 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java
index 9a1cbcad3df1..f09d18f69109 100644
--- 
a/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java
+++ 
b/components/camel-ai/camel-openai/src/test/java/org/apache/camel/component/openai/OpenAIToolErrorStrategyTest.java
@@ -185,7 +185,7 @@ class OpenAIToolErrorStrategyTest extends CamelTestSupport {
                 .isInstanceOf(CamelExecutionException.class)
                 .hasCauseInstanceOf(IllegalStateException.class)
                 .cause()
-                .hasMessageContaining("not found in any configured MCP 
server");
+                .hasMessageContaining("not found in any configured tool 
source");
     }
 
     @Test

Reply via email to