davsclaus commented on code in PR #26352:
URL: https://github.com/apache/camel/pull/26352#discussion_r3996125595


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java:
##########
@@ -46,6 +55,100 @@ public boolean hasProcess() {
         return pid >= 0;
     }
 
+    /** Forgets the selected process, so tools fall back to what needs no 
running integration. */
+    public void clearProcess() {
+        this.pid = -1;
+    }
+
+    /**
+     * Whether a runtime tool called without a name may pick the only running 
Camel process when none is selected: the
+     * default for a server without a selection of its own; the TUI turns it 
off, since there what the user selected is
+     * the integration.
+     */

Review Comment:
   Added a Javadoc note on `camelVersion()` in c65d94a4ef54 stating the 
contract: a context serves one tool call at a time (the MCP servers and the TUI 
build a fresh one per call, `camel ask` runs its calls one after the other), 
and the version is set before the catalog is first asked for, so a version 
change only drops the cached catalog for the next call. No synchronization 
added on purpose, since nothing shares a context across threads.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java:
##########
@@ -0,0 +1,469 @@
+/*
+ * 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.io.IOException;
+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.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor.tool;
+
+/**
+ * The neutral set of Camel authoring tools for AI agents, defined once and 
exposed by every Camel MCP server
+ * ({@code camel mcp} and {@code camel tui --mcp}) under the same {@code 
camel_} names, so an agent gets the same Camel
+ * through either door: catalog documentation with the URI rules and the 
simple syntax, source validation, reading and
+ * writing the source files of a project, running an integration in dev mode 
and reading its log and errors, evaluating
+ * an expression and diagnosing an error.
+ * <p>
+ * The tools are self-contained: a file tool takes the project {@code 
directory} as an argument (a server that has a
+ * selected integration fills it in through {@link 
ToolContext#setDefaultDirectory}), and a runtime tool takes the
+ * integration {@code name} (or uses the selected or the only running one). 
Writing a file is validated first and
+ * refused when the content is invalid; confirming a write with a human is the 
job of the client or of the TUI, not of
+ * these tools.
+ */
+public final class AuthoringTools {
+
+    static final String NAME_DESC = "Integration name or pid (default: the 
selected one, or the only one running)";
+    static final String VERSION_DESC = "Camel version to answer for (default: 
the CLI's own, or the selected integration's)";
+    static final String DIRECTORY_DESC = "Project directory with the source 
files (default: the selected integration's)";
+
+    /** Files listed and read by the file tools; more than that and a 
directory is not an integration's sources. */
+    private static final int MAX_FILES = 99;
+
+    private AuthoringTools() {
+    }
+
+    /** Registers the authoring tools; called once by the {@link 
ToolRegistry}. */
+    static void register(Consumer<ToolDescriptor> registry) {
+        registry.accept(tool("camel_catalog_doc",
+                "Camel catalog documentation of a component, data format, 
language or EIP (description, options, "
+                                                  + "Maven coordinates), with 
the URI rules of a component. For simple also its "
+                                                  + "syntax rules, functions 
and operators: count and names by group, or with "
+                                                  + "optionsFilter the 
matching ones with parameters and examples. endpoint "
+                                                  + "validates a URI: unknown 
or invalid options, missing path.")
+                .param("name", "string", "Name, e.g. kafka, json-jackson, 
simple, timer, choice, split", false)
+                .param("endpoint", "string", "Endpoint URI to check, e.g. 
kafka:orders?brokers=host:9092", false)
+                .param("kind", "string", "component, dataformat, language or 
eip (auto-detected)", false)
+                .param("includeOptions", "boolean", "Include the options 
(default true)", false)
+                .param("includeDoc", "boolean", "Include the full AsciiDoc 
page (default false)", false)
+                .param("docPage", "string", "A language doc sub-page (simple: 
functions, operators, ognl, advanced)"
+                                            + " to return as text",
+                        false)
+                .param("optionsFilter", "string", "Keyword to match in option 
names or descriptions", false)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    return CatalogDocs.catalogDoc(ctx.catalog(), 
args.get("name"), args.get("endpoint"),
+                            args.get("kind"), args.get("optionsFilter"), 
bool(args, "includeOptions", true),
+                            bool(args, "includeDoc", false), 
args.get("docPage")).toJson();
+                }));
+
+        registry.accept(tool("camel_catalog_find",
+                "Finds Camel components, data formats and languages by a 
protocol, product or other term that 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.")
+                .param("term", "string", "What to look for, e.g. mqtt, s3, 
database, csv", true)
+                .param("kind", "string", "component, dataformat or language 
(default: all)", false)
+                .param("limit", "integer", "Maximum matches per kind (default 
10)", false)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    return CatalogDocs.find(ctx.catalog(), args.get("term"), 
args.get("kind"),
+                            integer(args, "limit", 10)).toJson();
+                }));
+
+        registry.accept(tool("camel_validate_source",
+                "Validates Camel YAML DSL or .properties source without 
writing: schema (misspelled options such as "
+                                                      + "logLevel instead of 
loggingLevel), endpoint URIs, simple expressions, "
+                                                      + "camel.* options. Use 
on content before writing it, or on an existing "
+                                                      + "file (no content) to 
explain a reload error.")
+                .param("directory", "string", DIRECTORY_DESC, false)
+                .param("file", "string", "File name; picks the checks by 
extension, read when no content", true)
+                .param("content", "string", "The source to validate", false)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    String file = required(args, "file");
+                    String content = args.get("content");
+                    if (content == null) {
+                        Path path = 
resolveFile(ctx.resolveDirectory(args.get("directory")), file);
+                        if (!Files.isRegularFile(path)) {
+                            throw new ToolExecutionException("No such file in 
the directory: " + file);
+                        }
+                        content = read(path);
+                    }
+                    return validate(ctx, file, content).toJson();
+                }));
+
+        registry.accept(tool("camel_get_files",
+                "The source files of a project directory: without file the 
list (name, size, type), with file its "
+                                                + "content. Use before editing 
to see the routes, configuration and other "
+                                                + "files of the integration.")
+                .param("directory", "string", DIRECTORY_DESC, false)
+                .param("file", "string", "File name to read; omitted lists the 
files", false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    Path dir = ctx.resolveDirectory(args.get("directory"));
+                    String file = args.get("file");
+                    if (file != null && !file.isBlank()) {
+                        return readFile(dir, file).toJson();
+                    }
+                    return listFiles(dir).toJson();
+                }));
+
+        registry.accept(tool("camel_write_file",
+                "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. "
+                                                 + "An integration running in 
dev mode reloads the change, otherwise restart it "
+                                                 + "with camel_control.")
+                .param("directory", "string", DIRECTORY_DESC, false)
+                .param("file", "string", "File name, no path", true)
+                .param("content", "string", "The complete new content", true)
+                .param("validate", "boolean", "Validate before writing 
(default true)", false)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .readOnly(false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    Path dir = ctx.resolveDirectory(args.get("directory"));
+                    return writeFile(ctx, dir, required(args, "file"), 
required(args, "content"),
+                            bool(args, "validate", true)).toJson();
+                }));
+
+        registry.accept(tool("camel_run",
+                "Starts an integration from a project directory with camel run 
in a separate process, in dev 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.")
+                .param("directory", "string", "Project directory to run in", 
true)
+                .param("files", "string", "Source files to run, 
comma-separated (default: every route file in the"
+                                          + " directory)",
+                        false)
+                .param("name", "string", "Integration name (default: from the 
first file)", false)
+                .param("dev", "boolean", "Dev mode with reload on file change 
(default true)", false)
+                .readOnly(false)
+                .executor((ctx, args) -> {
+                    Path dir = ctx.resolveDirectory(args.get("directory"));
+                    List<String> files = new ArrayList<>();
+                    String list = args.get("files");
+                    if (list != null && !list.isBlank()) {
+                        for (String f : list.split(",")) {
+                            if (!f.isBlank()) {
+                                files.add(f.trim());
+                            }
+                        }
+                    }
+                    JsonObject result = IntegrationLauncher.run(dir, files, 
args.get("name"), bool(args, "dev", true),
+                            List.of());
+                    if (result.get("pid") instanceof Long pid) {
+                        ctx.selectProcess(pid);
+                    }
+                    return result.toJson();
+                }));
+
+        registry.accept(tool("camel_control",
+                "Controls a running integration: stop (graceful), kill, 
restart (picks up edited files without dev "
+                                              + "mode), stop-routes, 
start-routes, reset-stats (clears statistics, routes "
+                                              + "untouched). Never stop, kill 
or restart unless the user asked for it.")
+                .param("action", "string", "stop, kill, restart, stop-routes, 
start-routes or reset-stats", true)
+                .param("name", "string", NAME_DESC, false)
+                .readOnly(false)
+                .destructive(true)
+                .core(true)
+                .executor((ctx, args) -> {
+                    selectProcess(ctx, args);
+                    return IntegrationLauncher.control(ctx, required(args, 
"action"));
+                }));
+
+        registry.accept(tool("camel_get_log",
+                "Recent log records of a running integration, newest first, 
with optional filtering; a stack trace "
+                                              + "comes as one record with a 
detail block.")
+                .param("name", "string", NAME_DESC, false)
+                .param("limit", "integer", "Maximum records to return (default 
50)", false)
+                .param("filter", "string", "Case-insensitive substring filter 
on the message", false)
+                .param("level", "string", "Only this log level (INFO, WARN, 
ERROR, DEBUG, TRACE)", false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    RuntimeHelper.ProcessInfo p = selectProcess(ctx, args);
+                    return LogFileReader.read(ctx.pid(), p != null ? p.name() 
: null, integer(args, "limit", 50),
+                            args.get("filter"), args.get("level")).toJson();
+                }));
+
+        registry.accept(tool("camel_get_errors",
+                "The failed exchanges of a running integration: routeId, 
exchangeId, exception with stack trace, "
+                                                 + "body and headers.")
+                .param("name", "string", NAME_DESC, false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    selectProcess(ctx, args);
+                    JsonObject errors = ctx.readErrorFile();
+                    return errors != null ? errors.toJson() : "No errors 
captured.";
+                }));
+
+        registry.accept(tool("camel_eval_expression",
+                "Evaluates an expression: in the running integration when 
there is one, else locally. Returns the "
+                                                      + "value (true/false for 
a predicate) or the syntax error, so check simple "
+                                                      + "before answering or 
writing it.")
+                .param("expression", "string", "e.g. ${random(1,10)} or 
${body} ?: 'none'", true)
+                .param("language", "string", "simple (default), jsonpath, 
xpath, jq", false)
+                .param("body", "string", "Message body", false)
+                .param("name", "string", NAME_DESC, false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    String name = args.get("name");
+                    if (name != null && !name.isBlank()) {
+                        ctx.selectProcess(name);
+                    } else {
+                        ctx.selectSingleProcessIfNone();
+                    }
+                    return ExpressionEvaluator.evaluate(ctx, 
args.get("language"), required(args, "expression"),
+                            args.get("body")).toJson();
+                }));
+
+        registry.accept(tool("camel_error_diagnose",
+                "Diagnoses a Camel error from a stack trace or error message: 
the known exceptions in it with common "
+                                                     + "causes and suggested 
fixes, the components and EIPs it mentions with "
+                                                     + "documentation links, 
and the route id.")
+                .param("error", "string", "The stack trace or error message", 
true)
+                .param("camelVersion", "string", VERSION_DESC, false)
+                .core(true)
+                .executor((ctx, args) -> {
+                    applyVersion(ctx, args);
+                    return ErrorDiagnoser.diagnose(required(args, "error"), 
ctx.catalog()).toJson();
+                }));
+    }
+
+    // ---- shared logic, also used by the TUI over its own selection ----
+
+    /** Validates source content for the context's Camel version, as {@code 
camel_validate_source} answers it. */
+    public static JsonObject validate(ToolContext ctx, String file, String 
content) {
+        if (!SourceValidator.isValidatableFile(file)) {
+            throw new ToolExecutionException(
+                    "No validation for " + file + ": only YAML routes and 
.properties files are validated");
+        }
+        List<String> errors = SourceValidator.validate(file, content, 
ctx.catalog(), ctx.propertyLineValidator());
+        JsonObject result = new JsonObject();
+        result.put("valid", errors.isEmpty());
+        result.put("file", file);
+        result.put("errors", new JsonArray(errors));
+        result.put("message", errors.isEmpty()
+                ? "The source is valid"
+                : errors.size() + " problem(s) found; fix them before writing 
the file");
+        return result;
+    }
+
+    /** Writes a file after validating it, as {@code camel_write_file} does; 
no confirmation is asked here. */
+    public static JsonObject writeFile(ToolContext ctx, Path dir, String file, 
String content, boolean validate) {
+        Path path = resolveFile(dir, file);
+        boolean exists = Files.exists(path);
+        if (exists && !Files.isRegularFile(path)) {
+            throw new ToolExecutionException(file + " is not a regular file");
+        }
+        if (validate && SourceValidator.isValidatableFile(file)) {
+            List<String> errors = SourceValidator.validate(file, content, 
ctx.catalog(), ctx.propertyLineValidator());
+            if (!errors.isEmpty()) {
+                JsonObject result = new JsonObject();
+                result.put("status", "invalid");
+                result.put("file", file);
+                result.put("errors", new JsonArray(errors));
+                result.put("message", "The file was not written: the content 
has validation errors. Fix them and"
+                                      + " call camel_write_file again 
(validate=false writes it anyway).");
+                return result;
+            }
+        }
+        try {
+            Files.writeString(path, content, StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            throw new ToolExecutionException("Failed to write " + path + ": " 
+ e.getMessage());
+        }
+        JsonObject result = new JsonObject();
+        result.put("status", exists ? "overwritten" : "created");
+        result.put("file", file);
+        result.put("directory", dir.toString());
+        result.put("lines", content.isEmpty() ? 0 : (int) 
content.lines().count());
+        result.put("bytes", content.getBytes(StandardCharsets.UTF_8).length);
+        result.put("message", "An integration running the file in dev mode 
reloads it now; otherwise restart the"
+                              + " integration for the change to take effect.");
+        return result;
+    }
+
+    /** The files of a project directory, as {@code camel_get_files} lists 
them. */
+    public static JsonObject listFiles(Path dir) {
+        JsonArray files = new JsonArray();
+        try (Stream<Path> stream = Files.list(dir)) {
+            stream.filter(Files::isRegularFile)
+                    .sorted((a, b) -> 
a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString()))
+                    .limit(MAX_FILES)
+                    .forEach(p -> {
+                        JsonObject entry = new JsonObject();
+                        entry.put("name", p.getFileName().toString());
+                        entry.put("size", formatSize(size(p)));
+                        entry.put("type", 
fileType(p.getFileName().toString()));
+                        files.add(entry);
+                    });
+        } catch (IOException e) {
+            throw new ToolExecutionException("Cannot list " + dir + ": " + 
e.getMessage());
+        }
+        JsonObject result = new JsonObject();
+        result.put("directory", dir.toString());
+        result.put("files", files);
+        result.put("totalFiles", files.size());
+        if (files.isEmpty()) {
+            result.put("message", "The directory has no files");
+        }
+        return result;
+    }
+
+    /** One file of a project directory with its content, as {@code 
camel_get_files} reads it. */

Review Comment:
   Confirmed, that is the design: the arguments come from a model, and a 
`limit` of `"ten"` should fall back to the default rather than fail the whole 
call. The same holds for `bool()`, where anything but `true` reads as false.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java:
##########
@@ -0,0 +1,546 @@
+/*
+ * 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.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.networknt.schema.Error;
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
+import org.apache.camel.catalog.EndpointValidationResult;
+import org.apache.camel.catalog.LanguageValidationResult;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
+
+/**
+ * Validates integration source files the way the Camel TUI's editor does on 
save, before an AI agent writes them: Camel
+ * YAML DSL against the YAML DSL schema (unknown or misspelled options, wrong 
structure), then the endpoint URIs and
+ * simple expressions in it against the catalog; a .properties file line by 
line against the catalog of {@code camel.*}
+ * options. Every message names the line so the agent can fix the file.
+ */
+public final class SourceValidator {
+
+    private static final Set<String> PREDICATE_EIPS = Set.of(
+            "filter", "when", "validate", "onWhen", "on-when",
+            "handled", "continued", "retryWhile", "retry-while",
+            "completionPredicate", "completion-predicate",
+            "completion", "loopDoWhile", "loop-do-while");
+
+    private static final Set<String> CONSUMER_EIPS
+            = Set.of("from", "pollEnrich", "poll-enrich", "poll", 
"interceptFrom", "intercept-from");
+    private static final Set<String> PRODUCER_EIPS
+            = Set.of("to", "toD", "to-d", "wireTap", "wire-tap", "enrich",
+                    "interceptSendToEndpoint", "intercept-send-to-endpoint");
+
+    private static final Pattern YAML_URI_PATTERN = Pattern.compile(
+            
"^\\s*-?\\s*(?:uri|from|to|toD|wireTap|enrich|pollEnrich|deadLetterChannel):\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*(?::[^\"\\s]*)?)");
+
+    private static volatile YamlValidator yamlValidator;
+
+    private SourceValidator() {
+    }
+
+    /** Whether the file has a validator: YAML routes and .properties files 
do, other files have none. */
+    public static boolean isValidatableFile(String fileName) {
+        String name = fileName == null ? "" : 
fileName.toLowerCase(Locale.ROOT);
+        return name.endsWith(".yaml") || name.endsWith(".yml") || 
name.endsWith(".properties");
+    }
+
+    /**
+     * Validates source by file type: Camel YAML DSL for .yaml/.yml files, 
Camel options for .properties files. Other
+     * file types have no validation and yield no messages.
+     *
+     * @param  fileName          the file name; its extension picks the checks
+     * @param  content           the source
+     * @param  catalog           the catalog of the Camel version the source 
is for
+     * @param  extraPropertyLine an extra check for a properties line the 
catalog does not know (Spring Boot
+     *                           properties), returning the message or null; 
may be null
+     * @return                   the messages, empty when the source is valid
+     */
+    public static List<String> validate(
+            String fileName, String content, CamelCatalog catalog, 
Function<String, String> extraPropertyLine) {
+        String name = fileName == null ? "" : 
fileName.toLowerCase(Locale.ROOT);
+        if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+            return validateCamelYaml(content, catalog);
+        }
+        if (name.endsWith(".properties")) {
+            return validateProperties(content, catalog, extraPropertyLine);
+        }
+        return List.of();
+    }
+
+    /**
+     * Validates Camel YAML DSL source: the YAML DSL schema first, then 
endpoint URIs and simple expressions against the
+     * catalog. Returns the messages, empty when the source is valid.
+     */
+    public static List<String> validateCamelYaml(String content, CamelCatalog 
catalog) {
+        List<String> msgs = new ArrayList<>();
+        if (content == null || content.isBlank()) {
+            return msgs;
+        }
+        try {
+            msgs.addAll(formatSchemaErrors(yamlValidator().validate(content)));
+        } catch (Exception e) {
+            msgs.add("Invalid YAML: " + e.getMessage());
+            return msgs;
+        }
+        if (catalog != null) {
+            msgs.addAll(validateYamlEndpoints(content, catalog));
+            msgs.addAll(validateYamlSimple(content, catalog));
+        }
+        return msgs;
+    }
+
+    private static YamlValidator yamlValidator() throws Exception {
+        YamlValidator v = yamlValidator;
+        if (v == null) {
+            synchronized (SourceValidator.class) {
+                v = yamlValidator;
+                if (v == null) {
+                    v = new YamlValidator();
+                    v.init();
+                    yamlValidator = v;
+                }
+            }
+        }
+        return v;
+    }
+
+    /**
+     * Validates a properties file line by line: {@code camel.*} keys against 
the catalog, the rest with the extra
+     * check.
+     */
+    public static List<String> validateProperties(
+            String content, CamelCatalog catalog, Function<String, String> 
extraPropertyLine) {
+        return validatePropertiesLines(content, line -> 
validatePropertyLine(line, catalog, extraPropertyLine));
+    }
+
+    /** Validates one properties line: a {@code camel.*} key against the 
catalog, any other with the extra check. */
+    public static String validatePropertyLine(String line, CamelCatalog 
catalog, Function<String, String> extra) {
+        if (catalog != null) {
+            try {
+                ConfigurationPropertiesValidationResult result = 
catalog.validateConfigurationProperty(line);
+                if (result.isAccepted()) {
+                    if (!result.isSuccess()) {
+                        String msg = result.summaryErrorMessage(false);
+                        if (msg != null) {
+                            return msg.trim();
+                        }
+                    }
+                    return null;
+                }
+            } catch (Exception e) {
+                // ignore validation errors
+            }
+        }
+        return extra != null ? extra.apply(line) : null;
+    }
+
+    /** Runs a line validator over the key=value lines of a properties file, 
prefixing each message with its line. */
+    public static List<String> validatePropertiesLines(String content, 
Function<String, String> lineValidator) {
+        List<String> msgs = new ArrayList<>();
+        if (content == null) {
+            return msgs;

Review Comment:
   Right, the pattern only ever sees one line at a time, so the anchor does 
what it should. The scanner came over unchanged from the TUI's 
`SourceEditAssist`, where it has been used the same way; the moved tests 
(`SourceValidatorEndpointTest`) cover the shorthand, expanded and 
parameters-block forms.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java:
##########
@@ -0,0 +1,259 @@
+/*
+ * 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.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Starts, stops and restarts an integration for an AI agent that is building 
one: {@code camel run --dev} in a separate
+ * JVM (through the same launcher the CLI itself uses), watched until its 
status file appears so the caller gets the pid
+ * and the log file back, and a restart that relaunches the process with its 
own command line.
+ */
+public final class IntegrationLauncher {
+
+    /** How long a started integration is given to publish its status file 
before the call returns without a pid. */
+    static final long STARTUP_TIMEOUT_MS = 30_000;
+
+    private IntegrationLauncher() {
+    }
+
+    /**
+     * Starts {@code camel run} detached in the given directory.
+     *
+     * @param  directory the project directory (the working directory of the 
process)
+     * @param  files     the source files to run, relative to the directory; 
empty runs every route file in it
+     * @param  name      the integration name (the {@code --name} option); 
null keeps the default
+     * @param  dev       dev mode (reload on file changes)
+     * @param  extraArgs further {@code camel run} arguments
+     * @return           status started (with pid, name, log), failed (with 
the output) or starting
+     */
+    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);
+        }
+        JsonObject result = new JsonObject();
+        result.put("directory", directory.toString());
+        result.put("command", String.join(" ", cmd));
+        Path output;
+        Process process;
+        try {
+            Files.createDirectories(CommandLineHelper.getCamelDir());
+            output = Files.createTempFile(CommandLineHelper.getCamelDir(), 
"camel-launch-", ".log");
+            output.toFile().deleteOnExit();
+            ProcessBuilder pb = new ProcessBuilder(cmd);
+            pb.directory(directory.toFile());
+            pb.redirectErrorStream(true);
+            pb.redirectOutput(output.toFile());
+            process = pb.start();
+        } catch (IOException e) {
+            result.put("status", "failed");
+            result.put("error", "Cannot start camel run: " + e.getMessage());
+            return result;
+        }
+        long pid = process.pid();

Review Comment:
   Agreed. The first run of a fresh project can exceed 30s while dependencies 
download, and in that case the caller gets `status: starting` with the pid and 
the output so far, and `list_processes` shows the integration once it is up. 
Kept the constant as is rather than making it a tool argument for now.
   
   _Claude Code on behalf of davsclaus_



##########
dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogTools.java:
##########
@@ -432,59 +432,6 @@ public DocListResult camel_catalog_docs(
         }
     }
 
-    /**
-     * Tool to get the full AsciiDoc documentation content for a catalog page.
-     */
-    @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint 
= false, openWorldHint = false),
-          description = "Get the full AsciiDoc documentation page from the 
catalog. "
-                        + "Returns the complete human-readable documentation 
with usage examples, "
-                        + "code snippets, configuration guides, and best 
practices. "
-                        + "This complements the structured tools 
(camel_catalog_component_doc, "
-                        + "camel_catalog_eip_doc, etc.) which return parsed 
JSON with options and metadata. "
-                        + "Use camel_catalog_docs to discover available page 
names. "
-                        + "Naming conventions: components use 
'<name>-component' (e.g., 'kafka-component'), "
-                        + "data formats use '<name>-dataformat' (e.g., 
'jackson-dataformat'), "
-                        + "languages use '<name>-language' (e.g., 
'simple-language'), "
-                        + "EIPs use '<name>-eip' (e.g., 'split-eip'), "
-                        + "and others use their plain name (e.g., 
'cloudevents', 'debug'). "
-                        + "Only available from Camel 4.22 onwards.")
-    public DocResult camel_catalog_doc(
-            @ToolArg(description = "Documentation page name without .adoc 
extension "
-                                   + "(e.g., 'kafka-component', 'split-eip', 
'simple-language', 'jackson-dataformat')") String name,
-            @ToolArg(description = ToolArgDocs.CAMEL_VERSION) String 
camelVersion) {
-
-        if (name == null || name.isBlank()) {
-            throw new ToolCallException("Documentation page name is required", 
null);
-        }
-
-        try {
-            CamelCatalog cat = catalogService.loadCatalog(null, camelVersion, 
null);
-            String content = cat.asciiDoc(name);
-            if (content == null) {
-                List<String> allNames = cat.findDocNames();
-                if (allNames != null) {
-                    String lower = name.toLowerCase();
-                    List<String> suggestions = allNames.stream()
-                            .filter(n -> n.toLowerCase().contains(lower) || 
lower.contains(n.toLowerCase()))
-                            .limit(5)
-                            .collect(Collectors.toList());
-                    if (!suggestions.isEmpty()) {
-                        throw new ToolCallException(
-                                "Documentation page not found: " + name + ". 
Did you mean one of: " + suggestions, null);
-                    }
-                }
-                throw new ToolCallException("Documentation page not found: " + 
name, null);
-            }
-            return new DocResult(name, content);
-        } catch (ToolCallException e) {
-            throw e;
-        } catch (Throwable e) {
-            throw new ToolCallException(
-                    "Failed to load documentation: " + name + " (" + 
e.getClass().getName() + "): " + e.getMessage(),
-                    null);
-        }
-    }
-
     private static List<String> findComponentNames(CamelCatalog catalog) {
         List<String> answer = catalog.findComponentNames();

Review Comment:
   Yes, this is deliberate and comes from the issue discussion: one tool name 
with one shape on both servers, and the structured form (options, URI rules, 
simple syntax) is what the editing benchmark needs, with `includeDoc=true` 
still giving the AsciiDoc page. Old callers that pass a page name such as 
`kafka-component` get a not-found answer with suggestions. The 4.23 upgrade 
guide has the entry under "camel-jbang (MCP servers)".
   
   _Claude Code on behalf of davsclaus_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to