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]