This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 3ebb7bbfa728 CAMEL-24695: camel-jbang-mcp - optional arguments, blank
arguments and camel_run defaults on the shared authoring tools (#26376)
3ebb7bbfa728 is described below
commit 3ebb7bbfa728740fe8d7bcdc33ec3570cc1df97b
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 13 19:13:12 2026 +0200
CAMEL-24695: camel-jbang-mcp - optional arguments, blank arguments and
camel_run defaults on the shared authoring tools (#26376)
* CAMEL-24695: camel-jbang-mcp - optional arguments, blank arguments and
camel_run defaults
The Quarkus wrappers declared every argument required, so the server
rejected
any call that omitted an optional one and the documented defaults ("omitted
lists the files", "default: the only one running") were unreachable. Each
@ToolArg now carries the required flag of the shared descriptor, and the
wrapper test fails when the two disagree. Blank string arguments are left
out
like null ones, so a client that sends "" for an argument it does not need
gets
the default instead of, for example, a catalog kind called nothing.
camel_run with no files passed nothing to camel run, which then looks for an
application.properties with camel.main.routesIncludePattern and fails; the
launcher now lists the directory's source files itself, as a shell expands
camel run *, since the process is started without one. The result reports
the
name the caller gave, which is the one it uses with the other tools.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24695: camel-jbang - camel_run lists the source files camel run
accepts (yaml, xml, java, properties)
Review note on PR 26376: groovy, kts, js and jsh were Camel 3 DSLs; Camel 4
has Java, XML and YAML
(SourceHelper.ACCEPTED_FILE_EXT).
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---------
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
.../core/commands/ai/IntegrationLauncher.java | 84 ++++++++++++++++----
.../core/commands/ai/IntegrationLauncherTest.java | 55 +++++++++++++
.../jbang/core/commands/mcp/AuthoringTools.java | 89 ++++++++++++----------
.../dsl/jbang/core/commands/mcp/DiagnoseTools.java | 2 +-
.../core/commands/mcp/AuthoringToolsTest.java | 34 +++++++++
5 files changed, 209 insertions(+), 55 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index 908623b16404..09409e7738b6 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -56,19 +56,16 @@ public final class IntegrationLauncher {
*/
public static JsonObject run(Path directory, List<String> files, String
name, boolean dev, List<String> extraArgs) {
List<String> cmd = new ArrayList<>(LauncherHelper.getCamelCommand());
- cmd.add("run");
- cmd.addAll(files);
- if (dev) {
- cmd.add("--dev");
- }
- if (name != null && !name.isBlank()) {
- cmd.add("--name=" + name);
- }
- cmd.add("--logging-color=false");
- if (extraArgs != null) {
- cmd.addAll(extraArgs);
- }
+ List<String> sources = files == null || files.isEmpty() ?
sourceFiles(directory) : files;
+ cmd.addAll(runArguments(sources, name, dev, extraArgs));
JsonObject result = new JsonObject();
+ if (sources.isEmpty()) {
+ result.put("directory", directory.toString());
+ result.put("status", "failed");
+ result.put("error", "No source files to run in " + directory
+ + " (route files such as *.camel.yaml, *.xml,
*.java, or application.properties)");
+ return result;
+ }
result.put("directory", directory.toString());
result.put("command", String.join(" ", cmd));
Path output;
@@ -99,12 +96,16 @@ public final class IntegrationLauncher {
}
RuntimeHelper.ProcessInfo info = findByPid(pid);
if (info != null) {
+ // the name the other tools find the process by: the one the
caller gave, else the Camel context name
+ // (the source file name without its extensions); the pid is
in the result as well
+ String started = name != null && !name.isBlank()
+ ? name : info.contextName() != null &&
!info.contextName().isBlank() ? info.contextName() : info.name();
result.put("status", "started");
result.put("pid", pid);
- result.put("name", info.name());
+ result.put("name", started);
result.put("log", LogFileReader.logFile(pid,
info.name()).toString());
result.put("devMode", dev);
- result.put("message", "Started " + info.name() + " (pid " +
pid + ")"
+ result.put("message", "Started " + started + " (pid " + pid +
")"
+ (dev
? "; dev mode reloads the routes
when a source file changes"
: "; restart it after changing a
source file")
@@ -126,6 +127,61 @@ public final class IntegrationLauncher {
return result;
}
+ /**
+ * File extensions {@code camel run} loads from a project directory, as
{@code camel run *} would pass them: the
+ * three DSLs (see {@code SourceHelper.ACCEPTED_FILE_EXT}) and the
properties files.
+ */
+ private static final List<String> SOURCE_EXTENSIONS = List.of(".yaml",
".yml", ".xml", ".java", ".properties");
+
+ /**
+ * The source files {@code camel run} should load from a directory when
the caller names none: the regular,
+ * non-hidden files with a source extension, sorted by name. This is what
a shell expands {@code camel run *} to;
+ * the process is started without a shell, and {@code camel run} with no
files would instead look for an
+ * {@code application.properties} with {@code
camel.main.routesIncludePattern} and fail when there is none.
+ *
+ * @param directory the project directory
+ * @return the file names relative to the directory, empty when
there is nothing to run
+ */
+ static List<String> sourceFiles(Path directory) {
+ List<String> names = new ArrayList<>();
+ try (var stream = Files.list(directory)) {
+ stream.filter(Files::isRegularFile)
+ .map(p -> p.getFileName().toString())
+ .filter(n -> !n.startsWith(".") &&
SOURCE_EXTENSIONS.stream().anyMatch(n::endsWith))
+ .sorted()
+ .forEach(names::add);
+ } catch (IOException e) {
+ // an unreadable directory has nothing to run
+ }
+ return names;
+ }
+
+ /**
+ * The {@code camel run} arguments for the given files, name and mode.
+ *
+ * @param files the source files, relative to the directory
+ * @param name the integration name, or null for the default
+ * @param dev whether to run in dev mode
+ * @param extraArgs further {@code camel run} arguments
+ * @return the arguments after the camel command itself
+ */
+ static List<String> runArguments(List<String> files, String name, boolean
dev, List<String> extraArgs) {
+ List<String> cmd = new ArrayList<>();
+ cmd.add("run");
+ cmd.addAll(files);
+ if (dev) {
+ cmd.add("--dev");
+ }
+ if (name != null && !name.isBlank()) {
+ cmd.add("--name=" + name);
+ }
+ cmd.add("--logging-color=false");
+ if (extraArgs != null) {
+ cmd.addAll(extraArgs);
+ }
+ return cmd;
+ }
+
private static RuntimeHelper.ProcessInfo findByPid(long pid) {
for (RuntimeHelper.ProcessInfo p : RuntimeHelper.discoverProcesses()) {
if (p.pid() == pid) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
new file mode 100644
index 000000000000..46dcd3fa9f24
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class IntegrationLauncherTest {
+
+ @Test
+ void noFilesRunsEverySourceFileInTheDirectory(@TempDir Path dir) throws
Exception {
+ // the process is started without a shell, so nothing expands *, and
camel run with no files looks for an
+ // application.properties with camel.main.routesIncludePattern: the
launcher lists the sources itself
+ Files.writeString(dir.resolve("b.camel.yaml"), "- from:\n uri:
timer:tick\n steps: []\n");
+ Files.writeString(dir.resolve("a.camel.yaml"), "- from:\n uri:
timer:tock\n steps: []\n");
+ Files.writeString(dir.resolve("application.properties"), "x=1\n");
+ Files.writeString(dir.resolve("README.md"), "not a source\n");
+ Files.writeString(dir.resolve(".hidden.yaml"), "- from:\n uri:
timer:hidden\n steps: []\n");
+ Files.createDirectory(dir.resolve("sub.yaml"));
+ assertThat(IntegrationLauncher.sourceFiles(dir))
+ .containsExactly("a.camel.yaml", "application.properties",
"b.camel.yaml");
+
assertThat(IntegrationLauncher.runArguments(IntegrationLauncher.sourceFiles(dir),
null, true, null))
+ .containsExactly("run", "a.camel.yaml",
"application.properties", "b.camel.yaml", "--dev",
+ "--logging-color=false");
+
assertThat(IntegrationLauncher.sourceFiles(dir.resolve("nope"))).isEmpty();
+ }
+
+ @Test
+ void filesNameAndExtraArgumentsArePassedThrough() {
+ assertThat(IntegrationLauncher.runArguments(List.of("a.camel.yaml",
"application.properties"), "demo", true,
+ List.of("--port=9000")))
+ .containsExactly("run", "a.camel.yaml",
"application.properties", "--dev", "--name=demo",
+ "--logging-color=false", "--port=9000");
+ }
+}
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 4f3e86911d94..6146d06cca06 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
@@ -54,15 +54,18 @@ public class AuthoringTools {
+ "options, missing path. Replaces the former
AsciiDoc-only camel_catalog_doc: includeDoc=true "
+ "adds the AsciiDoc page.")
public JsonObject camel_catalog_doc(
- @ToolArg(description = "Name, e.g. kafka, json-jackson, simple,
timer, choice, split") String name,
- @ToolArg(description = "Endpoint URI to check, e.g.
kafka:orders?brokers=host:9092") String endpoint,
- @ToolArg(description = "component, dataformat, language or eip
(auto-detected)") String kind,
- @ToolArg(description = "Include the options (default true)")
Boolean includeOptions,
- @ToolArg(description = "Include the full AsciiDoc page (default
false)") Boolean includeDoc,
+ @ToolArg(description = "Name, e.g. kafka, json-jackson, simple,
timer, choice, split",
+ required = false) String name,
+ @ToolArg(description = "Endpoint URI to check, e.g.
kafka:orders?brokers=host:9092",
+ required = false) String endpoint,
+ @ToolArg(description = "component, dataformat, language or eip
(auto-detected)", required = false) String kind,
+ @ToolArg(description = "Include the options (default true)",
required = false) Boolean includeOptions,
+ @ToolArg(description = "Include the full AsciiDoc page (default
false)", required = false) Boolean includeDoc,
@ToolArg(description = "A language doc sub-page (simple:
functions, operators, ognl, advanced) to return"
- + " as text") String docPage,
- @ToolArg(description = "Keyword to match in option names or
descriptions") String optionsFilter,
- @ToolArg(description = VERSION_DESC) String camelVersion) {
+ + " as text",
+ required = false) String docPage,
+ @ToolArg(description = "Keyword to match in option names or
descriptions", required = false) String optionsFilter,
+ @ToolArg(description = VERSION_DESC, required = false) String
camelVersion) {
return call("camel_catalog_doc", args("name", name, "endpoint",
endpoint, "kind", kind,
"includeOptions", includeOptions, "includeDoc", includeDoc,
"docPage", docPage,
"optionsFilter", optionsFilter, "camelVersion", camelVersion));
@@ -73,10 +76,10 @@ public class AuthoringTools {
+ "is not the exact name (mqtt, s3, snowflake, csv):
best match first with title and "
+ "description. camel_catalog_doc then gives the
options of one.")
public JsonObject camel_catalog_find(
- @ToolArg(description = "What to look for, e.g. mqtt, s3, database,
csv") String term,
- @ToolArg(description = "component, dataformat or language
(default: all)") String kind,
- @ToolArg(description = "Maximum matches per kind (default 10)")
Integer limit,
- @ToolArg(description = VERSION_DESC) String camelVersion) {
+ @ToolArg(description = "What to look for, e.g. mqtt, s3, database,
csv", required = true) String term,
+ @ToolArg(description = "component, dataformat or language
(default: all)", required = false) String kind,
+ @ToolArg(description = "Maximum matches per kind (default 10)",
required = false) Integer limit,
+ @ToolArg(description = VERSION_DESC, required = false) String
camelVersion) {
return call("camel_catalog_find", args("term", term, "kind", kind,
"limit", limit,
"camelVersion", camelVersion));
}
@@ -87,10 +90,11 @@ public class AuthoringTools {
+ "options. Use on content before writing it, or on an
existing file (no content) to explain "
+ "a reload error.")
public JsonObject camel_validate_source(
- @ToolArg(description = DIRECTORY_DESC + "; needed when no content
is given") String directory,
- @ToolArg(description = "File name; picks the checks by extension,
read when no content") String file,
- @ToolArg(description = "The source to validate") String content,
- @ToolArg(description = VERSION_DESC) String camelVersion) {
+ @ToolArg(description = DIRECTORY_DESC + "; needed when no content
is given", required = false) String directory,
+ @ToolArg(description = "File name; picks the checks by extension,
read when no content",
+ required = true) String file,
+ @ToolArg(description = "The source to validate", required = false)
String content,
+ @ToolArg(description = VERSION_DESC, required = false) String
camelVersion) {
return call("camel_validate_source", args("directory", directory,
"file", file, "content", content,
"camelVersion", camelVersion));
}
@@ -100,8 +104,8 @@ public class AuthoringTools {
+ "file its content. Use before editing to see the
routes, configuration and other files of "
+ "the integration.")
public JsonObject camel_get_files(
- @ToolArg(description = DIRECTORY_DESC) String directory,
- @ToolArg(description = "File name to read; omitted lists the
files") String file) {
+ @ToolArg(description = DIRECTORY_DESC, required = false) String
directory,
+ @ToolArg(description = "File name to read; omitted lists the
files", required = false) String file) {
return call("camel_get_files", args("directory", directory, "file",
file));
}
@@ -111,11 +115,11 @@ public class AuthoringTools {
+ "An integration running in dev mode reloads the
change, otherwise restart it with "
+ "camel_control.")
public JsonObject camel_write_file(
- @ToolArg(description = DIRECTORY_DESC) String directory,
- @ToolArg(description = "File name, no path") String file,
- @ToolArg(description = "The complete new content") String content,
- @ToolArg(description = "Validate before writing (default true)")
Boolean validate,
- @ToolArg(description = VERSION_DESC) String camelVersion) {
+ @ToolArg(description = DIRECTORY_DESC, required = false) String
directory,
+ @ToolArg(description = "File name, no path", required = true)
String file,
+ @ToolArg(description = "The complete new content", required =
true) String content,
+ @ToolArg(description = "Validate before writing (default true)",
required = false) Boolean validate,
+ @ToolArg(description = VERSION_DESC, required = false) String
camelVersion) {
return call("camel_write_file", args("directory", directory, "file",
file, "content", content,
"validate", validate, "camelVersion", camelVersion));
}
@@ -125,11 +129,12 @@ public class AuthoringTools {
+ "mode by default (route files reload when written).
Returns the pid and log file once it is "
+ "up; camel_get_log and camel_get_errors then tell
how it does, camel_control stops it.")
public JsonObject camel_run(
- @ToolArg(description = "Project directory to run in (absolute
path)") String directory,
+ @ToolArg(description = "Project directory to run in (absolute
path)", required = true) String directory,
@ToolArg(description = "Source files to run, comma-separated
(default: every route file in the"
- + " directory)") String files,
- @ToolArg(description = "Integration name (default: from the first
file)") String name,
- @ToolArg(description = "Dev mode with reload on file change
(default true)") Boolean dev) {
+ + " directory)",
+ required = false) String files,
+ @ToolArg(description = "Integration name (default: from the first
file)", required = false) String name,
+ @ToolArg(description = "Dev mode with reload on file change
(default true)", required = false) Boolean dev) {
return call("camel_run", args("directory", directory, "files", files,
"name", name, "dev", dev));
}
@@ -138,8 +143,9 @@ public class AuthoringTools {
+ "without dev mode), stop-routes, start-routes,
reset-stats (clears statistics, routes "
+ "untouched). Never stop, kill or restart unless the
user asked for it.")
public JsonObject camel_control(
- @ToolArg(description = "stop, kill, restart, stop-routes,
start-routes or reset-stats") String action,
- @ToolArg(description = NAME_DESC) String name) {
+ @ToolArg(description = "stop, kill, restart, stop-routes,
start-routes or reset-stats",
+ required = true) String action,
+ @ToolArg(description = NAME_DESC, required = false) String name) {
return call("camel_control", args("action", action, "name", name));
}
@@ -147,10 +153,10 @@ public class AuthoringTools {
description = "Recent log records of a running integration, newest
first, with optional filtering; a "
+ "stack trace comes as one record with a detail
block.")
public JsonObject camel_get_log(
- @ToolArg(description = NAME_DESC) String name,
- @ToolArg(description = "Maximum records to return (default 50)")
Integer limit,
- @ToolArg(description = "Case-insensitive substring filter on the
message") String filter,
- @ToolArg(description = "Only this log level (INFO, WARN, ERROR,
DEBUG, TRACE)") String level) {
+ @ToolArg(description = NAME_DESC, required = false) String name,
+ @ToolArg(description = "Maximum records to return (default 50)",
required = false) Integer limit,
+ @ToolArg(description = "Case-insensitive substring filter on the
message", required = false) String filter,
+ @ToolArg(description = "Only this log level (INFO, WARN, ERROR,
DEBUG, TRACE)", required = false) String level) {
return call("camel_get_log", args("name", name, "limit", limit,
"filter", filter, "level", level));
}
@@ -158,7 +164,7 @@ public class AuthoringTools {
description = "The failed exchanges of a running integration:
routeId, exchangeId, exception with stack "
+ "trace, body and headers.")
public JsonObject camel_get_errors(
- @ToolArg(description = NAME_DESC) String name) {
+ @ToolArg(description = NAME_DESC, required = false) String name) {
return call("camel_get_errors", args("name", name));
}
@@ -167,10 +173,10 @@ public class AuthoringTools {
+ "Returns the value (true/false for a predicate) or
the syntax error, so check simple before "
+ "answering or writing it.")
public JsonObject camel_eval_expression(
- @ToolArg(description = "e.g. ${random(1,10)} or ${body} ?:
'none'") String expression,
- @ToolArg(description = "simple (default), jsonpath, xpath, jq")
String language,
- @ToolArg(description = "Message body") String body,
- @ToolArg(description = NAME_DESC) String name) {
+ @ToolArg(description = "e.g. ${random(1,10)} or ${body} ?:
'none'", required = true) String expression,
+ @ToolArg(description = "simple (default), jsonpath, xpath, jq",
required = false) String language,
+ @ToolArg(description = "Message body", required = false) String
body,
+ @ToolArg(description = NAME_DESC, required = false) String name) {
return call("camel_eval_expression", args("expression", expression,
"language", language, "body", body,
"name", name));
}
@@ -184,12 +190,15 @@ public class AuthoringTools {
}
}
- /** Name and value pairs into the string arguments of the registry; a null
value is left out. */
+ /**
+ * Name and value pairs into the string arguments of the registry. A null
or blank value is left out, so an argument
+ * the client did not provide, or sent as an empty string, gets the tool's
default.
+ */
static Map<String, String> args(Object... pairs) {
Map<String, String> map = new LinkedHashMap<>();
for (int i = 0; i + 1 < pairs.length; i += 2) {
Object value = pairs[i + 1];
- if (value != null) {
+ if (value != null && !(value instanceof String str &&
str.isBlank())) {
map.put(String.valueOf(pairs[i]), String.valueOf(value));
}
}
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 53d151e5d0c8..ff7e0775a962 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
@@ -51,7 +51,7 @@ public class DiagnoseTools {
+ "Covers the most common Camel exceptions including
NoSuchEndpointException, "
+ "ResolveEndpointFailedException,
FailedToCreateRouteException, and more.")
public JsonObject camel_error_diagnose(
- @ToolArg(description = "The Camel stack trace or error message to
diagnose") String error,
+ @ToolArg(description = "The Camel stack trace or error message to
diagnose", required = true) String error,
@ToolArg(description = ToolArgDocs.RUNTIME, required = false)
String runtime,
@ToolArg(description = ToolArgDocs.CAMEL_VERSION, required =
false) String camelVersion,
@ToolArg(description = ToolArgDocs.PLATFORM_BOM, required = false)
String platformBom) {
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
index abc8e38f708c..1dde205fcfc0 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringToolsTest.java
@@ -17,13 +17,16 @@
package org.apache.camel.dsl.jbang.core.commands.mcp;
import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
import io.quarkiverse.mcp.server.Tool;
+import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
import org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
@@ -108,4 +111,35 @@ class AuthoringToolsTest {
assertThatThrownBy(() -> tools.camel_get_log("no-such-app-xyz-1",
null, null, null))
.isInstanceOf(ToolCallException.class).hasMessageContaining("no-such-app-xyz-1");
}
+
+ @Test
+ void everyArgumentIsRequiredExactlyWhenTheSharedDescriptorSaysSo() {
+ // the Quarkus server rejects a call that omits an argument declared
required, so an optional argument with a
+ // documented default ("omitted lists the files", "default: the only
one running") must not be required here
+ for (Method m : AuthoringTools.class.getMethods()) {
+ if (m.getAnnotation(Tool.class) == null) {
+ continue;
+ }
+ ToolDescriptor td = ToolRegistry.findTool(m.getName());
+ List<ToolDescriptor.Param> params = td.params();
+ Parameter[] args = m.getParameters();
+ assertThat(args).as(m.getName() + " has one argument per
descriptor parameter").hasSize(params.size());
+ for (int i = 0; i < args.length; i++) {
+ ToolArg arg = args[i].getAnnotation(ToolArg.class);
+ assertThat(arg).as(m.getName() + " argument " + i).isNotNull();
+ assertThat(arg.required()).as(m.getName() + " argument '" +
params.get(i).name() + "' required")
+ .isEqualTo(params.get(i).required());
+ }
+ }
+ }
+
+ @Test
+ void blankArgumentsAreLeftOutSoTheToolDefaultApplies() {
+ Map<String, String> args = AuthoringTools.args("name", "timer",
"kind", "", "limit", 3, "dev", true, "x", null);
+ assertThat(args).containsExactly(Map.entry("name", "timer"),
Map.entry("limit", "3"), Map.entry("dev", "true"));
+ // a blank kind means auto-detect, not "a kind called nothing"
+ assertThat(tools.camel_catalog_doc("timer", "", "", null, null, "",
"period", "").getString("kind"))
+ .isEqualTo("component");
+ assertThat(tools.camel_catalog_find("mqtt", "", null,
"").getInteger("count")).isGreaterThan(0);
+ }
}