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) {