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

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


The following commit(s) were added to refs/heads/main by this push:
     new ba27c6a24890 CAMEL-26399: camel-jbang - AI tools: accept the argument 
names models pass, and name the expected ones when one is missing (#28677)
ba27c6a24890 is described below

commit ba27c6a24890899ced95ff52b275eef2f0009006
Author: Federico Mariani <[email protected]>
AuthorDate: Thu Oct 29 13:36:54 2026 +0100

    CAMEL-26399: camel-jbang - AI tools: accept the argument names models pass, 
and name the expected ones when one is missing (#28677)
    
    The catalog tools name their main argument differently (camel_catalog_doc 
takes
    name, camel_catalog_find term, camel_catalog_example_file example,
    camel_validate_source file and content), and models carry the names of one 
tool
    over to the next. The Quarkus MCP server rejects such a call before the tool
    runs with a bare "Missing required argument: term" and drops the argument it
    does not know without a word, so the model learns which name is missing but 
not
    that its own argument was meant for it; in the eval most agents then gave 
up on
    the tool.
    
    ToolArgumentAliases (camel-jbang-core, next to ToolRegistry) keeps the names
    models use: query and name for term, name for example, path, filename and 
name
    for file, source for content. An alias is taken only when the call does not 
give
    the declared argument and the tool has no argument of that name, so the 
name of
    camel_catalog_doc is never touched; of several aliases of one argument the 
first
    in the table wins, a path before a bare file name. A path is passed on as 
the
    file unchanged.
    
    McpArgumentGuardrail is declared with @ToolGuardrails on every tool of the
    server. Before the arguments are checked it takes the aliases for the 
declared
    names, and a call of any tool that still misses a required argument is 
answered
    with the tool's arguments and the ones it got ("camel_catalog_find takes 
term
    (required), kind, limit, camelVersion; got q"). After the call a JSON object
    answer gets an argumentNote field ("term is the argument name (got query)"),
    which also names the arguments the tool ignored. The note goes from input to
    output through a request scoped bean; the guardrail does not block, so it
    supports every execution model. A test fails when a tool lacks the 
guardrail.
    
    camel ask resolves the same aliases and adds the same note to its JSON 
answers.
---
 .../modules/ROOT/pages/camel-jbang-ai.adoc         |   4 +
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  38 +++
 .../camel/dsl/jbang/core/commands/AskTools.java    |  10 +-
 .../core/commands/ai/ToolArgumentAliases.java      | 187 +++++++++++++
 .../dsl/jbang/core/commands/AskToolsTest.java      |  18 ++
 .../core/commands/ai/ToolArgumentAliasesTest.java  | 200 ++++++++++++++
 .../dsl/jbang/core/commands/mcp/AdvisoryTools.java |   2 +
 .../core/commands/mcp/AiPipelineScaffoldTools.java |   2 +
 .../dsl/jbang/core/commands/mcp/AiTraceTools.java  |   2 +
 .../jbang/core/commands/mcp/AuthoringTools.java    |  16 ++
 .../dsl/jbang/core/commands/mcp/CatalogTools.java  |   2 +
 .../commands/mcp/ConfigurationValidateTools.java   |   2 +
 .../core/commands/mcp/DependencyCheckTools.java    |   2 +
 .../commands/mcp/DependencySecurityAuditTools.java |   2 +
 .../dsl/jbang/core/commands/mcp/DiagnoseTools.java |   2 +
 .../dsl/jbang/core/commands/mcp/ExampleTools.java  |   3 +
 .../dsl/jbang/core/commands/mcp/ExplainTools.java  |   2 +
 .../dsl/jbang/core/commands/mcp/HardenTools.java   |   2 +
 .../dsl/jbang/core/commands/mcp/KameletTools.java  |   3 +
 .../core/commands/mcp/McpArgumentGuardrail.java    | 140 ++++++++++
 .../jbang/core/commands/mcp/MigrationTools.java    |   5 +
 .../commands/mcp/MigrationWildflyKarafTools.java   |   2 +
 .../dsl/jbang/core/commands/mcp/OpenApiTools.java  |   3 +
 .../commands/mcp/PropertiesTranslateTools.java     |   2 +
 .../core/commands/mcp/RouteCostEstimateTools.java  |   2 +
 .../jbang/core/commands/mcp/RouteDiagramTools.java |   2 +
 .../dsl/jbang/core/commands/mcp/RuntimeTools.java  |  43 +++
 .../jbang/core/commands/mcp/SecurityScanTools.java |   2 +
 .../jbang/core/commands/mcp/TestScaffoldTools.java |   2 +
 .../jbang/core/commands/mcp/TransformTools.java    |   2 +
 .../dsl/jbang/core/commands/mcp/VersionTools.java  |   2 +
 .../commands/mcp/McpArgumentGuardrailTest.java     | 301 +++++++++++++++++++++
 .../jbang/core/commands/mcp/McpToolClasses.java    |  59 +++-
 33 files changed, 1058 insertions(+), 8 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
index afe21d3e232a..8cfb5d7eab93 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
@@ -44,6 +44,10 @@ The agent has access to tools that let it:
 * Discover and run any CLI command
 * Read and change the integration's files, each change validated first
 
+A model that names an argument the way another tool names it, such as `query` 
for the `term` of
+`camel_catalog_find`, still gets its call answered, with a note of the name to 
use (see
+xref:camel-jbang-mcp.adoc#_argument_names[Argument names]).
+
 Use `--show-tools` to see tool calls and results as they happen:
 
 [source,bash]
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index f6d9ecf98fa0..62b061c5291c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -517,6 +517,44 @@ in full, so re-reading a file after an edit or polling a 
log keeps working.
 A session is an MCP connection of `camel mcp`, and for `camel tui --mcp` the 
time from one `initialize` of the
 client to the next.
 
+==== Argument names
+
+Models carry the argument names of one tool over to the next: 
`camel_catalog_doc` takes `name`, so a model calls
+`camel_catalog_find` with `name` or `query` instead of `term`. `camel mcp` 
takes these names for the ones the tool
+declares:
+
+[cols="1,1,2",options="header"]
+|===
+| Tool | Argument | Also accepted
+
+| `camel_catalog_find` | `term` | `query`, `name`
+| `camel_catalog_example_file` | `example` | `name`
+| `camel_validate_source` | `file` | `path`, `filename`, `name`
+| `camel_validate_source` | `content` | `source`
+|===
+
+Another name is taken only when the call does not give the argument itself, 
and never when the tool has an argument
+of that name (the `name` of `camel_catalog_doc` stays its `name`). When a call 
gives several, the first one listed
+wins. A `path` is passed on as the `file`, as it is. The tool listing names 
the declared arguments only, and a JSON
+answer says which name to use the next time in its first field, `argumentNote`:
+
+[source,json]
+----
+{"argumentNote": "term is the argument name (got query)", "term": "activemq", 
"matches": [...]}
+----
+
+The note also names an argument the tool does not take, which is ignored, with 
the arguments the tool takes:
+`ignored: bogus; camel_catalog_find takes term (required), kind, limit, 
camelVersion`.
+
+A call of any tool that misses a required argument is answered, as a tool 
error, with the arguments of the tool in
+the order it declares them and the ones the call gave, rather than with the 
name of the missing one only:
+
+----
+camel_catalog_find takes term (required), kind, limit, camelVersion; got q
+----
+
+`camel ask` (see xref:camel-jbang-ai.adoc[]) takes the same names, and adds 
the same note to its JSON answers.
+
 === Catalog Exploration
 
 [cols="1,3",options="header"]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
index 58333a6c21e9..b5e81b494910 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
@@ -30,6 +30,7 @@ import java.util.function.Predicate;
 
 import org.apache.camel.dsl.jbang.core.commands.ai.AppFeatures;
 import org.apache.camel.dsl.jbang.core.commands.ai.AuthoringGuidelines;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolArgumentAliases;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolConfirms;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
@@ -294,16 +295,19 @@ public class AskTools {
                         }
                     }
                 }
-                ToolConfirms.Call call = confirmation(name, argMap);
+                // the argument names a model carries over from another tool, 
such as query for term (CAMEL-26399)
+                ToolArgumentAliases.Resolved<String> named
+                        = ToolArgumentAliases.resolve(name, 
ToolArgumentAliases.arguments(td), argMap);
+                ToolConfirms.Call call = confirmation(name, named.arguments());
                 if (!ctx.confirm(call)) {
                     return rejected(call);
                 }
-                Object result = ToolRegistry.execute(name, ctx, argMap);
+                Object result = ToolRegistry.execute(name, ctx, 
named.arguments());
                 if ("select_process".equals(name) && ctx.hasProcess()) {
                     // the file tools follow the integration
                     setTargetPid(ctx.pid());
                 }
-                return result != null ? result.toString() : "";
+                return ToolArgumentAliases.withNote(result != null ? 
result.toString() : "", named.note());
             }
             return switch (name) {
                 case CLI_LIST_COMMANDS -> executeCliListCommands(args);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliases.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliases.java
new file mode 100644
index 000000000000..ef1904c4e3b4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliases.java
@@ -0,0 +1,187 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * The names models give the arguments of a tool when they carry them over 
from another tool (CAMEL-26399):
+ * {@code camel_catalog_doc} takes {@code name}, so {@code camel_catalog_find} 
gets {@code name} or {@code query} for
+ * its {@code term}, and {@code camel_validate_source} gets {@code filename} 
or {@code source} for its {@code file} and
+ * {@code content}. Such a call runs as if it had used the name the tool 
declares, and its answer says which name that
+ * is, so the next call uses it.
+ * <p>
+ * An alias is taken only when the call does not give the declared argument 
itself and the tool has no argument of that
+ * name: {@code name} is never touched on {@code camel_catalog_doc}, where it 
is the real argument. When a call gives
+ * several aliases of one argument, the first in the table wins and the others 
are ignored. Used by the MCP server
+ * (before the arguments are checked, so a required argument given by an alias 
is not missing) and by {@code camel ask}.
+ */
+public final class ToolArgumentAliases {
+
+    /** The field of a JSON answer that carries the {@link Resolved#note() 
note}. */
+    public static final String NOTE_KEY = "argumentNote";
+
+    /** For each tool, alias to declared name, in the order the aliases are 
tried. */
+    private static final Map<String, Map<String, String>> ALIASES = Map.of(
+            "camel_catalog_find", pairs("query", "term", "name", "term"),
+            "camel_catalog_example_file", pairs("name", "example"),
+            // the registry tool behind camel_catalog_example_file, as camel 
ask names it
+            "get_example_file", pairs("name", "example"),
+            // a path says more than a file name, so it wins when a call gives 
both
+            "camel_validate_source", pairs("path", "file", "filename", "file", 
"name", "file", "source", "content"));
+
+    private ToolArgumentAliases() {
+    }
+
+    /** An argument a tool declares: its name and whether a call has to give 
it. */
+    public record Argument(String name, boolean required) {
+    }
+
+    /**
+     * A call with its aliases taken for the declared names.
+     *
+     * @param tool      the tool name
+     * @param declared  the arguments of the tool, in the order it declares 
them
+     * @param given     the names of the arguments the call gave, in its order
+     * @param arguments the arguments of the call under the declared names; an 
ignored argument is left in place
+     * @param renamed   for each declared name taken from an alias, that alias
+     * @param ignored   the names given that the tool does not take, with the 
aliases that lost to another one
+     */
+    public record Resolved<V>(String tool, List<Argument> declared, 
List<String> given, Map<String, V> arguments,
+            Map<String, String> renamed, List<String> ignored) {
+
+        /** The required arguments the call still does not give: it cannot 
run. */
+        public List<String> missing() {
+            return declared.stream()
+                    .filter(a -> a.required() && arguments.get(a.name()) == 
null)
+                    .map(Argument::name)
+                    .toList();
+        }
+
+        /**
+         * The answer to a call that misses a required argument: the arguments 
of the tool and the ones the call gave,
+         * such as {@code camel_catalog_find takes term (required), kind, 
limit, camelVersion; got query}.
+         */
+        public String missingMessage() {
+            return takes() + "; got " + (given.isEmpty() ? "no arguments" : 
String.join(", ", given));
+        }
+
+        /**
+         * What the answer of the call says about its argument names: the 
declared name of each alias, and the arguments
+         * the tool ignored with the ones it takes. Null when the call used 
the declared names only.
+         */
+        public String note() {
+            List<String> parts = new ArrayList<>();
+            renamed.forEach((name, alias) -> parts.add(name + " is the 
argument name (got " + alias + ")"));
+            if (!ignored.isEmpty()) {
+                parts.add("ignored: " + String.join(", ", ignored));
+                parts.add(takes());
+            }
+            return parts.isEmpty() ? null : String.join("; ", parts);
+        }
+
+        private String takes() {
+            List<String> names = declared.stream()
+                    .map(a -> a.required() ? a.name() + " (required)" : 
a.name())
+                    .toList();
+            return tool + " takes " + (names.isEmpty() ? "no arguments" : 
String.join(", ", names));
+        }
+    }
+
+    /** The aliases of a tool, alias to declared name; empty when it has none. 
*/
+    public static Map<String, String> aliases(String tool) {
+        return ALIASES.getOrDefault(tool, Map.of());
+    }
+
+    /** The arguments a registry tool declares, for {@link #resolve(String, 
List, Map)}. */
+    public static List<Argument> arguments(ToolDescriptor tool) {
+        return tool.params().stream().map(p -> new Argument(p.name(), 
p.required())).toList();
+    }
+
+    /**
+     * Takes the aliases of a call for the names the tool declares. A null 
value counts as not given, the way the MCP
+     * server reads it.
+     *
+     * @param tool      the tool name
+     * @param declared  the arguments of the tool, in the order it declares 
them
+     * @param arguments the arguments of the call
+     */
+    public static <V> Resolved<V> resolve(String tool, List<Argument> 
declared, Map<String, V> arguments) {
+        List<String> names = declared.stream().map(Argument::name).toList();
+        // alias to declared name, for the aliases taken
+        Map<String, String> taken = new LinkedHashMap<>();
+        Map<String, String> renamed = new LinkedHashMap<>();
+        for (Map.Entry<String, String> alias : aliases(tool).entrySet()) {
+            String name = alias.getValue();
+            if (names.contains(name) && !names.contains(alias.getKey()) && 
!renamed.containsKey(name)
+                    && arguments.get(name) == null && 
arguments.get(alias.getKey()) != null) {
+                taken.put(alias.getKey(), name);
+                renamed.put(name, alias.getKey());
+            }
+        }
+        Map<String, V> resolved = new LinkedHashMap<>();
+        List<String> ignored = new ArrayList<>();
+        for (Map.Entry<String, V> e : arguments.entrySet()) {
+            String name = taken.getOrDefault(e.getKey(), e.getKey());
+            if (!names.contains(name)) {
+                ignored.add(name);
+            }
+            // a declared argument given as null is left out for the alias 
that replaces it
+            if (resolved.get(name) == null) {
+                resolved.put(name, e.getValue());
+            }
+        }
+        return new Resolved<>(tool, declared, List.copyOf(arguments.keySet()), 
resolved, renamed, ignored);
+    }
+
+    /**
+     * A JSON object answer with the note as its first field; any other 
answer, or no note, is returned as it is.
+     */
+    public static String withNote(String answer, String note) {
+        if (note == null || answer == null || 
!answer.stripLeading().startsWith("{")) {
+            return answer;
+        }
+        Object json;
+        try {
+            json = Jsoner.deserialize(answer);
+        } catch (DeserializationException e) {
+            return answer;
+        }
+        if (!(json instanceof JsonObject object) || 
object.containsKey(NOTE_KEY)) {
+            return answer;
+        }
+        JsonObject noted = new JsonObject();
+        noted.put(NOTE_KEY, note);
+        noted.putAll(object);
+        return noted.toJson();
+    }
+
+    private static Map<String, String> pairs(String... pairs) {
+        Map<String, String> map = new LinkedHashMap<>();
+        for (int i = 0; i + 1 < pairs.length; i += 2) {
+            map.put(pairs[i], pairs[i + 1]);
+        }
+        return map;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
index 3de5c15d6177..242fdbe6fe70 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskToolsTest.java
@@ -174,6 +174,24 @@ class AskToolsTest {
         assertThat(asked).hasSize(4);
     }
 
+    @Test
+    void anArgumentNamedAsAnotherToolNamesItRunsAndTheAnswerSaysTheName() {
+        // CAMEL-26399: models carry the argument names of one tool over to 
the next
+        AskTools tools = tools(true);
+        tools.context().setDefaultDirectory(dir);
+
+        String found = tools.executeTool("camel_catalog_find", new 
JsonObject(Map.of("query", "mqtt")));
+        assertThat(found).startsWith("{\"argumentNote\":\"term is the argument 
name (got query)\"")
+                .contains("\"term\":\"mqtt\"").doesNotContain("error");
+
+        String validated = tools.executeTool("camel_validate_source",
+                new JsonObject(Map.of("filename", "tick.camel.yaml", "source", 
ROUTE)));
+        assertThat(validated)
+                .contains("\"argumentNote\":\"file is the argument name (got 
filename); content is the argument name"
+                          + " (got source)\"")
+                .contains("\"valid\":true");
+    }
+
     @Test
     void theToolGroupsSayTheNamesOfTheSharedTools() {
         assertThat(AskTools.sharedToolNames(
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliasesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliasesTest.java
new file mode 100644
index 000000000000..3f1c45635188
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolArgumentAliasesTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.dsl.jbang.core.commands.ai;
+
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+/**
+ * CAMEL-26399: the argument names models carry over from another tool are 
taken for the names the tool declares, and a
+ * call that still misses one is answered with the arguments the tool takes.
+ */
+class ToolArgumentAliasesTest {
+
+    private static ToolArgumentAliases.Resolved<Object> resolve(String tool, 
Object... pairs) {
+        Map<String, Object> args = new LinkedHashMap<>();
+        for (int i = 0; i + 1 < pairs.length; i += 2) {
+            args.put((String) pairs[i], pairs[i + 1]);
+        }
+        return ToolArgumentAliases.resolve(tool, 
ToolArgumentAliases.arguments(ToolRegistry.findTool(tool)), args);
+    }
+
+    @Test
+    void queryAndNameAreTheTermOfCatalogFind() {
+        ToolArgumentAliases.Resolved<Object> query = 
resolve("camel_catalog_find", "query", "activemq classic 6 jms");
+        assertThat(query.arguments()).containsExactly(entry("term", "activemq 
classic 6 jms"));
+        assertThat(query.missing()).isEmpty();
+        assertThat(query.note()).isEqualTo("term is the argument name (got 
query)");
+
+        ToolArgumentAliases.Resolved<Object> name = 
resolve("camel_catalog_find", "name", "groovy", "kind", "language");
+        assertThat(name.arguments()).containsExactly(entry("term", "groovy"), 
entry("kind", "language"));
+        assertThat(name.note()).isEqualTo("term is the argument name (got 
name)");
+    }
+
+    @Test
+    void nameIsTheExampleOfExampleFile() {
+        // get_example_file is the registry tool behind 
camel_catalog_example_file, as camel ask calls it
+        ToolArgumentAliases.Resolved<Object> resolved = 
resolve("get_example_file", "name", "contracts/rest-code-first",
+                "file", "rest-code-first.camel.yaml");
+
+        assertThat(resolved.arguments()).containsExactly(entry("example", 
"contracts/rest-code-first"),
+                entry("file", "rest-code-first.camel.yaml"));
+        assertThat(resolved.missing()).isEmpty();
+        assertThat(resolved.note()).isEqualTo("example is the argument name 
(got name)");
+    }
+
+    @Test
+    void nameFilenameAndPathAreTheFileAndSourceTheContentOfValidateSource() {
+        assertThat(resolve("camel_validate_source", "name", 
"hr-api-call.yaml", "content", "- route:").arguments())
+                .containsExactly(entry("file", "hr-api-call.yaml"), 
entry("content", "- route:"));
+
+        ToolArgumentAliases.Resolved<Object> both = 
resolve("camel_validate_source", "filename", "routes.yaml",
+                "source", "- route:");
+        assertThat(both.arguments()).containsExactly(entry("file", 
"routes.yaml"), entry("content", "- route:"));
+        assertThat(both.note()).isEqualTo("file is the argument name (got 
filename); content is the argument name"
+                                          + " (got source)");
+
+        // an absolute path is only taken for the file: resolving it is the 
tool's business
+        assertThat(resolve("camel_validate_source", "path", 
"/workspace/camel/salesleads.yaml").arguments())
+                .containsExactly(entry("file", 
"/workspace/camel/salesleads.yaml"));
+    }
+
+    @Test
+    void theFirstAliasOfAnArgumentWinsAndTheOthersAreIgnored() {
+        // a path says more than a file name
+        ToolArgumentAliases.Resolved<Object> resolved = 
resolve("camel_validate_source",
+                "filename", "salesleads.yaml", "path", 
"/workspace/camel/salesleads.yaml");
+
+        assertThat(resolved.arguments()).containsEntry("file", 
"/workspace/camel/salesleads.yaml");
+        assertThat(resolved.renamed()).containsExactly(entry("file", "path"));
+        assertThat(resolved.ignored()).containsExactly("filename");
+        assertThat(resolved.note()).isEqualTo("file is the argument name (got 
path); ignored: filename;"
+                                              + " camel_validate_source takes 
directory, file (required), content,"
+                                              + " camelVersion");
+    }
+
+    @Test
+    void theDeclaredArgumentWinsOverItsAlias() {
+        ToolArgumentAliases.Resolved<Object> resolved = 
resolve("camel_catalog_find", "term", "mqtt", "query", "s3");
+
+        assertThat(resolved.arguments()).containsEntry("term", "mqtt");
+        assertThat(resolved.renamed()).isEmpty();
+        assertThat(resolved.ignored()).containsExactly("query");
+    }
+
+    @Test
+    void aNullArgumentIsNotGiven() {
+        Map<String, Object> args = new HashMap<>();
+        args.put("term", null);
+        args.put("query", "mqtt");
+
+        ToolArgumentAliases.Resolved<Object> resolved = 
ToolArgumentAliases.resolve("camel_catalog_find",
+                
ToolArgumentAliases.arguments(ToolRegistry.findTool("camel_catalog_find")), 
args);
+
+        assertThat(resolved.arguments()).containsExactly(entry("term", 
"mqtt"));
+        assertThat(resolved.missing()).isEmpty();
+    }
+
+    @Test
+    void anAliasThatIsAnArgumentOfTheToolIsLeftAlone() {
+        // camel_catalog_doc takes name: it has no alias, and its name is 
never taken for anything else
+        ToolArgumentAliases.Resolved<Object> doc = 
resolve("camel_catalog_doc", "name", "kafka");
+        assertThat(doc.arguments()).containsExactly(entry("name", "kafka"));
+        assertThat(doc.note()).isNull();
+
+        // a tool of the table that declares one of its aliases keeps it as 
its own argument
+        List<ToolArgumentAliases.Argument> declared = List.of(new 
ToolArgumentAliases.Argument("term", true),
+                new ToolArgumentAliases.Argument("name", false));
+        ToolArgumentAliases.Resolved<String> find = 
ToolArgumentAliases.resolve("camel_catalog_find", declared,
+                Map.of("name", "groovy"));
+        assertThat(find.arguments()).containsExactly(entry("name", "groovy"));
+        assertThat(find.missing()).containsExactly("term");
+    }
+
+    @Test
+    void theDeclaredNamesNeedNoNote() {
+        ToolArgumentAliases.Resolved<Object> resolved = 
resolve("camel_catalog_find", "term", "mqtt", "limit", 1);
+
+        assertThat(resolved.renamed()).isEmpty();
+        assertThat(resolved.ignored()).isEmpty();
+        assertThat(resolved.note()).isNull();
+    }
+
+    @Test
+    void anUnknownArgumentIsNamedWithTheArgumentsOfTheTool() {
+        ToolArgumentAliases.Resolved<Object> resolved = 
resolve("camel_catalog_find", "term", "mqtt", "limit", 1,
+                "bogus", "x");
+
+        assertThat(resolved.missing()).isEmpty();
+        assertThat(resolved.note())
+                .isEqualTo("ignored: bogus; camel_catalog_find takes term 
(required), kind, limit, camelVersion");
+    }
+
+    @Test
+    void aMissingArgumentIsAnsweredWithTheArgumentsOfTheTool() {
+        ToolArgumentAliases.Resolved<Object> unknown = 
resolve("camel_catalog_find", "q", "mqtt");
+        assertThat(unknown.missing()).containsExactly("term");
+        assertThat(unknown.missingMessage())
+                .isEqualTo("camel_catalog_find takes term (required), kind, 
limit, camelVersion; got q");
+
+        ToolArgumentAliases.Resolved<Object> none = 
resolve("get_example_file");
+        assertThat(none.missing()).containsExactly("example", "file");
+        assertThat(none.missingMessage()).isEqualTo("get_example_file takes 
example (required), file (required);"
+                                                    + " got no arguments");
+
+        // the names the call gave, aliases included
+        assertThat(resolve("get_example_file", "name", 
"timer-log").missingMessage())
+                .isEqualTo("get_example_file takes example (required), file 
(required); got name");
+    }
+
+    @Test
+    void theAliasesNameArgumentsOfTheRegistryTools() {
+        for (String tool : List.of("camel_catalog_find", "get_example_file", 
"camel_validate_source")) {
+            List<String> names = 
ToolArgumentAliases.arguments(ToolRegistry.findTool(tool)).stream()
+                    .map(ToolArgumentAliases.Argument::name).toList();
+            
assertThat(ToolArgumentAliases.aliases(tool)).as(tool).isNotEmpty().allSatisfy((alias,
 name) -> {
+                assertThat(names).as(tool + " declares " + 
name).contains(name);
+                assertThat(names).as(tool + " does not declare " + 
alias).doesNotContain(alias);
+            });
+        }
+    }
+
+    @Test
+    void theNoteIsTheFirstFieldOfAJsonObjectAnswer() throws Exception {
+        String answer = 
ToolArgumentAliases.withNote("{\"term\":\"mqtt\",\"matches\":[]}", "term is the 
argument name");
+
+        JsonObject json = (JsonObject) Jsoner.deserialize(answer);
+        
assertThat(json.keySet()).containsExactly(ToolArgumentAliases.NOTE_KEY, "term", 
"matches");
+        
assertThat(json.getString(ToolArgumentAliases.NOTE_KEY)).isEqualTo("term is the 
argument name");
+
+        // other answers are left as they are
+        assertThat(ToolArgumentAliases.withNote("[1, 2]", 
"note")).isEqualTo("[1, 2]");
+        assertThat(ToolArgumentAliases.withNote("No running Camel processes 
found", "note"))
+                .isEqualTo("No running Camel processes found");
+        assertThat(ToolArgumentAliases.withNote("{not json", 
"note")).isEqualTo("{not json");
+        assertThat(ToolArgumentAliases.withNote("{\"a\":1}", 
null)).isEqualTo("{\"a\":1}");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
index 5c8ca4127954..8cc9188fb665 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AdvisoryTools.java
@@ -24,6 +24,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.tooling.model.SecurityAdvisoryModel;
 
 /**
@@ -40,6 +41,7 @@ public class AdvisoryTools {
     @Inject
     AdvisoryService advisoryService;
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List published Apache Camel CVE security advisories 
(the data behind "
                         + "https://camel.apache.org/security/), optionally 
filtered by Camel version, component and "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
index 807733a151e0..85c42713a7e6 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiPipelineScaffoldTools.java
@@ -21,6 +21,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 
 /**
  * MCP Tool for generating AI document-processing pipeline scaffolds.
@@ -35,6 +36,7 @@ public class AiPipelineScaffoldTools {
     private static final String DEFAULT_MODEL = 
"anthropic.claude-sonnet-4-20250514-v1:0";
     private static final String DEFAULT_REGION = "us-east-1";
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Generate a YAML DSL Camel route for an AI 
document-processing pipeline. "
                         + "Combines document processors (Docling for 
open-source/on-prem, Textract for AWS-managed) "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
index 968cc390b7b6..f641d53548d1 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AiTraceTools.java
@@ -27,6 +27,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
@@ -64,6 +65,7 @@ public class AiTraceTools {
     @Inject
     RuntimeService runtimeService;
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Trace AI-specific exchange flow in a running Camel 
application. "
                         + "Shows token usage, model IDs, guardrail outcomes, 
completion reasons, "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
index e6c637197059..6a5b5bd3fa8e 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java
@@ -28,6 +28,7 @@ import io.quarkiverse.mcp.server.MetaField;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
@@ -67,6 +68,7 @@ public class AuthoringTools {
     @Inject
     CatalogService catalogService;
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Camel catalog documentation of a component, data 
format, language, EIP, Kamelet, built-in bean or the "
                         + "Java API (description, options, Maven coordinates), 
with the URI rules of a component. For "
@@ -105,6 +107,7 @@ public class AuthoringTools {
                 "optionsFilter", optionsFilter, "limit", limit, 
"camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Finds Camel components, data formats, languages, EIPs 
and Kamelets by a protocol, product, alias or "
                         + "other term that is not the exact name (mqtt, s3, 
snowflake, csv, fan-out, dedup): best "
@@ -121,6 +124,7 @@ public class AuthoringTools {
                 "camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "A validated YAML DSL sample of an EIP or file entry 
(onException, aggregate, split, rest, beans), "
                         + "a component (kafka, file), a data format (csv) or a 
language (jq) from the docs, with where "
@@ -141,6 +145,7 @@ public class AuthoringTools {
                 args("name", name, "kind", kind, "limit", limit, 
"camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Validates Camel YAML/Java/XML DSL or .properties 
source without writing: schema (misspelled options "
                         + "such as logLevel instead of loggingLevel), endpoint 
URIs, simple expressions, camel.* "
@@ -160,6 +165,7 @@ public class AuthoringTools {
                 "content", content, "camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "The source files of a project directory, 
subdirectories included: without file the list, "
                         + "with the route and configuration files named first 
(routeFiles, configFiles) and, for a "
@@ -173,6 +179,7 @@ public class AuthoringTools {
         return call("camel_get_files", args("directory", directory, "file", 
file));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = "Writes the complete content of a file in the project 
directory. YAML and .properties "
                         + "content is validated first; invalid content is not 
written and the errors are returned. "
@@ -189,6 +196,7 @@ public class AuthoringTools {
                 "camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = "Changes a file by replacing a snippet: the exact text 
to find (it must occur once) and "
                         + "what to put there. Validated and reloaded as a 
write is. Use it to change an existing file, "
@@ -259,6 +267,7 @@ public class AuthoringTools {
         return list.toJson();
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = true),
           description = "Starts an integration with camel run in a separate 
process, in dev mode by default (files reload when written). Returns the pid 
and log file; camel_get_log, camel_get_errors and camel_control follow it.")
     public JsonObject camel_run(
@@ -271,6 +280,7 @@ public class AuthoringTools {
         return call("camel_run", args("directory", directory, "files", files, 
"name", name, "dev", dev));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = "Controls a running integration: stop (graceful), 
kill, restart (picks up edited files "
                         + "without dev mode), stop-routes, start-routes, 
reset-stats (clears statistics, routes "
@@ -282,6 +292,7 @@ public class AuthoringTools {
         return call("camel_control", args("action", action, "name", name));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Recent log records of a running integration, newest 
first, with optional filtering; stack "
                         + "traces only with details. An integration that is 
not running gets the log of its previous "
@@ -298,6 +309,7 @@ public class AuthoringTools {
                 "details", details, "previous", previous));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "The failed exchanges of a running integration: 
routeId, exchangeId, exception with stack "
                         + "trace, body and headers. For an integration that is 
not running, it says how its previous "
@@ -307,6 +319,7 @@ public class AuthoringTools {
         return call("camel_get_errors", args("name", name));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Evaluates an expression: in the running integration 
when there is one, else locally, in any "
                         + "language (jsonpath, jq, xpath, groovy: its 
component is downloaded when needed). Returns "
@@ -321,6 +334,7 @@ public class AuthoringTools {
                 "name", name));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = true),
           description = "Which Maven dependency provides a class, and how to 
declare it: the known dependencies "
                         + "camel run downloads by itself (nothing to declare 
there), a Camel component's artifact per "
@@ -340,6 +354,7 @@ public class AuthoringTools {
                 mavenCentral, "camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "A high-level overview of a project's integrations 
from its route sources (no running "
                         + "integration needed): routes, entry points, how 
routes connect (call, hand-off, event), "
@@ -353,6 +368,7 @@ public class AuthoringTools {
         return call("camel_project_overview", args("directory", directory, 
"camelVersion", camelVersion));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = "Saves what you wrote about a project into its 
camel-summary.md, marked as "
                         + "AI-assisted, beside the facts derived from the 
sources, so people and tools can tell them "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
index 216fcfb5b7ce..a7a5788186cf 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java
@@ -25,6 +25,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 
 /**
@@ -41,6 +42,7 @@ public class CatalogTools {
     /**
      * Tool to list available AsciiDoc documentation pages.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List available AsciiDoc documentation page names from 
the catalog. "
                         + "Returns page names (without .adoc extension), e.g. 
kafka-component or split-eip; "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
index f9d6075c5394..fb774f3101d5 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java
@@ -30,6 +30,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
 
@@ -48,6 +49,7 @@ public class ConfigurationValidateTools {
     /**
      * Tool to validate one or more Camel configuration property lines.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Validate one or more Camel configuration property 
lines (e.g. lines from application.properties). "
                         +
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
index 01e07e330fac..124280a6efa4 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencyCheckTools.java
@@ -28,6 +28,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.DependencyLookup;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
@@ -52,6 +53,7 @@ public class DependencyCheckTools {
     /**
      * Tool to check Camel dependency hygiene for a project.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Check Camel project dependency hygiene. Given a 
pom.xml (and optionally route definitions), "
                         + "detects an outdated Camel version compared to the 
latest Camel release, "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
index 1353b38ce5db..c40fb1e2a465 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DependencySecurityAuditTools.java
@@ -29,6 +29,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.DependencyLookup;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
@@ -52,6 +53,7 @@ public class DependencySecurityAuditTools {
     @Inject
     AdvisoryService advisoryService;
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Perform a security vulnerability audit on a Camel 
project's dependencies. "
                         + "Cross-references the project's pom.xml dependencies 
(Spring Boot starters and Camel Quarkus "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
index d1a94ae8c96c..0d6a7ac4a836 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/DiagnoseTools.java
@@ -26,6 +26,7 @@ import io.quarkiverse.mcp.server.MetaField;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.ErrorDiagnoser;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
@@ -51,6 +52,7 @@ public class DiagnoseTools {
      * Tool to diagnose Camel errors from stack traces or error messages: a 
thin wrapper over the shared
      * {@code camel_error_diagnose} tool of the {@link ToolRegistry} 
(CAMEL-24695), so the TUI answers the same.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Diagnose a Camel error from a stack trace or error 
message. "
                         + "Returns the identified component/EIP involved, 
common causes for the error, "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
index 2b5c0b735456..f39e3fad1e99 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExampleTools.java
@@ -20,6 +20,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
 import org.apache.camel.util.json.JsonObject;
 
@@ -35,6 +36,7 @@ import static 
org.apache.camel.dsl.jbang.core.commands.mcp.AuthoringTools.call;
 @ApplicationScoped
 public class ExampleTools {
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List the Camel CLI examples, grouped as the ladder of 
the examples: "
                         + "quick-start, run, transform, route, fail-well, 
connect, connect-service, contracts, ai, "
@@ -54,6 +56,7 @@ public class ExampleTools {
         return call("list_examples", args("filter", filter, "level", level, 
"limit", limit));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get the content of a specific file from a Camel CLI 
example. "
                         + "Use camel_catalog_examples first to find the 
example name and its files. "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
index 87971f87e75d..b8a7e71f7b39 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ExplainTools.java
@@ -25,6 +25,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
@@ -46,6 +47,7 @@ public class ExplainTools {
     /**
      * Tool to get enriched context for a Camel route.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get enriched context for a Camel route including 
documentation for all components and EIPs used. "
                         +
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
index 9c6eba1bbe9a..de9562822c86 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/HardenTools.java
@@ -29,6 +29,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.ai.RouteComponents;
 import org.apache.camel.tooling.model.ComponentModel;
@@ -59,6 +60,7 @@ public class HardenTools {
     /**
      * Tool to get security hardening context for a Camel route.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get security hardening analysis context for a Camel 
route. " +
                         "Returns security-sensitive components, potential 
vulnerabilities, " +
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
index a3ed06b2630d..ced067c900e4 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/KameletTools.java
@@ -28,6 +28,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.ai.CatalogDocs;
 import org.apache.camel.dsl.jbang.core.commands.ai.KameletDefinitions;
 import org.apache.camel.dsl.jbang.core.commands.catalog.KameletCatalogHelper;
@@ -69,6 +70,7 @@ public class KameletTools {
     /**
      * Tool to list available Kamelets.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List available Camel Kamelets from the Kamelet 
Catalog. " +
                         "Returns kamelet name, type (source, sink, action), 
support level, and description. " +
@@ -129,6 +131,7 @@ public class KameletTools {
     /**
      * Tool to get detailed documentation for a specific Kamelet.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get detailed documentation for a specific Camel 
Kamelet including all properties/options, "
                         + "dependencies, and usage information.")
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrail.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrail.java
new file mode 100644
index 000000000000..515c43e1bbdd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrail.java
@@ -0,0 +1,140 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.util.List;
+
+import jakarta.enterprise.context.RequestScoped;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+import io.quarkiverse.mcp.server.Content;
+import io.quarkiverse.mcp.server.TextContent;
+import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
+import io.quarkiverse.mcp.server.ToolInputGuardrail;
+import io.quarkiverse.mcp.server.ToolManager.ToolInfo;
+import io.quarkiverse.mcp.server.ToolOutputGuardrail;
+import io.quarkiverse.mcp.server.ToolResponse;
+import io.vertx.core.json.DecodeException;
+import io.vertx.core.json.JsonObject;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolArgumentAliases;
+
+/**
+ * The argument names of a tool call (CAMEL-26399), declared with {@link 
ToolGuardrails} on every tool of the server.
+ * Before the server checks the arguments, the names models carry over from 
another tool are taken for the declared ones
+ * ({@link ToolArgumentAliases}: {@code query} for the {@code term} of {@code 
camel_catalog_find}), and a call that
+ * still misses a required argument is answered with the arguments of the tool 
and the ones it got, where the server
+ * would only say {@code Missing required argument: term}. After the call, a 
JSON object answer gets a note with the
+ * declared name of each alias and the arguments the tool ignored, so the next 
call uses the right names. Other answers,
+ * errors included, are left as they are.
+ * <p>
+ * The note goes from the input to the output through a {@link RequestScoped} 
bean: the server runs each tool call in a
+ * request context of its own. Nothing here blocks, so the guardrail supports 
every execution model (all tools of the
+ * server run on a worker thread).
+ */
+@Singleton
+public class McpArgumentGuardrail implements ToolInputGuardrail, 
ToolOutputGuardrail {
+
+    @Inject
+    CallNote callNote;
+
+    @Override
+    public void apply(ToolInputContext context) {
+        ToolInfo tool = context.getTool();
+        JsonObject arguments = context.getArguments();
+        ToolArgumentAliases.Resolved<Object> resolved
+                = ToolArgumentAliases.resolve(tool.name(), declared(tool), 
arguments.getMap());
+        if (!resolved.missing().isEmpty()) {
+            throw new ToolCallException(resolved.missingMessage());
+        }
+        if (!resolved.renamed().isEmpty()) {
+            context.setArguments(new JsonObject(resolved.arguments()));
+        }
+        callNote.set(resolved.note());
+    }
+
+    @Override
+    public void apply(ToolOutputContext context) {
+        String note = callNote.take();
+        if (note != null) {
+            ToolResponse noted = withNote(context.getResponse(), note);
+            if (noted != null) {
+                context.setResponse(noted);
+            }
+        }
+    }
+
+    /**
+     * The arguments of the tool as the server checks them: an argument with a 
default value is never missing.
+     */
+    static List<ToolArgumentAliases.Argument> declared(ToolInfo tool) {
+        return tool.arguments().stream()
+                .map(a -> new ToolArgumentAliases.Argument(a.name(), 
a.required() && a.defaultValue() == null))
+                .toList();
+    }
+
+    /**
+     * The response with the note as the first field of its JSON object, or 
null when it is not a successful answer of
+     * one JSON object.
+     */
+    static ToolResponse withNote(ToolResponse response, String note) {
+        if (response.isError() || response.structuredContent() != null || 
response.content() == null
+                || response.content().size() != 1) {
+            return null;
+        }
+        Content content = response.content().get(0);
+        if (!(content instanceof TextContent text) || 
!text.text().stripLeading().startsWith("{")) {
+            return null;
+        }
+        JsonObject answer;
+        try {
+            answer = new JsonObject(text.text());
+        } catch (DecodeException e) {
+            return null;
+        }
+        if (answer.containsKey(ToolArgumentAliases.NOTE_KEY)) {
+            return null;
+        }
+        JsonObject noted = new JsonObject().put(ToolArgumentAliases.NOTE_KEY, 
note);
+        answer.forEach(e -> noted.put(e.getKey(), e.getValue()));
+        return new ToolResponse(
+                false, List.of(new TextContent(noted.encode(), text._meta(), 
text.annotations())),
+                null, response._meta());
+    }
+
+    /**
+     * The note of the current tool call, from its arguments to its answer. 
Read through its methods: a field of a
+     * request scoped bean is not reached through the client proxy.
+     */
+    @RequestScoped
+    public static class CallNote {
+
+        private String note;
+
+        public void set(String note) {
+            this.note = note;
+        }
+
+        /** The note, which is then cleared. */
+        public String take() {
+            String taken = note;
+            note = null;
+            return taken;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
index b0febb99df8b..bf384e21c591 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationTools.java
@@ -26,6 +26,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.tooling.model.ReleaseModel;
 
 /**
@@ -56,6 +57,7 @@ public class MigrationTools {
     /**
      * Step 1: Analyze a project's pom.xml to detect runtime, Camel version, 
Java version, and components.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Analyze a Camel project's pom.xml to detect the 
runtime type (main, spring-boot, quarkus, "
                         + "wildfly, karaf), Camel version, Java version, and 
Camel component dependencies. "
@@ -114,6 +116,7 @@ public class MigrationTools {
     /**
      * Step 2: Check compatibility and provide relevant migration guide 
references.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Check whether a migration between two Camel versions 
is possible on the project's Java version "
                         + "(the JDK versions each Camel release supports, as 
camel_version_list reports them) and "
@@ -199,6 +202,7 @@ public class MigrationTools {
     /**
      * Step 3: Get Maven commands to run OpenRewrite migration recipes.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get Maven commands to run Camel OpenRewrite migration 
recipes for upgrading between versions. "
                         + "Returns the exact Maven commands to execute on the 
project: camel-upgrade-recipes for "
@@ -309,6 +313,7 @@ public class MigrationTools {
     /**
      * Search migration guides for a specific term, optionally scoped to the 
versions being migrated between.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Search Camel migration and upgrade guides for a 
specific term or component name. "
                         + "Each result is one topic entry of a release (e.g. 
'Upgrading Camel 4.20 to 4.21 / camel-cxf') "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
index 3d1c0fd29b08..dd6e55eee546 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/MigrationWildflyKarafTools.java
@@ -26,6 +26,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 
 /**
  * MCP Tool for migrating Camel projects from WildFly or Karaf to modern 
runtimes.
@@ -43,6 +44,7 @@ public class MigrationWildflyKarafTools {
     /**
      * WildFly/Karaf migration guidance with archetype-based project creation.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get migration guidance for Camel projects running on 
WildFly, Karaf, or WAR-based "
                         + "application servers. Returns the Maven archetype 
command to create a new target project, "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
index a6d5a2d5694f..47b15d725045 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/OpenApiTools.java
@@ -29,6 +29,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.Operation;
 import io.swagger.v3.oas.models.PathItem;
@@ -67,6 +68,7 @@ public class OpenApiTools {
               + " against it. Secure the HTTP layer (for a Bearer token, the 
rest-openapi oauthProfile option)"
               + " or check the credential in the route.";
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Validate an OpenAPI specification for use with 
Camel's contract-first REST support. "
                         + "Reports operations without operationId (with the id 
Camel routes them to), "
@@ -143,6 +145,7 @@ public class OpenApiTools {
         return new ValidateResult(valid, errors, warnings, info, 
operationCount);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Generate Camel YAML scaffold for contract-first 
OpenAPI integration. "
                         + "Produces a rest:openApi configuration block and a 
direct:<operationId> route stub for "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
index 675f90eb86bd..2efc52689ff3 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
@@ -28,6 +28,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 
 /**
  * MCP Tool for translating Camel configuration properties between Camel 
runtimes (camel-main / jbang standalone,
@@ -202,6 +203,7 @@ public class PropertiesTranslateTools {
     /**
      * Tool to translate Camel configuration property lines between Camel 
runtimes.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Translate Camel configuration property lines (e.g. 
lines from application.properties) between "
                         +
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteCostEstimateTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteCostEstimateTools.java
index f74e01822ed4..2d4000a8ea0a 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteCostEstimateTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteCostEstimateTools.java
@@ -30,6 +30,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 
 /**
  * MCP Tool for estimating API costs of a Camel route based on its component 
usage.
@@ -55,6 +56,7 @@ public class RouteCostEstimateTools {
 
     private static final Map<String, ComponentCostProfile> COST_PROFILES = 
buildCostProfiles();
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Estimate API costs for a Camel route based on its 
component usage. "
                         + "Analyzes a YAML or XML route definition and 
identifies components with "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index 4207f1d6eb9b..23eaf07b9337 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -26,6 +26,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteDiagramAction;
 import org.apache.camel.dsl.jbang.core.common.CliException;
@@ -41,6 +42,7 @@ public class RouteDiagramTools {
 
     private static final Set<String> TEXT_THEMES = Set.of("ascii", "unicode");
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = "Generate a diagram of Camel routes from a route 
source file (YAML, XML, Java, ...). "
                         + "Java routes are read by the Java DSL parser, 
without compiling them; a step it cannot "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
index 55017b894a70..55397fa7fb88 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java
@@ -34,6 +34,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
 import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
@@ -126,6 +127,7 @@ public class RuntimeTools {
 
     // ---- Process discovery (MCP-specific, not in ToolRegistry) ----
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   List all running Camel processes that can be inspected. \
@@ -136,6 +138,7 @@ public class RuntimeTools {
 
     // ---- Read-only status tools ----
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get Camel context information: name, version, state, 
uptime, route count, exchange statistics.")
     public JsonObject camel_runtime_context(
@@ -143,6 +146,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_context", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List Camel routes with their state, uptime, messages 
processed, last error, and throughput statistics."
                         + " A route with mixedBodyKinds has steps reached with 
bodies of more than one kind, which need"
@@ -155,6 +159,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_routes", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get health check status for the Camel application.")
     public JsonObject camel_runtime_health(
@@ -162,6 +167,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_health", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List all endpoints registered in the Camel context 
with their URIs and usage statistics.")
     public JsonObject camel_runtime_endpoints(
@@ -169,6 +175,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_endpoints", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show currently in-flight exchanges (messages being 
processed).")
     public JsonObject camel_runtime_inflight(
@@ -176,6 +183,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_inflight", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show blocked exchanges that are stuck or waiting.")
     public JsonObject camel_runtime_blocked(
@@ -183,6 +191,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_blocked", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show exchange variables in the Camel context.")
     public JsonObject camel_runtime_variables(
@@ -190,6 +199,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_variables", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show consumer statistics (polling consumers, 
event-driven consumers).")
     public JsonObject camel_runtime_consumers(
@@ -197,6 +207,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_consumers", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show configuration properties of the running Camel 
application.")
     public JsonObject camel_runtime_properties(
@@ -204,6 +215,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_properties", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show services registered in the Camel service 
registry.")
     public JsonObject camel_runtime_services(
@@ -211,6 +223,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_services", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show JVM memory usage (heap/non-heap), garbage 
collection stats, and thread counts.")
     public JsonObject camel_runtime_memory(
@@ -220,6 +233,7 @@ public class RuntimeTools {
 
     // ---- Interactive action tools ----
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get the source code of routes in the running Camel 
application.")
     public JsonObject camel_runtime_route_source(
@@ -229,6 +243,7 @@ public class RuntimeTools {
                 Map.of("filter", filter != null ? filter : "*"));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Dump route definitions in XML, YAML, or Java DSL 
format.")
     public JsonObject camel_runtime_route_dump(
@@ -242,6 +257,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_route_dump", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show the route structure as a tree of processors. 
With the message history on (camel run --dev),"
                         + " a processor has bodyTypes: the body types it has 
been reached with, across the messages, and"
@@ -268,6 +284,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_route_structure", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Show configured options for all processors in a route. \
@@ -289,6 +306,7 @@ public class RuntimeTools {
 
     // one tool for the four commands, and the hint is per tool: destructive 
because stop and suspend take the route's
     // consumer offline (the shared registry, with one tool per command, marks 
only stop_route destructive)
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = "Control a route: start, stop, suspend, or resume it.")
     public JsonObject camel_runtime_route_control(
@@ -315,6 +333,7 @@ public class RuntimeTools {
 
     // ---- Data, resilience, metrics and analysis (CAMEL-24867: the rest of 
the shared runtime registry) ----
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = """
                   Execute a SQL statement against a DataSource of the running 
Camel application. \
@@ -336,6 +355,7 @@ public class RuntimeTools {
         return delegateToRegistry("execute_sql", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Which runtime tool groups the Camel application needs, from 
what it has: sql (datasources, \
@@ -347,6 +367,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_tool_groups", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   List the HTTP endpoints the running Camel application serves 
(Rest DSL and platform-http): \
@@ -363,6 +384,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_http_endpoints", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = """
                   Send an HTTP request to the running Camel application's own 
server (localhost and its port) and \
@@ -394,6 +416,7 @@ public class RuntimeTools {
         return delegateToRegistry("http_request", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the datasources of the running Camel application with 
their connection pool status: \
@@ -403,6 +426,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_datasources", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the traced SQL executions of the camel-sql and 
camel-jdbc components: per statement timing, \
@@ -412,6 +436,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_sql_trace", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the circuit breakers of the running Camel application: 
state (CLOSED/OPEN/HALF_OPEN), \
@@ -421,6 +446,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_circuit_breakers", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the Micrometer metrics of the running Camel application: 
counters, gauges, timers and \
@@ -430,6 +456,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_metrics", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Aggregate EIP usage across all routes: which processors are 
used, how often, and their \
@@ -439,6 +466,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_eip_stats", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the OpenTelemetry spans of the running Camel 
application: trace IDs, span names, durations, \
@@ -451,6 +479,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_spans", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the startup recorder steps of the running Camel 
application: each step with duration, level \
@@ -460,6 +489,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_startup_steps", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Analyze the routes of the running Camel application for 
anti-patterns and structural issues: \
@@ -473,6 +503,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_route_analysis", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Compare the running route definitions with the original 
source files, to detect configuration \
@@ -491,6 +522,7 @@ public class RuntimeTools {
         }
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = "Send a test message to a Camel endpoint in the 
running application.")
     public JsonObject camel_runtime_send(
@@ -513,6 +545,7 @@ public class RuntimeTools {
         return delegateToRegistry("send_message", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = """
                   Enable, disable, or dump message tracing for the running 
Camel application. \
@@ -526,6 +559,7 @@ public class RuntimeTools {
         return delegateToRegistry("trace_control", nameOrPid, Map.of("action", 
action));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Dump the messages traced in the running Camel application 
(tracing must be enabled, \
@@ -535,6 +569,7 @@ public class RuntimeTools {
         return delegateToRegistry("trace_control", nameOrPid, Map.of("action", 
"dump"));
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Show top processor statistics: which processors are 
slowest and most active.")
     public JsonObject camel_runtime_top(
@@ -542,6 +577,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_top_processors", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the inter-route topology showing how routes connect to 
each other \
@@ -558,6 +594,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_route_topology", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Get a JVM thread dump showing thread names, states, 
and stack traces.")
     public JsonObject camel_runtime_thread_dump(
@@ -565,6 +602,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_thread_dump", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get a class-level heap histogram showing instance counts and 
byte usage per class. \
@@ -574,6 +612,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_heap_histogram", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = """
                   Write a heap dump (.hprof) file from a running Camel 
integration for deep memory analysis \
@@ -591,6 +630,7 @@ public class RuntimeTools {
         return delegateToRegistry("write_heap_dump", nameOrPid, args);
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = false, openWorldHint = false),
           description = """
                   Diagnose memory leaks in a running Camel integration using 
Java Flight Recorder (JFR). \
@@ -765,6 +805,7 @@ public class RuntimeTools {
         }
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = """
                   Get the message history trace of the last completed 
exchange, with its route path, processors \
@@ -776,6 +817,7 @@ public class RuntimeTools {
         return delegateToRegistry("get_history", nameOrPid, Map.of());
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = false, 
destructiveHint = true, openWorldHint = false),
           description = """
                   Receive (consume) a message from an endpoint of the running 
application, the complement of \
@@ -1002,6 +1044,7 @@ public class RuntimeTools {
         return e.getMessage() != null ? e.getMessage() : e.toString();
     }
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Browse messages in a Camel endpoint (e.g., browse 
messages queued in a SEDA endpoint).")
     public JsonObject camel_runtime_browse(
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
index 5c0d0421ec76..0e06b7748146 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/SecurityScanTools.java
@@ -28,6 +28,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.util.SecurityUtils;
 
 /**
@@ -54,6 +55,7 @@ public class SecurityScanTools {
             "cxf", "cxfrs", "rest", "coap", "grpc"
     };
 
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Scan a Camel route for security anti-patterns. "
                         + "Performs static analysis to detect: exposed secrets 
in URIs, "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
index b140bf47e319..8eaa7040a5c5 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TestScaffoldTools.java
@@ -29,6 +29,7 @@ import jakarta.inject.Inject;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.tooling.model.ComponentModel;
 import org.apache.camel.util.json.JsonArray;
@@ -73,6 +74,7 @@ public class TestScaffoldTools {
     /**
      * Tool to generate a JUnit 5 test skeleton for a Camel route.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Generate a JUnit 5 test skeleton for a Camel route. "
                         + "Given a YAML or XML route definition, produces a 
test class with "
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
index e7c64364d8e8..5df42f23a8d9 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
@@ -27,6 +27,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.RouteDslConverter;
 
 /**
@@ -42,6 +43,7 @@ public class TransformTools {
     /**
      * Tool to transform routes between DSL formats.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "Transform Camel routes between the YAML, XML and Java 
DSLs, without running them."
                         + " Routes, rests, route templates, route 
configurations and beans are converted."
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
index a564f5a26af9..bf86c233811b 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/VersionTools.java
@@ -24,6 +24,7 @@ import jakarta.enterprise.context.ApplicationScoped;
 import io.quarkiverse.mcp.server.Tool;
 import io.quarkiverse.mcp.server.ToolArg;
 import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.version.VersionList;
 import org.apache.camel.dsl.jbang.core.common.RuntimeType;
@@ -42,6 +43,7 @@ public class VersionTools {
     /**
      * Tool to list available Camel versions for a specific runtime.
      */
+    @ToolGuardrails(input = McpArgumentGuardrail.class, output = 
McpArgumentGuardrail.class)
     @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
           description = "List available Camel versions for a specific runtime 
(main, spring-boot, quarkus). " +
                         "Returns version information including release date, 
JDK requirements, and LTS status.")
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrailTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrailTest.java
new file mode 100644
index 000000000000..adb3985df530
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpArgumentGuardrailTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.dsl.jbang.core.commands.mcp;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import io.quarkiverse.mcp.server.McpConnection;
+import io.quarkiverse.mcp.server.Meta;
+import io.quarkiverse.mcp.server.RequestId;
+import io.quarkiverse.mcp.server.TextContent;
+import io.quarkiverse.mcp.server.ToolCallException;
+import io.quarkiverse.mcp.server.ToolGuardrails;
+import io.quarkiverse.mcp.server.ToolInputGuardrail.ToolInputContext;
+import io.quarkiverse.mcp.server.ToolManager.ToolArgument;
+import io.quarkiverse.mcp.server.ToolManager.ToolInfo;
+import io.quarkiverse.mcp.server.ToolOutputGuardrail.ToolOutputContext;
+import io.quarkiverse.mcp.server.ToolResponse;
+import io.vertx.core.json.JsonObject;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolArgumentAliases;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * CAMEL-26399: the guardrail every tool declares takes the argument names 
models carry over from another tool for the
+ * declared ones before the server checks them, answers a call that still 
misses a required argument with the arguments
+ * of the tool, and notes the declared names in a JSON object answer. The 
tools are described from their real
+ * declarations, as the server describes them.
+ */
+class McpArgumentGuardrailTest {
+
+    private final McpArgumentGuardrail guardrail = guardrail();
+
+    private static McpArgumentGuardrail guardrail() {
+        McpArgumentGuardrail guardrail = new McpArgumentGuardrail();
+        guardrail.callNote = new McpArgumentGuardrail.CallNote();
+        return guardrail;
+    }
+
+    @Test
+    void everyToolDeclaresTheGuardrail() throws Exception {
+        List<Method> tools = McpToolClasses.toolMethods();
+
+        // guard against a scan that finds nothing and makes the check below 
vacuous
+        assertThat(tools).hasSizeGreaterThan(80);
+        assertThat(tools)
+                .filteredOn(m -> !declaresGuardrail(m))
+                .as("tools without @ToolGuardrails(input = 
McpArgumentGuardrail.class, output = McpArgumentGuardrail.class)")
+                .extracting(m -> m.getDeclaringClass().getSimpleName() + "#" + 
m.getName())
+                .isEmpty();
+    }
+
+    private static boolean declaresGuardrail(Method method) {
+        ToolGuardrails guardrails = method.getAnnotation(ToolGuardrails.class);
+        return guardrails != null && 
Arrays.asList(guardrails.input()).contains(McpArgumentGuardrail.class)
+                && 
Arrays.asList(guardrails.output()).contains(McpArgumentGuardrail.class);
+    }
+
+    @Test
+    void theAliasesNameArgumentsOfTheToolsWhichStayRequired() throws Exception 
{
+        for (String tool : List.of("camel_catalog_find", 
"camel_catalog_example_file", "camel_validate_source")) {
+            List<ToolArgument> arguments = 
McpToolClasses.toolInfo(McpToolClasses.toolMethod(tool)).arguments();
+            List<String> names = 
arguments.stream().map(ToolArgument::name).toList();
+            
assertThat(ToolArgumentAliases.aliases(tool)).as(tool).isNotEmpty().allSatisfy((alias,
 name) -> {
+                assertThat(names).as(tool + " declares " + 
name).contains(name);
+                assertThat(names).as(tool + " does not declare " + 
alias).doesNotContain(alias);
+            });
+        }
+        // the schemas the clients get still say which argument a call needs
+        assertThat(required("camel_catalog_find")).containsExactly("term");
+        
assertThat(required("camel_catalog_example_file")).containsExactly("example", 
"file");
+        assertThat(required("camel_validate_source")).containsExactly("file");
+    }
+
+    private static List<String> required(String tool) throws Exception {
+        return 
McpToolClasses.toolInfo(McpToolClasses.toolMethod(tool)).arguments().stream()
+                
.filter(ToolArgument::required).map(ToolArgument::name).toList();
+    }
+
+    @Test
+    void theCallsOfTheIssueRunWithTheDeclaredNames() throws Exception {
+        assertThat(input("camel_catalog_find", "query", "activemq classic 6 
jms").getArguments().getMap())
+                .containsExactly(Map.entry("term", "activemq classic 6 jms"));
+        assertThat(input("camel_catalog_find", "name", 
"groovy").getArguments().getMap())
+                .containsExactly(Map.entry("term", "groovy"));
+        assertThat(input("camel_catalog_example_file", "name", 
"contracts/rest-code-first",
+                "file", "rest-code-first.camel.yaml").getArguments().getMap())
+                .containsExactly(Map.entry("example", 
"contracts/rest-code-first"),
+                        Map.entry("file", "rest-code-first.camel.yaml"));
+        assertThat(input("camel_validate_source", "name", "hr-api-call.yaml", 
"content", "- route:")
+                .getArguments().getMap())
+                .containsExactly(Map.entry("file", "hr-api-call.yaml"), 
Map.entry("content", "- route:"));
+        assertThat(input("camel_validate_source", "filename", "routes.yaml", 
"source", "- route:")
+                .getArguments().getMap())
+                .containsExactly(Map.entry("file", "routes.yaml"), 
Map.entry("content", "- route:"));
+        // the path wins over the file name, which is ignored; resolving an 
absolute path is the tool's business
+        assertThat(input("camel_validate_source", "path", 
"/workspace/camel/salesleads.yaml",
+                "filename", "salesleads.yaml").getArguments().getMap())
+                .containsEntry("file", "/workspace/camel/salesleads.yaml");
+    }
+
+    @Test
+    void theDeclaredArgumentWinsAndAToolOwnNameIsLeftAlone() throws Exception {
+        assertThat(input("camel_catalog_find", "term", "mqtt", "query", 
"s3").getArguments().getMap())
+                .containsEntry("term", "mqtt").containsEntry("query", "s3");
+        // camel_catalog_doc and camel_catalog_sample take name themselves
+        assertThat(input("camel_catalog_doc", "name", 
"kafka").getArguments().getMap())
+                .containsExactly(Map.entry("name", "kafka"));
+        assertThat(input("camel_catalog_sample", "name", 
"split").getArguments().getMap())
+                .containsExactly(Map.entry("name", "split"));
+        assertThat(guardrail.callNote.take()).isNull();
+    }
+
+    @Test
+    void aMissingArgumentIsAnsweredWithTheArgumentsOfTheTool() {
+        assertThatThrownBy(() -> input("camel_catalog_find", "q", "mqtt"))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessage("camel_catalog_find takes term (required), kind, 
limit, camelVersion; got q");
+        assertThatThrownBy(() -> input("camel_catalog_example_file", "name", 
"timer-log"))
+                .hasMessage("camel_catalog_example_file takes example 
(required), file (required); got name");
+        // any tool, not only the ones with aliases
+        assertThatThrownBy(() -> input("camel_write_file", "file", 
"routes.yaml"))
+                .hasMessage("camel_write_file takes directory, file 
(required), content (required), camelVersion;"
+                            + " got file");
+        assertThatThrownBy(() -> input("camel_catalog_sample"))
+                .hasMessage("camel_catalog_sample takes name (required), kind, 
limit, camelVersion; got no arguments");
+    }
+
+    @Test
+    void theAnswerOfAnAliasedCallSaysTheDeclaredNames() throws Exception {
+        input("camel_catalog_find", "query", "mqtt");
+
+        ToolResponse response = 
output(ToolResponse.success("{\"term\":\"mqtt\",\"matches\":[{\"name\":\"paho\"}]}"));
+
+        JsonObject answer = new 
JsonObject(response.firstContent().asText().text());
+        
assertThat(answer.fieldNames()).containsExactly(ToolArgumentAliases.NOTE_KEY, 
"term", "matches");
+        
assertThat(answer.getString(ToolArgumentAliases.NOTE_KEY)).isEqualTo("term is 
the argument name (got query)");
+        
assertThat(answer.getJsonArray("matches").getJsonObject(0).getString("name")).isEqualTo("paho");
+        assertThat(response.isError()).isFalse();
+    }
+
+    @Test
+    void anUnknownArgumentIsNotedOnAnOtherwiseValidCall() throws Exception {
+        input("camel_catalog_find", "term", "mqtt", "limit", 1, "bogus", "x");
+
+        ToolResponse response = 
output(ToolResponse.success("{\"term\":\"mqtt\"}"));
+
+        assertThat(new 
JsonObject(response.firstContent().asText().text()).getString(ToolArgumentAliases.NOTE_KEY))
+                .isEqualTo("ignored: bogus; camel_catalog_find takes term 
(required), kind, limit, camelVersion");
+    }
+
+    @Test
+    void aCallWithTheDeclaredNamesIsAnsweredAsItIs() throws Exception {
+        ToolInputContext context = input("camel_catalog_find", "term", "mqtt", 
"limit", 1);
+        
assertThat(context.getArguments().getMap()).containsExactly(Map.entry("term", 
"mqtt"), Map.entry("limit", 1));
+
+        ToolResponse answer = ToolResponse.success("{\"term\":\"mqtt\"}");
+        assertThat(output(answer)).isSameAs(answer);
+    }
+
+    @Test
+    void onlyAJsonObjectAnswerGetsTheNote() {
+        String note = "term is the argument name (got query)";
+        assertThat(McpArgumentGuardrail.withNote(ToolResponse.error("No such 
file in the directory: x.yaml"), note))
+                .isNull();
+        assertThat(McpArgumentGuardrail.withNote(ToolResponse.success("No 
running Camel processes found"), note))
+                .isNull();
+        
assertThat(McpArgumentGuardrail.withNote(ToolResponse.success("[\"a\"]"), 
note)).isNull();
+        assertThat(McpArgumentGuardrail.withNote(ToolResponse.success("{not 
json"), note)).isNull();
+        assertThat(McpArgumentGuardrail.withNote(ToolResponse.success(new 
TextContent("{}"), new TextContent("{}")),
+                note)).isNull();
+        
assertThat(McpArgumentGuardrail.withNote(ToolResponse.structuredSuccess(Map.of("a",
 1)), note)).isNull();
+        assertThat(McpArgumentGuardrail.withNote(ToolResponse.success("{}"), 
note).firstContent().asText().text())
+                .isEqualTo("{\"argumentNote\":\"term is the argument name (got 
query)\"}");
+    }
+
+    @Test
+    void theNoteOfOneCallDoesNotReachTheNext() throws Exception {
+        input("camel_catalog_find", "query", "mqtt");
+        output(ToolResponse.success("{}"));
+
+        ToolResponse answer = ToolResponse.success("{\"term\":\"s3\"}");
+        assertThat(output(answer)).isSameAs(answer);
+    }
+
+    /** Runs the input guardrail on a call of the tool with these arguments; 
the context has the arguments it set. */
+    private ToolInputContext input(String tool, Object... pairs) throws 
Exception {
+        JsonObject arguments = new JsonObject();
+        for (int i = 0; i + 1 < pairs.length; i += 2) {
+            arguments.put((String) pairs[i], pairs[i + 1]);
+        }
+        InputContext context = new 
InputContext(McpToolClasses.toolInfo(McpToolClasses.toolMethod(tool)), 
arguments);
+        guardrail.apply(context);
+        return context;
+    }
+
+    /** Runs the output guardrail on the response of the call last given to 
{@link #input}. */
+    private ToolResponse output(ToolResponse response) {
+        OutputContext context = new OutputContext(response);
+        guardrail.apply(context);
+        return context.response;
+    }
+
+    private static final class InputContext implements ToolInputContext {
+
+        private final ToolInfo tool;
+        private JsonObject arguments;
+
+        InputContext(ToolInfo tool, JsonObject arguments) {
+            this.tool = tool;
+            this.arguments = arguments;
+        }
+
+        @Override
+        public ToolInfo getTool() {
+            return tool;
+        }
+
+        @Override
+        public McpConnection getConnection() {
+            return null;
+        }
+
+        @Override
+        public RequestId getRequestId() {
+            return null;
+        }
+
+        @Override
+        public JsonObject getArguments() {
+            return arguments;
+        }
+
+        @Override
+        public Meta getMeta() {
+            return null;
+        }
+
+        @Override
+        public void setArguments(JsonObject arguments) {
+            this.arguments = arguments;
+        }
+    }
+
+    private static final class OutputContext implements ToolOutputContext {
+
+        private ToolResponse response;
+
+        OutputContext(ToolResponse response) {
+            this.response = response;
+        }
+
+        @Override
+        public ToolInfo getTool() {
+            return null;
+        }
+
+        @Override
+        public McpConnection getConnection() {
+            return null;
+        }
+
+        @Override
+        public RequestId getRequestId() {
+            return null;
+        }
+
+        @Override
+        public Meta getMeta() {
+            return null;
+        }
+
+        @Override
+        public ToolResponse getResponse() {
+            return response;
+        }
+
+        @Override
+        public void setResponse(ToolResponse response) {
+            this.response = response;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpToolClasses.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpToolClasses.java
index a1c5d138d1df..73bd6b23260f 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpToolClasses.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/McpToolClasses.java
@@ -17,17 +17,25 @@
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
 import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.Proxy;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
+import io.quarkiverse.mcp.server.ToolManager.ToolArgument;
+import io.quarkiverse.mcp.server.ToolManager.ToolInfo;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -57,14 +65,55 @@ final class McpToolClasses {
 
     /** The tool names: the {@link Tool#name()} when set, else the method 
name. */
     static Set<String> toolNames() throws IOException, URISyntaxException {
+        return 
toolMethods().stream().map(McpToolClasses::toolName).collect(Collectors.toSet());
+    }
+
+    /** The methods that are tools. */
+    static List<Method> toolMethods() throws IOException, URISyntaxException {
         return toolClasses().stream()
                 .flatMap(c -> Arrays.stream(c.getDeclaredMethods()))
                 .filter(m -> m.isAnnotationPresent(Tool.class))
-                .map(m -> {
-                    String name = m.getAnnotation(Tool.class).name();
-                    return Tool.ELEMENT_NAME.equals(name) ? m.getName() : name;
-                })
-                .collect(Collectors.toSet());
+                .toList();
+    }
+
+    static String toolName(Method method) {
+        String name = method.getAnnotation(Tool.class).name();
+        return Tool.ELEMENT_NAME.equals(name) ? method.getName() : name;
+    }
+
+    /** The tool of that name. */
+    static Method toolMethod(String name) throws IOException, 
URISyntaxException {
+        return toolMethods().stream().filter(m -> 
toolName(m).equals(name)).findFirst()
+                .orElseThrow(() -> new IllegalArgumentException("No tool " + 
name));
+    }
+
+    /**
+     * The tool as the server describes it to a guardrail: its name and its 
arguments in declaration order, with the
+     * names and required flags the server derives from the parameters (the 
{@link ToolArg} name, else the parameter
+     * name; required unless {@link ToolArg#required()} is false or the type 
is {@link Optional}). The parameters the
+     * server provides itself, such as the {@code McpConnection}, are not 
arguments.
+     */
+    static ToolInfo toolInfo(Method method) {
+        List<ToolArgument> arguments = new ArrayList<>();
+        for (Parameter p : method.getParameters()) {
+            if 
(p.getType().getPackageName().equals(Tool.class.getPackageName())) {
+                continue;
+            }
+            ToolArg arg = p.getAnnotation(ToolArg.class);
+            String name = arg != null && 
!ToolArg.ELEMENT_NAME.equals(arg.name()) ? arg.name() : p.getName();
+            boolean required = arg != null ? arg.required() : 
!Optional.class.equals(p.getType());
+            arguments.add(new ToolArgument(
+                    name, arg != null ? arg.description() : "", required,
+                    p.getParameterizedType(), null));
+        }
+        String name = toolName(method);
+        return (ToolInfo) 
Proxy.newProxyInstance(McpToolClasses.class.getClassLoader(), new Class<?>[] { 
ToolInfo.class },
+                (proxy, m, args) -> switch (m.getName()) {
+                    case "name" -> name;
+                    case "arguments" -> arguments;
+                    case "toString" -> name;
+                    default -> throw new 
UnsupportedOperationException(m.getName());
+                });
     }
 
     private static boolean declaresTool(Class<?> clazz) {

Reply via email to