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);
+    }
 }

Reply via email to