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

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


The following commit(s) were added to refs/heads/main by this push:
     new 6bda4be83495 CAMEL-24831: camel-ai-tool - authorize tool calls with a 
component authorizationPolicy, including over MCP (#27332)
6bda4be83495 is described below

commit 6bda4be83495fe07931147615f6dcdf015352946
Author: Andrea Cosentino <[email protected]>
AuthorDate: Sun Oct 4 16:28:46 2026 +0200

    CAMEL-24831: camel-ai-tool - authorize tool calls with a component 
authorizationPolicy, including over MCP (#27332)
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../apache/camel/catalog/components/ai-tool.json   |   6 +-
 .../camel/catalog/docs/ai-tool-component.adoc      |  68 +++++++++
 .../ai/tool/AiToolComponentConfigurer.java         |   6 +
 .../ai/tool/AiToolConfigurationConfigurer.java     |   6 +
 .../ai/tool/AiToolEndpointConfigurer.java          |   6 +
 .../ai/tool/AiToolEndpointUriFactory.java          |   3 +-
 .../apache/camel/component/ai/tool/ai-tool.json    |   6 +-
 .../src/main/docs/ai-tool-component.adoc           |  68 +++++++++
 .../component/ai/tool/AiToolConfiguration.java     |  21 +++
 .../camel/component/ai/tool/AiToolConsumer.java    |  67 +++++++++
 .../camel/component/ai/tool/AiToolExecutor.java    |  38 ++++-
 .../camel/component/ai/tool/AiToolResult.java      |  13 ++
 .../tool/AiToolAuthorizationPolicyStartupTest.java |  96 ++++++++++++
 .../ai/tool/AiToolAuthorizationPolicyTest.java     | 167 +++++++++++++++++++++
 .../AiToolComponentAuthorizationPolicyTest.java    | 121 +++++++++++++++
 .../agent/LangChain4jAgentProducer.java            |   4 +
 .../component/mcp/server/McpServerBridge.java      |  23 ++-
 .../component/mcp/server/McpToolCallContext.java   |  46 ++++++
 .../component/mcp/server/McpToolCallHandler.java   |  17 +++
 .../mcp/server/vertx/VertxMcpServerEngine.java     |  13 +-
 .../VertxMcpStreamableServerTransportProvider.java |  22 ++-
 .../main/McpServerMainAuthenticationTest.java      |  85 +++++++++++
 .../component/openai/McpToolCallExecutor.java      |   4 +
 .../springai/chat/AiToolSpecToSpringAi.java        |   4 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |   9 ++
 .../dsl/AiToolComponentBuilderFactory.java         |  28 ++++
 .../endpoint/dsl/AiToolEndpointBuilderFactory.java |  50 ++++++
 27 files changed, 987 insertions(+), 10 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
index b2d28298224d..069c3ebe3bd4 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/ai-tool.json
@@ -38,7 +38,8 @@
     "returnDirect": { "index": 11, "kind": "property", "displayName": "Return 
Direct", "group": "consumer", "label": "consumer", "required": false, "type": 
"boolean", "javaType": "java.lang.Boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "When true, AI producers that support agentic 
tool loo [...]
     "tags": { "index": 12, "kind": "property", "displayName": "Tags", "group": 
"consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of tags used to group 
tools. Producers filter the registry by these tags t [...]
     "title": { "index": 13, "kind": "property", "displayName": "Title", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Optional display title for MCP tool listings. 
Advisory hint for MCP clients only." },
-    "autowiredEnabled": { "index": 14, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
+    "autowiredEnabled": { "index": 14, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
+    "authorizationPolicy": { "index": 15, "kind": "property", "displayName": 
"Authorization Policy", "group": "security", "label": "consumer,security", 
"required": false, "type": "object", "javaType": 
"org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", 
"configurationField": "configuration", "description": "Reference to an 
org.apache.came [...]
   },
   "properties": {
     "toolName": { "index": 0, "kind": "path", "displayName": "Tool Name", 
"group": "consumer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The tool name. This is the 
name the LLM sees and uses to invoke the tool." },
@@ -56,6 +57,7 @@
     "title": { "index": 12, "kind": "parameter", "displayName": "Title", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Optional display title for MCP tool listings. 
Advisory hint for MCP clients only." },
     "bridgeErrorHandler": { "index": 13, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
     "exceptionHandler": { "index": 14, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
-    "exchangePattern": { "index": 15, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." }
+    "exchangePattern": { "index": 15, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
+    "authorizationPolicy": { "index": 16, "kind": "parameter", "displayName": 
"Authorization Policy", "group": "security", "label": "consumer,security", 
"required": false, "type": "object", "javaType": 
"org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", 
"configurationField": "configuration", "description": "Reference to an 
org.apache.cam [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
index 13e9da463c4e..8d431a7d5210 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/ai-tool-component.adoc
@@ -353,6 +353,74 @@ YAML::
 ----
 ====
 
+== Authorizing tool calls
+
+A tool call is a security boundary: an AI model decides, from its own output, 
which `ai-tool` route to invoke. Set
+an `authorizationPolicy` — a reference to an 
`org.apache.camel.spi.AuthorizationPolicy` bean — to authorize every
+call before the route runs. Set it on the *component* to guard every tool 
route by construction, or on a single
+endpoint to override.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// one policy guarding every ai-tool route
+AiToolComponent ai = context.getComponent("ai-tool", AiToolComponent.class);
+ai.getConfiguration().setAuthorizationPolicy(myAuthorizationPolicy);
+
+from("ai-tool:transferFunds?tags=banking&description=Transfer funds")
+    .to("bean:ledger");
+
+// ...or override on a single endpoint
+from("ai-tool:transferFunds?tags=banking&description=Transfer 
funds&authorizationPolicy=#myAuthorizationPolicy")
+    .to("bean:ledger");
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: ai-tool:transferFunds
+      parameters:
+        tags: banking
+        description: "Transfer funds"
+        authorizationPolicy: "#myAuthorizationPolicy"
+      steps:
+        - to: bean:ledger
+----
+====
+
+The guard runs in front of the route: it wraps the route's outer processor, so 
it executes before the route's unit
+of work, tracing and error handling. A denied call 
(`CamelAuthorizationException`) is returned to the model as a
+short refusal it can relay — not as a tool result and not as a stack trace — 
regardless of the tool-execution error
+strategy; the denial is logged at `WARN`, but it does not produce a route span 
or metric.
+
+The policy authorizes on *trustworthy* input only:
+
+* the *tool name* comes from the route (the tool's id), never from model 
output;
+* the *caller identity* comes from an exchange *property* (or a validated 
token) set before the agent ran — for
+  example by `camel-spiffe` or `camel-keycloak`. Authorize on properties or 
validated tokens only, *never* on
+  message headers: on a tool route the headers carry the model-controlled tool 
arguments (and, on the
+  `langchain4j-agent` path, the caller's inbound HTTP headers), so a policy 
such as OPA with the default
+  `includeHeaders="*"` would otherwise read attacker-influenced values.
+
+Which runtimes carry the caller identity:
+
+* `camel-langchain4j-agent` copies the calling exchange, so the identity 
property reaches the tool route today.
+* `camel-openai` and `camel-spring-ai-chat` currently build a fresh exchange, 
so an identity-based policy denies
+  under them until CAMEL-24832 propagates the caller context — the guard still 
applies, it simply has no identity to
+  authorize on yet.
+* Over the xref:others:mcp-server.adoc[MCP server] the authenticated transport 
caller is carried onto the tool
+  exchange as the `CamelMcpSecurityPrincipal` property (the raw transport 
principal — for the Vert.x streamable HTTP
+  server, an `io.vertx.ext.auth.User`). A policy over MCP reads that property 
directly; Camel's shipped
+  identity/token policies (Keycloak, Spring Security, Shiro) do not read it 
yet, so MCP authorization needs a policy
+  that inspects `CamelMcpSecurityPrincipal`. Exposing a runtime-neutral 
principal name and roles for the shipped
+  policies is tracked as a follow-up.
+
 == See Also
 
 * xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
diff --git 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
index a4235b3ed125..c78b3ef676e0 100644
--- 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
+++ 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolComponentConfigurer.java
@@ -32,6 +32,8 @@ public class AiToolComponentConfigurer extends 
PropertyConfigurerSupport impleme
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": 
getOrCreateConfiguration(target).setArgSchema(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "authorizationpolicy":
+        case "authorizationPolicy": 
getOrCreateConfiguration(target).setAuthorizationPolicy(property(camelContext, 
org.apache.camel.spi.AuthorizationPolicy.class, value)); return true;
         case "autowiredenabled":
         case "autowiredEnabled": 
target.setAutowiredEnabled(property(camelContext, boolean.class, value)); 
return true;
         case "bridgeerrorhandler":
@@ -64,6 +66,8 @@ public class AiToolComponentConfigurer extends 
PropertyConfigurerSupport impleme
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return java.lang.String.class;
+        case "authorizationpolicy":
+        case "authorizationPolicy": return 
org.apache.camel.spi.AuthorizationPolicy.class;
         case "autowiredenabled":
         case "autowiredEnabled": return boolean.class;
         case "bridgeerrorhandler":
@@ -97,6 +101,8 @@ public class AiToolComponentConfigurer extends 
PropertyConfigurerSupport impleme
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return 
getOrCreateConfiguration(target).getArgSchema();
+        case "authorizationpolicy":
+        case "authorizationPolicy": return 
getOrCreateConfiguration(target).getAuthorizationPolicy();
         case "autowiredenabled":
         case "autowiredEnabled": return target.isAutowiredEnabled();
         case "bridgeerrorhandler":
diff --git 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
index 05be37996d54..0b56cc32a071 100644
--- 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
+++ 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolConfigurationConfigurer.java
@@ -25,6 +25,8 @@ public class AiToolConfigurationConfigurer extends 
org.apache.camel.support.comp
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": target.setArgSchema(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "authorizationpolicy":
+        case "authorizationPolicy": 
target.setAuthorizationPolicy(property(camelContext, 
org.apache.camel.spi.AuthorizationPolicy.class, value)); return true;
         case "description": target.setDescription(property(camelContext, 
java.lang.String.class, value)); return true;
         case "destructivehint":
         case "destructiveHint": 
target.setDestructiveHint(property(camelContext, java.lang.Boolean.class, 
value)); return true;
@@ -52,6 +54,8 @@ public class AiToolConfigurationConfigurer extends 
org.apache.camel.support.comp
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return java.lang.String.class;
+        case "authorizationpolicy":
+        case "authorizationPolicy": return 
org.apache.camel.spi.AuthorizationPolicy.class;
         case "description": return java.lang.String.class;
         case "destructivehint":
         case "destructiveHint": return java.lang.Boolean.class;
@@ -80,6 +84,8 @@ public class AiToolConfigurationConfigurer extends 
org.apache.camel.support.comp
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return target.getArgSchema();
+        case "authorizationpolicy":
+        case "authorizationPolicy": return target.getAuthorizationPolicy();
         case "description": return target.getDescription();
         case "destructivehint":
         case "destructiveHint": return target.getDestructiveHint();
diff --git 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
index a55896933241..cd65ce6ad376 100644
--- 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
+++ 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointConfigurer.java
@@ -25,6 +25,8 @@ public class AiToolEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": 
target.getConfiguration().setArgSchema(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "authorizationpolicy":
+        case "authorizationPolicy": 
target.getConfiguration().setAuthorizationPolicy(property(camelContext, 
org.apache.camel.spi.AuthorizationPolicy.class, value)); return true;
         case "bridgeerrorhandler":
         case "bridgeErrorHandler": 
target.setBridgeErrorHandler(property(camelContext, boolean.class, value)); 
return true;
         case "description": 
target.getConfiguration().setDescription(property(camelContext, 
java.lang.String.class, value)); return true;
@@ -58,6 +60,8 @@ public class AiToolEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return java.lang.String.class;
+        case "authorizationpolicy":
+        case "authorizationPolicy": return 
org.apache.camel.spi.AuthorizationPolicy.class;
         case "bridgeerrorhandler":
         case "bridgeErrorHandler": return boolean.class;
         case "description": return java.lang.String.class;
@@ -92,6 +96,8 @@ public class AiToolEndpointConfigurer extends 
PropertyConfigurerSupport implemen
         switch (ignoreCase ? name.toLowerCase() : name) {
         case "argschema":
         case "argSchema": return target.getConfiguration().getArgSchema();
+        case "authorizationpolicy":
+        case "authorizationPolicy": return 
target.getConfiguration().getAuthorizationPolicy();
         case "bridgeerrorhandler":
         case "bridgeErrorHandler": return target.isBridgeErrorHandler();
         case "description": return target.getConfiguration().getDescription();
diff --git 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
index e55825db4395..ededca6cc9a1 100644
--- 
a/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
+++ 
b/components/camel-ai/camel-ai-tool/src/generated/java/org/apache/camel/component/ai/tool/AiToolEndpointUriFactory.java
@@ -24,8 +24,9 @@ public class AiToolEndpointUriFactory 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<>(16);
+        Set<String> props = new HashSet<>(17);
         props.add("argSchema");
+        props.add("authorizationPolicy");
         props.add("bridgeErrorHandler");
         props.add("description");
         props.add("destructiveHint");
diff --git 
a/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
 
b/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
index b2d28298224d..069c3ebe3bd4 100644
--- 
a/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
+++ 
b/components/camel-ai/camel-ai-tool/src/generated/resources/META-INF/org/apache/camel/component/ai/tool/ai-tool.json
@@ -38,7 +38,8 @@
     "returnDirect": { "index": 11, "kind": "property", "displayName": "Return 
Direct", "group": "consumer", "label": "consumer", "required": false, "type": 
"boolean", "javaType": "java.lang.Boolean", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, "defaultValue": 
false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "When true, AI producers that support agentic 
tool loo [...]
     "tags": { "index": 12, "kind": "property", "displayName": "Tags", "group": 
"consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Comma-separated list of tags used to group 
tools. Producers filter the registry by these tags t [...]
     "title": { "index": 13, "kind": "property", "displayName": "Title", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Optional display title for MCP tool listings. 
Advisory hint for MCP clients only." },
-    "autowiredEnabled": { "index": 14, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
+    "autowiredEnabled": { "index": 14, "kind": "property", "displayName": 
"Autowired Enabled", "group": "advanced", "label": "advanced", "required": 
false, "type": "boolean", "javaType": "boolean", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": true, "description": 
"Whether autowiring is enabled. This is used for automatic autowiring options 
(the option must be marked as autowired) by looking up in the registry to find 
if there is a single instance of matching  [...]
+    "authorizationPolicy": { "index": 15, "kind": "property", "displayName": 
"Authorization Policy", "group": "security", "label": "consumer,security", 
"required": false, "type": "object", "javaType": 
"org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", 
"configurationField": "configuration", "description": "Reference to an 
org.apache.came [...]
   },
   "properties": {
     "toolName": { "index": 0, "kind": "path", "displayName": "Tool Name", 
"group": "consumer", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The tool name. This is the 
name the LLM sees and uses to invoke the tool." },
@@ -56,6 +57,7 @@
     "title": { "index": 12, "kind": "parameter", "displayName": "Title", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.ai.tool.AiToolConfiguration", "configurationField": 
"configuration", "description": "Optional display title for MCP tool listings. 
Advisory hint for MCP clients only." },
     "bridgeErrorHandler": { "index": 13, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
     "exceptionHandler": { "index": 14, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
-    "exchangePattern": { "index": 15, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." }
+    "exchangePattern": { "index": 15, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
+    "authorizationPolicy": { "index": 16, "kind": "parameter", "displayName": 
"Authorization Policy", "group": "security", "label": "consumer,security", 
"required": false, "type": "object", "javaType": 
"org.apache.camel.spi.AuthorizationPolicy", "deprecated": false, 
"deprecationNote": "", "autowired": false, "secret": false, 
"configurationClass": "org.apache.camel.component.ai.tool.AiToolConfiguration", 
"configurationField": "configuration", "description": "Reference to an 
org.apache.cam [...]
   }
 }
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc 
b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
index 13e9da463c4e..8d431a7d5210 100644
--- a/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
+++ b/components/camel-ai/camel-ai-tool/src/main/docs/ai-tool-component.adoc
@@ -353,6 +353,74 @@ YAML::
 ----
 ====
 
+== Authorizing tool calls
+
+A tool call is a security boundary: an AI model decides, from its own output, 
which `ai-tool` route to invoke. Set
+an `authorizationPolicy` — a reference to an 
`org.apache.camel.spi.AuthorizationPolicy` bean — to authorize every
+call before the route runs. Set it on the *component* to guard every tool 
route by construction, or on a single
+endpoint to override.
+
+[tabs]
+====
+Java::
++
+[source,java]
+----
+// one policy guarding every ai-tool route
+AiToolComponent ai = context.getComponent("ai-tool", AiToolComponent.class);
+ai.getConfiguration().setAuthorizationPolicy(myAuthorizationPolicy);
+
+from("ai-tool:transferFunds?tags=banking&description=Transfer funds")
+    .to("bean:ledger");
+
+// ...or override on a single endpoint
+from("ai-tool:transferFunds?tags=banking&description=Transfer 
funds&authorizationPolicy=#myAuthorizationPolicy")
+    .to("bean:ledger");
+----
+
+YAML::
++
+[source,yaml]
+----
+- route:
+    from:
+      uri: ai-tool:transferFunds
+      parameters:
+        tags: banking
+        description: "Transfer funds"
+        authorizationPolicy: "#myAuthorizationPolicy"
+      steps:
+        - to: bean:ledger
+----
+====
+
+The guard runs in front of the route: it wraps the route's outer processor, so 
it executes before the route's unit
+of work, tracing and error handling. A denied call 
(`CamelAuthorizationException`) is returned to the model as a
+short refusal it can relay — not as a tool result and not as a stack trace — 
regardless of the tool-execution error
+strategy; the denial is logged at `WARN`, but it does not produce a route span 
or metric.
+
+The policy authorizes on *trustworthy* input only:
+
+* the *tool name* comes from the route (the tool's id), never from model 
output;
+* the *caller identity* comes from an exchange *property* (or a validated 
token) set before the agent ran — for
+  example by `camel-spiffe` or `camel-keycloak`. Authorize on properties or 
validated tokens only, *never* on
+  message headers: on a tool route the headers carry the model-controlled tool 
arguments (and, on the
+  `langchain4j-agent` path, the caller's inbound HTTP headers), so a policy 
such as OPA with the default
+  `includeHeaders="*"` would otherwise read attacker-influenced values.
+
+Which runtimes carry the caller identity:
+
+* `camel-langchain4j-agent` copies the calling exchange, so the identity 
property reaches the tool route today.
+* `camel-openai` and `camel-spring-ai-chat` currently build a fresh exchange, 
so an identity-based policy denies
+  under them until CAMEL-24832 propagates the caller context — the guard still 
applies, it simply has no identity to
+  authorize on yet.
+* Over the xref:others:mcp-server.adoc[MCP server] the authenticated transport 
caller is carried onto the tool
+  exchange as the `CamelMcpSecurityPrincipal` property (the raw transport 
principal — for the Vert.x streamable HTTP
+  server, an `io.vertx.ext.auth.User`). A policy over MCP reads that property 
directly; Camel's shipped
+  identity/token policies (Keycloak, Spring Security, Shiro) do not read it 
yet, so MCP authorization needs a policy
+  that inspects `CamelMcpSecurityPrincipal`. Exposing a runtime-neutral 
principal name and roles for the shipped
+  policies is tracked as a follow-up.
+
 == See Also
 
 * xref:langchain4j-agent-component.adoc[LangChain4j Agent Component] — 
discovers ai-tool tools via the `tags` option
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
index d3ef196b8995..a0f8d2a443dd 100644
--- 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConfiguration.java
@@ -20,6 +20,7 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.AuthorizationPolicy;
 import org.apache.camel.spi.Configurer;
 import org.apache.camel.spi.Metadata;
 import org.apache.camel.spi.UriParam;
@@ -106,6 +107,18 @@ public class AiToolConfiguration implements Cloneable {
                             + "Also published as an MCP tool annotation when 
the tool is exposed via camel-mcp-server.")
     private Boolean returnDirect;
 
+    @Metadata(label = "consumer,security")
+    @UriParam(description = "Reference to an 
org.apache.camel.spi.AuthorizationPolicy used to authorize tool calls "
+                            + "before the route runs. Set it on the component 
to guard every tool route by "
+                            + "construction, or per endpoint to override. The 
policy authorizes on trustworthy input "
+                            + "only: the tool name comes from the route (never 
from model output), and the caller "
+                            + "identity is carried as an exchange property set 
before the agent ran (for example by "
+                            + "camel-spiffe or camel-keycloak), which the 
model cannot set. Authorize on exchange "
+                            + "properties or validated tokens only, never on 
message headers (on a tool route the "
+                            + "headers carry the model-controlled tool 
arguments). A denied call surfaces to the "
+                            + "model as a short refusal rather than a stack 
trace.")
+    private AuthorizationPolicy authorizationPolicy;
+
     public AiToolConfiguration() {
     }
 
@@ -205,6 +218,14 @@ public class AiToolConfiguration implements Cloneable {
         this.returnDirect = returnDirect;
     }
 
+    public AuthorizationPolicy getAuthorizationPolicy() {
+        return authorizationPolicy;
+    }
+
+    public void setAuthorizationPolicy(AuthorizationPolicy 
authorizationPolicy) {
+        this.authorizationPolicy = authorizationPolicy;
+    }
+
     public AiToolConfiguration copy() {
         try {
             AiToolConfiguration copy = (AiToolConfiguration) super.clone();
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
index b61762818b31..bf16639aee57 100644
--- 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolConsumer.java
@@ -18,9 +18,12 @@ package org.apache.camel.component.ai.tool;
 
 import java.util.Map;
 
+import org.apache.camel.CamelAuthorizationException;
 import org.apache.camel.Processor;
+import org.apache.camel.spi.AuthorizationPolicy;
 import org.apache.camel.support.CamelContextHelper;
 import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.service.ServiceHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -38,6 +41,7 @@ public class AiToolConsumer extends DefaultConsumer {
     private AiToolSpec registeredSpec;
     private String[] registeredTags;
     private boolean registeredInDefaultPool;
+    private volatile Processor toolProcessor;
 
     public AiToolConsumer(AiToolEndpoint endpoint, Processor processor) {
         super(endpoint, processor);
@@ -54,6 +58,55 @@ public class AiToolConsumer extends DefaultConsumer {
         register();
     }
 
+    /**
+     * Wraps the tool route's processor with the configured {@link 
AuthorizationPolicy}, if any, so the call is
+     * authorized before the route runs. The policy is applied via {@code 
beforeWrap} then {@code wrap} (as the
+     * {@code .policy()} DSL does through {@code PolicyReifier}); the wrapped 
processor is started and stopped with this
+     * consumer. Because {@code getProcessor()} is the route's <em>outer</em> 
processor, the guard runs in front of the
+     * route (before its unit of work, tracing and error handling): a denied 
call is logged and returned to the model as
+     * {@link AiToolResult.AuthorizationDenied}, but it does not produce a 
route span or metric. There is no
+     * {@code ProcessorDefinition} here (the component owns the guard, not the 
DSL), so {@code beforeWrap} gets a
+     * {@code null} definition, which Camel's {@link AuthorizationPolicy} 
implementations ignore (they use only the
+     * route). Called from {@link #prepare()} before the tool is registered, 
so the tool is never discoverable
+     * unguarded; idempotent (a policy already applied is not wrapped twice).
+     */
+    private void applyAuthorizationPolicy() throws Exception {
+        if (toolProcessor != null) {
+            return;
+        }
+        AuthorizationPolicy policy = configuration.getAuthorizationPolicy();
+        Processor target = getProcessor();
+        if (policy == null || target == null) {
+            return;
+        }
+        policy.beforeWrap(getRoute(), null);
+        Processor guarded = policy.wrap(getRoute(), target);
+        ServiceHelper.startService(guarded);
+        toolProcessor = guarded;
+        LOG.debug("Tool '{}' is guarded by authorization policy {}", toolName, 
policy.getClass().getName());
+    }
+
+    /**
+     * The processor {@link AiToolExecutor} invokes for this tool: the 
authorization-guarded processor when an
+     * {@link AuthorizationPolicy} is configured, otherwise the plain route 
processor. Fails <em>closed</em>: if a
+     * policy is configured but the guard is not yet in place, it returns a 
processor that denies the call rather than
+     * exposing the unguarded route processor.
+     */
+    Processor getToolProcessor() {
+        Processor guarded = toolProcessor;
+        if (guarded != null) {
+            return guarded;
+        }
+        if (configuration.getAuthorizationPolicy() != null) {
+            // fail closed: never fall back to the unguarded route processor 
while a policy is configured
+            return exchange -> {
+                throw new CamelAuthorizationException(
+                        "Authorization policy for tool '" + toolName + "' is 
not ready", exchange);
+            };
+        }
+        return getProcessor();
+    }
+
     /**
      * Registers during route warm-up, which Camel completes for all routes 
before it starts any route consumer, so a
      * route that sends a request as soon as its consumer starts (such as 
{@code stream:in}) sees every tool. Routes
@@ -73,6 +126,12 @@ public class AiToolConsumer extends DefaultConsumer {
         if (registeredSpec != null && !isStarted()) {
             deregister();
             registeredSpec = null;
+            // prepare() may have wrapped and started the guard during early 
registration; stop it so an undone early
+            // registration does not leave a started processor behind (matches 
doStop)
+            if (toolProcessor != null) {
+                ServiceHelper.stopService(toolProcessor);
+                toolProcessor = null;
+            }
         }
     }
 
@@ -124,6 +183,10 @@ public class AiToolConsumer extends DefaultConsumer {
             registeredTags = null;
             registeredInDefaultPool = true;
         }
+
+        // Apply the authorization guard BEFORE the tool is registered 
(register() follows in both doStart() and
+        // registerEarly()), so the tool is never discoverable in an unguarded 
state during route warm-up.
+        applyAuthorizationPolicy();
     }
 
     @Override
@@ -150,6 +213,10 @@ public class AiToolConsumer extends DefaultConsumer {
             registeredTags = null;
             registeredInDefaultPool = false;
         }
+        if (toolProcessor != null) {
+            ServiceHelper.stopService(toolProcessor);
+            toolProcessor = null;
+        }
         super.doStop();
     }
 
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
index 850af4aebc4a..85a485d22fca 100644
--- 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolExecutor.java
@@ -21,6 +21,7 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
+import org.apache.camel.CamelAuthorizationException;
 import org.apache.camel.Exchange;
 import org.apache.camel.Processor;
 import org.apache.camel.support.DefaultConsumer;
@@ -83,7 +84,9 @@ public final class AiToolExecutor {
             return new AiToolResult.ExecutionError(cause.getMessage(), cause);
         }
 
-        Processor routeProcessor = consumer.getProcessor();
+        // Use the authorization-guarded processor when the consumer applied 
an AuthorizationPolicy, so the guard runs
+        // before the route body; falls back to the plain route processor 
otherwise.
+        Processor routeProcessor = consumer instanceof AiToolConsumer atc ? 
atc.getToolProcessor() : consumer.getProcessor();
         if (routeProcessor == null) {
             IllegalStateException cause = new IllegalStateException(
                     String.format("No route processor available for tool 
'%s'", toolName));
@@ -144,6 +147,10 @@ public final class AiToolExecutor {
 
             if (exchange.getException() != null) {
                 Exception routeError = exchange.getException();
+                AiToolResult denied = authorizationDenied(toolName, 
routeError);
+                if (denied != null) {
+                    return denied;
+                }
                 LOG.error("Error executing tool '{}': {}", toolName, 
routeError.getMessage(), routeError);
                 return new AiToolResult.ExecutionError(
                         String.format("Error executing tool '%s': %s", 
toolName, routeError.getMessage()), routeError);
@@ -153,12 +160,41 @@ public final class AiToolExecutor {
             LOG.debug("Tool '{}' execution completed successfully", toolName);
             return buildSuccessResult(spec, exchange, result);
         } catch (Exception e) {
+            AiToolResult denied = authorizationDenied(toolName, e);
+            if (denied != null) {
+                return denied;
+            }
             LOG.error("Error executing tool '{}': {}", toolName, 
e.getMessage(), e);
             return new AiToolResult.ExecutionError(
                     String.format("Error executing tool '%s': %s", toolName, 
e.getMessage()), e);
         }
     }
 
+    /**
+     * Classifies an error from route execution as an authorization denial 
when a {@link CamelAuthorizationException} is
+     * present in its cause chain (the route's {@link 
org.apache.camel.spi.AuthorizationPolicy} rejected the call).
+     * Returns a caller-safe {@link AiToolResult.AuthorizationDenied} refusal 
that does not leak the policy's internal
+     * message, or {@code null} when the error is not an authorization denial.
+     */
+    private static AiToolResult authorizationDenied(String toolName, Throwable 
error) {
+        CamelAuthorizationException denial = findAuthorizationException(error);
+        if (denial == null) {
+            return null;
+        }
+        LOG.warn("Tool '{}' call denied by authorization policy: {}", 
toolName, denial.getMessage());
+        return new AiToolResult.AuthorizationDenied(
+                String.format("Access denied: not authorized to call tool 
'%s'", toolName), denial);
+    }
+
+    private static CamelAuthorizationException 
findAuthorizationException(Throwable error) {
+        for (Throwable t = error; t != null; t = t.getCause()) {
+            if (t instanceof CamelAuthorizationException cae) {
+                return cae;
+            }
+        }
+        return null;
+    }
+
     private static AiToolResult buildSuccessResult(AiToolSpec spec, Exchange 
exchange, String stringBody) {
         String outputSchema = spec.getOutputJsonSchema();
         if (outputSchema == null || outputSchema.isBlank()) {
diff --git 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolResult.java
 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolResult.java
index 0345db189efb..5cea14576540 100644
--- 
a/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolResult.java
+++ 
b/components/camel-ai/camel-ai-tool/src/main/java/org/apache/camel/component/ai/tool/AiToolResult.java
@@ -61,4 +61,17 @@ public sealed interface AiToolResult {
      */
     record ExecutionError(String message, Exception cause) implements 
AiToolResult {
     }
+
+    /**
+     * The route's {@link org.apache.camel.spi.AuthorizationPolicy} denied the 
call: the caller is not authorized to
+     * invoke this tool. Framework adapters should return {@link #message()} 
to the model as a short refusal it can
+     * relay, rather than rethrowing or failing the exchange — a denial is 
expected control flow, not a tool error, so
+     * it is not subject to the tool-execution error strategy. {@link 
#message()} is a generic, caller-safe refusal and
+     * must not be enriched with the cause before returning it to the model.
+     *
+     * @param message a short, caller-safe refusal message (no internal policy 
detail)
+     * @param cause   the underlying {@link 
org.apache.camel.CamelAuthorizationException}
+     */
+    record AuthorizationDenied(String message, Exception cause) implements 
AiToolResult {
+    }
 }
diff --git 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyStartupTest.java
 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyStartupTest.java
new file mode 100644
index 000000000000..2f2540924a75
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyStartupTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.ai.tool;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.CamelContext;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.EventNotifierSupport;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24831: the authorization guard must hold during the window between a 
tool's early (warm-up) registration and
+ * its consumer start. Since CAMEL-25000 the tool is published during warm-up, 
so a route with a lower startupOrder can
+ * invoke it before the tool route's own consumer starts; the guard must 
already be in place by then (applied in
+ * {@code prepare()} before {@code register()}), and {@code 
getToolProcessor()} must fail closed otherwise.
+ */
+class AiToolAuthorizationPolicyStartupTest extends CamelTestSupport {
+
+    private static final AtomicReference<AiToolResult> EARLY = new 
AtomicReference<>();
+
+    static final class DenyAll implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+            // nothing to prepare
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> {
+                throw new CamelAuthorizationException("denied", exchange);
+            };
+        }
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext ctx = super.createCamelContext();
+        ctx.getRegistry().bind("denyAll", new DenyAll());
+        ctx.getManagementStrategy().addEventNotifier(new 
EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) {
+                // the caller route has started but the ai-tool route's 
consumer has not: the guard must already apply
+                if (event instanceof CamelEvent.RouteStartedEvent rse && 
"caller".equals(rse.getRoute().getRouteId())) {
+                    
AiToolRegistry.getOrCreate(ctx).getToolsByTag("t").stream().findFirst()
+                            .ifPresent(spec -> EARLY.set(
+                                    AiToolExecutor.execute(spec, Map.of(), new 
DefaultExchange(ctx))));
+                }
+            }
+        });
+        return ctx;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                
from("direct:caller").routeId("caller").startupOrder(1).log("caller started");
+                
from("ai-tool:transfer?tags=t&description=Transfer&authorizationPolicy=#denyAll")
+                        
.routeId("tool").startupOrder(2).setBody(constant("TRANSFERRED"));
+            }
+        };
+    }
+
+    @Test
+    void callDuringStartupMustBeDenied() {
+        assertThat(EARLY.get())
+                .as("a tool call during the warm-up/start window must be 
guarded, not fall back to the unguarded route")
+                .isInstanceOf(AiToolResult.AuthorizationDenied.class);
+    }
+}
diff --git 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyTest.java
 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyTest.java
new file mode 100644
index 000000000000..6f6a4183bc1d
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolAuthorizationPolicyTest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.ai.tool;
+
+import java.util.Map;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.Exchange;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.spi.Registry;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24831: an {@code authorizationPolicy} on an {@code ai-tool} route 
guards the tool call by construction. The
+ * policy runs before the route body; a denial surfaces as {@link 
AiToolResult.AuthorizationDenied} (a short refusal),
+ * not as a route result or a rethrow.
+ */
+class AiToolAuthorizationPolicyTest extends CamelTestSupport {
+
+    /**
+     * Allows the call only when the exchange carries {@code subject == 
alice}; otherwise throws
+     * {@link CamelAuthorizationException}, exactly as a real {@link 
AuthorizationPolicy} does.
+     */
+    static final class SubjectPolicy implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+            // nothing to prepare
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> {
+                if (!"alice".equals(exchange.getProperty("subject", 
String.class))) {
+                    throw new CamelAuthorizationException("caller is not 
authorized", exchange);
+                }
+                processor.process(exchange);
+            };
+        }
+    }
+
+    /** Allows every call (delegates straight to the route). */
+    static final class AllowAll implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return processor::process;
+        }
+    }
+
+    /** Denies by setting the exception on the exchange rather than throwing 
(as Spring Security's policy does). */
+    static final class ExceptionDeny implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> exchange.setException(new 
CamelAuthorizationException("denied via exchange", exchange));
+        }
+    }
+
+    @Override
+    protected void bindToRegistry(Registry registry) {
+        registry.bind("subjectPolicy", new SubjectPolicy());
+        registry.bind("allowAll", new AllowAll());
+        registry.bind("exceptionDeny", new ExceptionDeny());
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("ai-tool:guarded"
+                     + "?tags=test"
+                     + "&description=A guarded tool"
+                     + "&authorizationPolicy=#subjectPolicy")
+                        .setBody(constant("ok"));
+
+                from("ai-tool:exceptionGuarded"
+                     + "?tags=test"
+                     + "&description=A tool whose policy sets the exception 
instead of throwing"
+                     + "&authorizationPolicy=#exceptionDeny")
+                        .setBody(constant("ok"));
+
+                from("ai-tool:allowedButFails"
+                     + "?tags=test"
+                     + "&description=A tool that is allowed but whose route 
fails"
+                     + "&authorizationPolicy=#allowAll")
+                        .throwException(new RuntimeException("route boom"));
+            }
+        };
+    }
+
+    @Test
+    void allowsTheCallWhenThePolicyPasses() {
+        AiToolSpec spec = findSpec("guarded");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setProperty("subject", "alice");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), exchange);
+
+        assertThat(result).isInstanceOf(AiToolResult.Success.class);
+        assertThat(((AiToolResult.Success) result).value()).isEqualTo("ok");
+    }
+
+    @Test
+    void deniesTheCallAsARefusalWhenThePolicyRejects() {
+        AiToolSpec spec = findSpec("guarded");
+        Exchange exchange = new DefaultExchange(context);
+        exchange.setProperty("subject", "mallory");
+
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), exchange);
+
+        // a denial is its own result type (a short refusal), not a Success 
and not a generic ExecutionError
+        
assertThat(result).isInstanceOf(AiToolResult.AuthorizationDenied.class);
+        assertThat(((AiToolResult.AuthorizationDenied) 
result).message()).contains("guarded");
+    }
+
+    @Test
+    void deniesWhenThePolicySetsTheExceptionInsteadOfThrowing() {
+        AiToolSpec spec = findSpec("exceptionGuarded");
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), new 
DefaultExchange(context));
+        
assertThat(result).isInstanceOf(AiToolResult.AuthorizationDenied.class);
+        assertThat(((AiToolResult.AuthorizationDenied) 
result).message()).contains("exceptionGuarded");
+    }
+
+    @Test
+    void aNormalRouteErrorIsAnExecutionErrorNotADenial() {
+        // the policy allows, but the route then throws a non-authorization 
exception: it must come back as an
+        // ExecutionError, not be misclassified as a denial
+        AiToolSpec spec = findSpec("allowedButFails");
+        AiToolResult result = AiToolExecutor.execute(spec, Map.of(), new 
DefaultExchange(context));
+        assertThat(result).isInstanceOf(AiToolResult.ExecutionError.class);
+    }
+
+    private AiToolSpec findSpec(String toolName) {
+        return 
AiToolRegistry.getOrCreate(context).getToolsByTag("test").stream()
+                .filter(s -> toolName.equals(s.getName()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("Tool not found: " + 
toolName));
+    }
+}
diff --git 
a/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolComponentAuthorizationPolicyTest.java
 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolComponentAuthorizationPolicyTest.java
new file mode 100644
index 000000000000..18c81d99aba4
--- /dev/null
+++ 
b/components/camel-ai/camel-ai-tool/src/test/java/org/apache/camel/component/ai/tool/AiToolComponentAuthorizationPolicyTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.ai.tool;
+
+import java.util.Map;
+
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.AuthorizationPolicy;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-24831 "guard by construction": an {@code authorizationPolicy} set on 
the ai-tool component guards every tool
+ * route, and a per-endpoint {@code authorizationPolicy} overrides it.
+ */
+class AiToolComponentAuthorizationPolicyTest extends CamelTestSupport {
+
+    static final class SubjectPolicy implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> {
+                if (!"alice".equals(exchange.getProperty("subject", 
String.class))) {
+                    throw new CamelAuthorizationException("caller is not 
authorized", exchange);
+                }
+                processor.process(exchange);
+            };
+        }
+    }
+
+    static final class AllowAll implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return processor::process;
+        }
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext ctx = super.createCamelContext();
+        ctx.getRegistry().bind("allowAll", new AllowAll());
+        // set the policy on the component, so it guards every tool route by 
construction
+        AiToolComponent component = ctx.getComponent("ai-tool", 
AiToolComponent.class);
+        component.getConfiguration().setAuthorizationPolicy(new 
SubjectPolicy());
+        return ctx;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            public void configure() {
+                from("ai-tool:inherits?tags=t&description=Inherits the 
component policy")
+                        .setBody(constant("ok"));
+                
from("ai-tool:overrides?tags=t&description=Overrides&authorizationPolicy=#allowAll")
+                        .setBody(constant("ok"));
+            }
+        };
+    }
+
+    @Test
+    void componentPolicyGuardsRoutesWithoutAnEndpointOption() {
+        AiToolSpec spec = findSpec("inherits");
+
+        Exchange bob = new DefaultExchange(context);
+        bob.setProperty("subject", "bob");
+        assertThat(AiToolExecutor.execute(spec, Map.of(), bob))
+                .isInstanceOf(AiToolResult.AuthorizationDenied.class);
+
+        Exchange alice = new DefaultExchange(context);
+        alice.setProperty("subject", "alice");
+        assertThat(AiToolExecutor.execute(spec, Map.of(), alice))
+                .isInstanceOf(AiToolResult.Success.class);
+    }
+
+    @Test
+    void endpointPolicyOverridesTheComponentPolicy() {
+        // bob would be denied by the component policy, but the endpoint 
overrides with allow-all
+        AiToolSpec spec = findSpec("overrides");
+        Exchange bob = new DefaultExchange(context);
+        bob.setProperty("subject", "bob");
+        assertThat(AiToolExecutor.execute(spec, Map.of(), bob))
+                .isInstanceOf(AiToolResult.Success.class);
+    }
+
+    private AiToolSpec findSpec(String toolName) {
+        return AiToolRegistry.getOrCreate(context).getToolsByTag("t").stream()
+                .filter(s -> toolName.equals(s.getName()))
+                .findFirst()
+                .orElseThrow(() -> new AssertionError("Tool not found: " + 
toolName));
+    }
+}
diff --git 
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
 
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
index d82c6da90bb7..87b135a1bfb8 100644
--- 
a/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
+++ 
b/components/camel-ai/camel-langchain4j-agent/src/main/java/org/apache/camel/component/langchain4j/agent/LangChain4jAgentProducer.java
@@ -484,6 +484,10 @@ public class LangChain4jAgentProducer extends 
DefaultProducer {
         } else if (result instanceof AiToolResult.ArgumentError error) {
             LOG.warn("Tool '{}' argument error: {}", toolName, 
error.message(), error.cause());
             return "Invalid arguments: " + error.message();
+        } else if (result instanceof AiToolResult.AuthorizationDenied denied) {
+            // A denial is expected control flow: relay the refusal to the 
model instead of rethrowing.
+            LOG.warn("Tool '{}' call denied by authorization policy", 
toolName);
+            return denied.message();
         } else if (result instanceof AiToolResult.ExecutionError error) {
             // Rethrow so LangChain4j's error handling machinery
             // (ToolExecutionErrorHandler, compensateOnToolErrors) can fire.
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
index 2bdc6750fed0..1f39b1083546 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpServerBridge.java
@@ -386,7 +386,17 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
     }
 
     private McpServerTool createTool(AiToolSpec spec) {
-        McpToolCallHandler handler = arguments -> execute(spec, arguments);
+        McpToolCallHandler handler = new McpToolCallHandler() {
+            @Override
+            public McpToolCallResult call(Map<String, Object> arguments) {
+                return execute(spec, arguments, null);
+            }
+
+            @Override
+            public McpToolCallResult call(Map<String, Object> arguments, 
McpToolCallContext context) {
+                return execute(spec, arguments, context != null ? 
context.securityPrincipal() : null);
+            }
+        };
         return new McpServerTool() {
             @Override
             public String name() {
@@ -425,8 +435,13 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
         };
     }
 
-    private McpToolCallResult execute(AiToolSpec spec, Map<String, Object> 
arguments) {
+    private McpToolCallResult execute(AiToolSpec spec, Map<String, Object> 
arguments, Object securityPrincipal) {
         Exchange exchange = spec.getConsumer().getEndpoint().createExchange();
+        if (securityPrincipal != null) {
+            // carry the transport's authenticated caller onto the tool 
exchange so a tool route's AuthorizationPolicy
+            // can authorize on it; it is an exchange property, which the 
model cannot set
+            
exchange.setProperty(McpToolCallContext.SECURITY_PRINCIPAL_PROPERTY, 
securityPrincipal);
+        }
         boolean release = true;
         try {
             Future<AiToolResult> future = executor.submit(() -> 
AiToolExecutor.execute(spec, arguments, exchange));
@@ -454,6 +469,10 @@ public class McpServerBridge extends ServiceSupport 
implements CamelContextAware
                 return new McpToolCallResult(success.value(), false, 
success.structuredContent());
             } else if (result instanceof AiToolResult.ArgumentError error) {
                 return new McpToolCallResult(error.message(), true);
+            } else if (result instanceof AiToolResult.AuthorizationDenied 
denied) {
+                // The denial message is already generic and caller-safe, so 
it is safe to return to the MCP client.
+                LOG.warn("MCP tool '{}' call denied by authorization policy", 
spec.getName());
+                return new McpToolCallResult(denied.message(), true);
             } else {
                 AiToolResult.ExecutionError error = 
(AiToolResult.ExecutionError) result;
                 // never leak raw route exception messages to remote MCP 
clients
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallContext.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallContext.java
new file mode 100644
index 000000000000..5c9f6994f858
--- /dev/null
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallContext.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mcp.server;
+
+/**
+ * Transport-supplied context for a single MCP tool call, carrying the 
caller's authenticated security principal when
+ * the transport can determine one. An engine builds it from its transport 
(for the Vert.x streamable HTTP transport,
+ * from the authenticated {@code RoutingContext} user) and passes it to
+ * {@link McpToolCallHandler#call(java.util.Map, McpToolCallContext)}; the 
bridge then stamps the principal onto the
+ * tool exchange as the {@code CamelMcpSecurityPrincipal} exchange property so 
a tool route's
+ * {@link org.apache.camel.spi.AuthorizationPolicy} can authorize on it.
+ * <p>
+ * The principal is an opaque {@link Object} (for example an {@code 
io.vertx.ext.auth.User}) so this transport-agnostic
+ * API does not depend on any transport library.
+ *
+ * @param securityPrincipal the authenticated caller principal, or {@code 
null} when the transport has none
+ * @since                   4.23
+ */
+public record McpToolCallContext(Object securityPrincipal) {
+
+    /**
+     * An empty context: no transport-supplied caller principal.
+     */
+    public static final McpToolCallContext EMPTY = new 
McpToolCallContext(null);
+
+    /**
+     * Exchange property under which the bridge stamps {@link 
#securityPrincipal()} on the tool exchange, so a tool
+     * route (or its {@link org.apache.camel.spi.AuthorizationPolicy}) can 
read the MCP caller identity via
+     * {@code exchangeProperty.CamelMcpSecurityPrincipal}.
+     */
+    public static final String SECURITY_PRINCIPAL_PROPERTY = 
"CamelMcpSecurityPrincipal";
+}
diff --git 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallHandler.java
 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallHandler.java
index 2d429bec4af2..ff4b53575462 100644
--- 
a/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallHandler.java
+++ 
b/components/camel-ai/camel-mcp-server-api/src/main/java/org/apache/camel/component/mcp/server/McpToolCallHandler.java
@@ -37,4 +37,21 @@ public interface McpToolCallHandler {
      * @return           the sanitized result, never null
      */
     McpToolCallResult call(Map<String, Object> arguments);
+
+    /**
+     * Invokes the tool with the given arguments and the transport-supplied 
caller context, so a tool route's
+     * {@link org.apache.camel.spi.AuthorizationPolicy} can authorize on the 
caller's identity. The default ignores the
+     * context and calls {@link #call(Map)}; the bridge overrides it to stamp 
the caller principal onto the tool
+     * exchange. Engines that can determine a caller identity (such as the 
Vert.x streamable HTTP transport) call this
+     * overload; others keep calling {@link #call(Map)}.
+     *
+     * @param  arguments the tool arguments as parsed from the MCP {@code 
tools/call} request, never null
+     * @param  context   the transport-supplied caller context, never null 
(use {@link McpToolCallContext#EMPTY} when
+     *                   there is no caller identity)
+     * @return           the sanitized result, never null
+     * @since            4.23
+     */
+    default McpToolCallResult call(Map<String, Object> arguments, 
McpToolCallContext context) {
+        return call(arguments);
+    }
 }
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
index 7da999cddc6b..78b1551fe478 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpServerEngine.java
@@ -37,6 +37,7 @@ import org.apache.camel.component.mcp.server.McpServerIcon;
 import org.apache.camel.component.mcp.server.McpServerInfo;
 import org.apache.camel.component.mcp.server.McpServerResource;
 import org.apache.camel.component.mcp.server.McpServerTool;
+import org.apache.camel.component.mcp.server.McpToolCallContext;
 import org.apache.camel.component.mcp.server.McpToolCallResult;
 import org.apache.camel.component.platform.http.PlatformHttpComponent;
 import org.apache.camel.component.platform.http.vertx.VertxPlatformHttpRouter;
@@ -66,6 +67,12 @@ public class VertxMcpServerEngine extends ServiceSupport 
implements McpServerEng
             """;
     private static final String APPLICATION_JSON = "application/json";
 
+    /**
+     * Key under which {@link VertxMcpStreamableServerTransportProvider} 
stashes the authenticated caller principal in
+     * the MCP transport context, so the tool-call handler reads it back via 
{@code exchange.transportContext()}.
+     */
+    static final String TRANSPORT_PRINCIPAL_KEY = "CamelMcpTransportPrincipal";
+
     private CamelContext camelContext;
     private McpServerInfo info;
     private McpJsonMapper jsonMapper;
@@ -159,7 +166,11 @@ public class VertxMcpServerEngine extends ServiceSupport 
implements McpServerEng
                 .tool(mcpTool)
                 .callHandler((exchange, request) -> {
                     Map<String, Object> arguments = request.arguments() != 
null ? request.arguments() : Map.of();
-                    McpToolCallResult result = tool.handler().call(arguments);
+                    Object principal = exchange.transportContext() != null
+                            ? 
exchange.transportContext().get(TRANSPORT_PRINCIPAL_KEY) : null;
+                    McpToolCallContext context = principal != null
+                            ? new McpToolCallContext(principal) : 
McpToolCallContext.EMPTY;
+                    McpToolCallResult result = tool.handler().call(arguments, 
context);
                     McpSchema.CallToolResult.Builder builder = 
McpSchema.CallToolResult.builder()
                             .addTextContent(result.text())
                             .isError(result.isError());
diff --git 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
index 7ef1cf0a2ffe..9c6b6805ffe5 100644
--- 
a/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
+++ 
b/components/camel-ai/camel-mcp-server/src/main/java/org/apache/camel/component/mcp/server/vertx/VertxMcpStreamableServerTransportProvider.java
@@ -19,10 +19,12 @@ package org.apache.camel.component.mcp.server.vertx;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import io.modelcontextprotocol.common.McpTransportContext;
 import io.modelcontextprotocol.json.McpJsonMapper;
 import io.modelcontextprotocol.json.TypeRef;
 import io.modelcontextprotocol.spec.HttpHeaders;
@@ -35,6 +37,7 @@ import io.vertx.core.Context;
 import io.vertx.core.Vertx;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.HttpServerResponse;
+import io.vertx.ext.auth.User;
 import io.vertx.ext.web.Route;
 import io.vertx.ext.web.RoutingContext;
 import io.vertx.ext.web.handler.BodyHandler;
@@ -242,8 +245,11 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
             endWithStatus(connection, ctx, 202);
         } else if (message instanceof McpSchema.JSONRPCRequest request) {
             VertxMcpSessionTransport transport = startSseResponse(ctx, 
connection, sessionId);
+            McpTransportContext transportContext = toTransportContext(ctx);
             try {
-                managed.session.responseStream(request, transport).block();
+                managed.session.responseStream(request, transport)
+                        .contextWrite(reactorCtx -> 
reactorCtx.put(McpTransportContext.KEY, transportContext))
+                        .block();
             } catch (Exception e) {
                 LOG.warn("Failed to handle MCP request stream: {}", 
e.getMessage());
                 transport.close();
@@ -254,6 +260,20 @@ public class VertxMcpStreamableServerTransportProvider 
implements McpStreamableS
         }
     }
 
+    /**
+     * Builds the MCP transport context carrying the authenticated caller 
principal (when the HTTP request was
+     * authenticated), so a tool route invoked by this request can authorize 
on it. The principal is the Vert.x
+     * {@link User} that the platform-http authentication handler set on the 
routing context; absent it, the context is
+     * empty.
+     */
+    private static McpTransportContext toTransportContext(RoutingContext ctx) {
+        User user = ctx.user();
+        if (user == null) {
+            return McpTransportContext.EMPTY;
+        }
+        return 
McpTransportContext.create(Map.of(VertxMcpServerEngine.TRANSPORT_PRINCIPAL_KEY, 
user));
+    }
+
     private void handleInitialize(RoutingContext ctx, Context connection, 
McpSchema.JSONRPCRequest request)
             throws Exception {
         McpSchema.InitializeRequest initializeRequest
diff --git 
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
index a15a6eccdaa6..90229bc1121c 100644
--- 
a/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
+++ 
b/components/camel-ai/camel-mcp-server/src/test/java/org/apache/camel/component/mcp/server/main/McpServerMainAuthenticationTest.java
@@ -28,8 +28,14 @@ import io.modelcontextprotocol.client.McpClient;
 import io.modelcontextprotocol.client.McpSyncClient;
 import 
io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
 import io.modelcontextprotocol.spec.McpSchema;
+import org.apache.camel.CamelAuthorizationException;
+import org.apache.camel.NamedNode;
+import org.apache.camel.Processor;
+import org.apache.camel.Route;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mcp.server.McpToolCallContext;
 import org.apache.camel.main.Main;
+import org.apache.camel.spi.AuthorizationPolicy;
 import org.apache.camel.test.AvailablePortFinder;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
@@ -62,8 +68,18 @@ class McpServerMainAuthenticationTest {
                 from("ai-tool:say_hello?tags=secured&description=Say hello"
                      + "&parameter.name=string&parameter.name.required=true")
                         .setBody(simple("Hello ${header.name}"));
+
+                from("ai-tool:whoami?tags=secured&description=Report whether 
the caller is authenticated")
+                        .process(e -> {
+                            Object principal = 
e.getProperty(McpToolCallContext.SECURITY_PRINCIPAL_PROPERTY);
+                            e.getMessage().setBody(principal != null ? 
"principal-present" : "no-principal");
+                        });
+
+                from("ai-tool:blocked?tags=secured&description=Always 
denied&authorizationPolicy=#deny")
+                        .setBody(constant("SHOULD-NOT-RUN"));
             }
         });
+        main.bind("deny", new DenyAll());
         main.addInitialProperty("camel.server.enabled", "true");
         main.addInitialProperty("camel.server.port", String.valueOf(PORT));
         main.addInitialProperty("camel.server.authentication-enabled", "true");
@@ -127,6 +143,75 @@ class McpServerMainAuthenticationTest {
         }
     }
 
+    @Test
+    void testMcpToolCallCarriesAuthenticatedPrincipalToTheToolRoute() {
+        // CAMEL-24831: the authenticated caller (the Vert.x basic-auth user) 
must reach the tool route over MCP as the
+        // CamelMcpSecurityPrincipal exchange property, so a tool route's 
AuthorizationPolicy can authorize on it.
+        String credentials = 
Base64.getEncoder().encodeToString("camel:mcpPass".getBytes(UTF_8));
+        McpSyncClient client = null;
+        try {
+            client = 
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:"; + 
PORT)
+                    .httpRequestCustomizer((builder, method, uri, body, 
context) -> builder
+                            .header("Authorization", "Basic " + credentials))
+                    .build())
+                    .requestTimeout(Duration.ofSeconds(10))
+                    .initializationTimeout(Duration.ofSeconds(10))
+                    .build();
+
+            client.initialize();
+
+            McpSchema.CallToolResult result
+                    = client.callTool(new McpSchema.CallToolRequest("whoami", 
Map.of()));
+            assertThat(result.isError()).isNotEqualTo(Boolean.TRUE);
+            
assertThat(result.content().toString()).contains("principal-present");
+        } finally {
+            if (client != null) {
+                client.closeGracefully();
+            }
+        }
+    }
+
+    @Test
+    void testMcpToolCallDeniedByPolicyIsReturnedAsError() {
+        // CAMEL-24831: a tool guarded by a deny-all authorizationPolicy, 
called over MCP, comes back as an error
+        // (the model-relayable refusal), and the route body does not run.
+        String credentials = 
Base64.getEncoder().encodeToString("camel:mcpPass".getBytes(UTF_8));
+        McpSyncClient client = null;
+        try {
+            client = 
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:"; + 
PORT)
+                    .httpRequestCustomizer((builder, method, uri, body, 
context) -> builder
+                            .header("Authorization", "Basic " + credentials))
+                    .build())
+                    .requestTimeout(Duration.ofSeconds(10))
+                    .initializationTimeout(Duration.ofSeconds(10))
+                    .build();
+            client.initialize();
+
+            McpSchema.CallToolResult result
+                    = client.callTool(new McpSchema.CallToolRequest("blocked", 
Map.of()));
+            assertThat(result.isError()).isEqualTo(Boolean.TRUE);
+            assertThat(result.content().toString()).contains("blocked");
+        } finally {
+            if (client != null) {
+                client.closeGracefully();
+            }
+        }
+    }
+
+    /** Denies every call, so a tool guarded by it must come back to the MCP 
client as an error. */
+    static final class DenyAll implements AuthorizationPolicy {
+        @Override
+        public void beforeWrap(Route route, NamedNode definition) {
+        }
+
+        @Override
+        public Processor wrap(Route route, Processor processor) {
+            return exchange -> {
+                throw new CamelAuthorizationException("denied", exchange);
+            };
+        }
+    }
+
     private static HttpRequest.Builder mcpRequest() {
         return HttpRequest.newBuilder(URI.create("http://localhost:"; + PORT + 
"/mcp"))
                 .header("Accept", "application/json, text/event-stream");
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
index 54ccc06e2987..40fbf92d2e24 100644
--- 
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
@@ -290,6 +290,10 @@ class McpToolCallExecutor extends ServiceSupport {
                 } else if (result instanceof AiToolResult.ArgumentError error) 
{
                     LOG.warn("Route tool '{}' argument error: {}", toolName, 
error.message());
                     return errorResult(toolCall, "Error: invalid tool 
arguments: " + error.message());
+                } else if (result instanceof AiToolResult.AuthorizationDenied 
denied) {
+                    // A denial is expected control flow: always relay the 
refusal to the model, never fail the exchange.
+                    LOG.warn("Route tool '{}' call denied by authorization 
policy", toolName);
+                    return errorResult(toolCall, denied.message());
                 } else {
                     AiToolResult.ExecutionError error = 
(AiToolResult.ExecutionError) result;
                     if (config.getToolExecutionErrorStrategy() == 
ToolExecutionErrorStrategy.FAIL_EXCHANGE) {
diff --git 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java
 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java
index e4992f29e30c..713dc5f38dee 100644
--- 
a/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java
+++ 
b/components/camel-spring-parent/camel-spring-ai/camel-spring-ai-chat/src/main/java/org/apache/camel/component/springai/chat/AiToolSpecToSpringAi.java
@@ -49,6 +49,10 @@ final class AiToolSpecToSpringAi {
                     return success.value();
                 } else if (result instanceof AiToolResult.ArgumentError 
argErr) {
                     return "Tool execution failed: " + argErr.message();
+                } else if (result instanceof AiToolResult.AuthorizationDenied 
denied) {
+                    // A denial is expected control flow: relay the refusal to 
the model.
+                    LOG.warn("Tool '{}' call denied by authorization policy", 
spec.getName());
+                    return denied.message();
                 } else if (result instanceof AiToolResult.ExecutionError 
execErr) {
                     LOG.warn("Tool '{}' execution failed: {}", spec.getName(), 
execErr.message(), execErr.cause());
                     return "Tool execution failed";
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 17b251975ef4..c0db19ae1876 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -4413,3 +4413,12 @@ Property placeholders are kept as written instead of 
being resolved.
 
 In return, Java is also a target format, and rests, route templates, route 
configurations and beans are converted
 instead of only routes. A new `notes` field of the result lists what differs 
or is not carried over.
+
+=== camel-ai-tool - new AiToolResult.AuthorizationDenied result variant (SPI)
+
+`org.apache.camel.component.ai.tool.AiToolResult` is a sealed interface and 
gained a new variant,
+`AiToolResult.AuthorizationDenied`, returned when an `ai-tool` route's 
`authorizationPolicy` denies a call. Code that
+consumes `AiToolResult` and handles its variants exhaustively — for example an 
AI adapter whose final branch casts to
+`AiToolResult.ExecutionError` — must add a branch for `AuthorizationDenied` 
and relay its `message()` to the model as
+a refusal (do not rethrow). The in-tree adapters (camel-openai, 
camel-spring-ai-chat, camel-langchain4j-agent and the
+MCP server bridge) already handle it.
diff --git 
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
 
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
index 9d3eea663f1c..658e4f8d9102 100644
--- 
a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
+++ 
b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/AiToolComponentBuilderFactory.java
@@ -343,6 +343,33 @@ public interface AiToolComponentBuilderFactory {
             doSetProperty("autowiredEnabled", autowiredEnabled);
             return this;
         }
+    
+        /**
+         * Reference to an org.apache.camel.spi.AuthorizationPolicy used to
+         * authorize tool calls before the route runs. Set it on the component
+         * to guard every tool route by construction, or per endpoint to
+         * override. The policy authorizes on trustworthy input only: the tool
+         * name comes from the route (never from model output), and the caller
+         * identity is carried as an exchange property set before the agent ran
+         * (for example by camel-spiffe or camel-keycloak), which the model
+         * cannot set. Authorize on exchange properties or validated tokens
+         * only, never on message headers (on a tool route the headers carry 
the
+         * model-controlled tool arguments). A denied call surfaces to the 
model
+         * as a short refusal rather than a stack trace.
+         * 
+         * The option is a:
+         * &lt;code&gt;org.apache.camel.spi.AuthorizationPolicy&lt;/code&gt;
+         * type.
+         * 
+         * Group: security
+         * 
+         * @param authorizationPolicy the value to set
+         * @return the dsl builder
+         */
+        default AiToolComponentBuilder 
authorizationPolicy(org.apache.camel.spi.AuthorizationPolicy 
authorizationPolicy) {
+            doSetProperty("authorizationPolicy", authorizationPolicy);
+            return this;
+        }
     }
 
     class AiToolComponentBuilderImpl
@@ -379,6 +406,7 @@ public interface AiToolComponentBuilderFactory {
             case "tags": getOrCreateConfiguration((AiToolComponent) 
component).setTags((java.lang.String) value); return true;
             case "title": getOrCreateConfiguration((AiToolComponent) 
component).setTitle((java.lang.String) value); return true;
             case "autowiredEnabled": ((AiToolComponent) 
component).setAutowiredEnabled((boolean) value); return true;
+            case "authorizationPolicy": 
getOrCreateConfiguration((AiToolComponent) 
component).setAuthorizationPolicy((org.apache.camel.spi.AuthorizationPolicy) 
value); return true;
             default: return false;
             }
         }
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
index 6813dc670ba4..9b46adc52f2e 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/AiToolEndpointBuilderFactory.java
@@ -393,6 +393,56 @@ public interface AiToolEndpointBuilderFactory {
             doSetProperty("title", title);
             return this;
         }
+        /**
+         * Reference to an org.apache.camel.spi.AuthorizationPolicy used to
+         * authorize tool calls before the route runs. Set it on the component
+         * to guard every tool route by construction, or per endpoint to
+         * override. The policy authorizes on trustworthy input only: the tool
+         * name comes from the route (never from model output), and the caller
+         * identity is carried as an exchange property set before the agent ran
+         * (for example by camel-spiffe or camel-keycloak), which the model
+         * cannot set. Authorize on exchange properties or validated tokens
+         * only, never on message headers (on a tool route the headers carry 
the
+         * model-controlled tool arguments). A denied call surfaces to the 
model
+         * as a short refusal rather than a stack trace.
+         * 
+         * The option is a:
+         * <code>org.apache.camel.spi.AuthorizationPolicy</code> type.
+         * 
+         * Group: security
+         * 
+         * @param authorizationPolicy the value to set
+         * @return the dsl builder
+         */
+        default AiToolEndpointBuilder 
authorizationPolicy(org.apache.camel.spi.AuthorizationPolicy 
authorizationPolicy) {
+            doSetProperty("authorizationPolicy", authorizationPolicy);
+            return this;
+        }
+        /**
+         * Reference to an org.apache.camel.spi.AuthorizationPolicy used to
+         * authorize tool calls before the route runs. Set it on the component
+         * to guard every tool route by construction, or per endpoint to
+         * override. The policy authorizes on trustworthy input only: the tool
+         * name comes from the route (never from model output), and the caller
+         * identity is carried as an exchange property set before the agent ran
+         * (for example by camel-spiffe or camel-keycloak), which the model
+         * cannot set. Authorize on exchange properties or validated tokens
+         * only, never on message headers (on a tool route the headers carry 
the
+         * model-controlled tool arguments). A denied call surfaces to the 
model
+         * as a short refusal rather than a stack trace.
+         * 
+         * The option will be converted to a
+         * <code>org.apache.camel.spi.AuthorizationPolicy</code> type.
+         * 
+         * Group: security
+         * 
+         * @param authorizationPolicy the value to set
+         * @return the dsl builder
+         */
+        default AiToolEndpointBuilder authorizationPolicy(String 
authorizationPolicy) {
+            doSetProperty("authorizationPolicy", authorizationPolicy);
+            return this;
+        }
     }
 
     /**

Reply via email to