This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/processor-detail-mcp-tool in repository https://gitbox.apache.org/repos/asf/camel.git
commit 9ab526172ac15120503cc1d1baf0b1e398f77d29 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Jul 24 09:51:24 2026 +0200 camel-jbang - Add camel_runtime_processor_detail MCP tool Co-Authored-By: Claude Opus 4.6 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/dev-console/processor-detail.json | 4 +- .../impl/console/ProcessorDetailDevConsole.java | 45 +++++-- .../camel/cli/connector/LocalCliConnector.java | 6 +- .../dsl/jbang/core/commands/ai/ToolRegistry.java | 138 +++++++++++++++++++++ .../dsl/jbang/core/commands/mcp/RuntimeTools.java | 18 +++ 5 files changed, 201 insertions(+), 10 deletions(-) diff --git a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json index 2ea6f8d4be50..9b0d6bdadbb9 100644 --- a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json +++ b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/processor-detail.json @@ -18,13 +18,13 @@ "displayName": "Route Id", "group": "query", "label": "query", - "required": true, + "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, - "description": "The route id to get processor details for" + "description": "The route id to get processor details for (use * for all routes)" } } } diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java index 186cdd488486..0b565ffcb18b 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/ProcessorDetailDevConsole.java @@ -17,6 +17,7 @@ package org.apache.camel.impl.console; import java.io.StringReader; +import java.util.List; import java.util.Map; import javax.xml.parsers.DocumentBuilder; @@ -44,8 +45,8 @@ import org.apache.camel.util.json.JsonObject; @DevConsole(name = "processor-detail", description = "Show configured options for all processors in a route") public class ProcessorDetailDevConsole extends AbstractDevConsole { - @Metadata(label = "query", required = true, - description = "The route id to get processor details for", + @Metadata(label = "query", + description = "The route id to get processor details for (use * for all routes)", javaType = "java.lang.String") public static final String ROUTE_ID = "routeId"; @@ -58,10 +59,22 @@ public class ProcessorDetailDevConsole extends AbstractDevConsole { JsonObject json = doCallJson(options); StringBuilder sb = new StringBuilder(); - String routeId = json.getString("routeId"); + JsonArray routes = (JsonArray) json.get("routes"); + if (routes != null) { + for (Object routeObj : routes) { + appendRouteText(sb, (JsonObject) routeObj); + } + } else { + appendRouteText(sb, json); + } + return sb.toString(); + } + + private static void appendRouteText(StringBuilder sb, JsonObject routeJson) { + String routeId = routeJson.getString("routeId"); if (routeId != null) { sb.append(String.format("Route: %s%n", routeId)); - JsonArray processors = (JsonArray) json.get("processors"); + JsonArray processors = (JsonArray) routeJson.get("processors"); if (processors != null) { for (Object obj : processors) { JsonObject p = (JsonObject) obj; @@ -75,7 +88,6 @@ public class ProcessorDetailDevConsole extends AbstractDevConsole { } } } - return sb.toString(); } @Override @@ -98,15 +110,35 @@ public class ProcessorDetailDevConsole extends AbstractDevConsole { return root; } + if ("*".equals(routeId)) { + List<ManagedRouteMBean> managedRoutes = mcc.getManagedRoutes(); + if (managedRoutes == null || managedRoutes.isEmpty()) { + return root; + } + JsonArray routes = new JsonArray(); + for (ManagedRouteMBean mr : managedRoutes) { + JsonObject routeJson = buildRouteDetail(mr); + if (routeJson != null) { + routes.add(routeJson); + } + } + root.put("routes", routes); + return root; + } + ManagedRouteMBean mr = mcc.getManagedRoute(routeId); if (mr == null) { return root; } + return buildRouteDetail(mr); + } + private static JsonObject buildRouteDetail(ManagedRouteMBean mr) { + String routeId = mr.getRouteId(); + JsonObject root = new JsonObject(); root.put("routeId", routeId); JsonArray processors = new JsonArray(); - // add the "from" node (uses the route ID as its processor ID in the diagram) JsonObject fromEntry = new JsonObject(); fromEntry.put("id", routeId); fromEntry.put("type", "from"); @@ -114,7 +146,6 @@ public class ProcessorDetailDevConsole extends AbstractDevConsole { fromEntry.put("options", new JsonObject()); processors.add(fromEntry); - // parse the route XML to extract all processor elements with their configured options try { String xml = mr.dumpRouteAsXml(); if (xml != null && !xml.isBlank()) { diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java index de21d1a064d7..615f43f7907c 100644 --- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java +++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java @@ -789,8 +789,12 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C .resolveById("processor-detail"); if (dc != null) { String routeId = root.getString("routeId"); + if (routeId == null || routeId.isBlank()) { + routeId = root.getString("id"); + } JsonObject json - = (JsonObject) dc.call(DevConsole.MediaType.JSON, Map.of("routeId", routeId != null ? routeId : "")); + = (JsonObject) dc.call(DevConsole.MediaType.JSON, + Map.of("routeId", routeId != null ? routeId : "*")); LOG.trace("Updating output file: {}", outputFile); IOHelper.writeText(json.toJson(), outputFile); } else { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java index 04825e196bdf..152feb878a81 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java @@ -28,7 +28,9 @@ import java.util.stream.Collectors; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.dsl.jbang.core.common.ExampleHelper; import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; +import org.apache.camel.tooling.model.BaseOptionModel; import org.apache.camel.tooling.model.ComponentModel; +import org.apache.camel.tooling.model.EipModel; import org.apache.camel.util.IOHelper; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -236,6 +238,33 @@ public final class ToolRegistry { root -> root.put("id", routeId != null ? routeId : "*")); })); + register(tool("get_processor_detail", + "Show configured options for all processors in a route. " + + "Returns each processor's type, id, and configured options (attributes, expressions). " + + "Use includeDocs=true to enrich the response with documentation from the Camel catalog " + + "for each EIP option and component endpoint option.") + .param("routeId", "string", "Route ID to inspect (use * for all routes)", false) + .param("includeDocs", "string", + "If true, enrich each processor's options with documentation from the Camel catalog", false) + .executor((ctx, args) -> { + String routeId = args.get("routeId"); + String result = ctx.executeAction("processor-detail", + root -> root.put("routeId", routeId != null ? routeId : "*")); + String includeDocs = args.get("includeDocs"); + if ("true".equalsIgnoreCase(includeDocs)) { + try { + Object parsed = Jsoner.deserialize(result); + if (parsed instanceof JsonObject jo) { + enrichWithDocs(jo, ctx); + return jo.toJson(); + } + } catch (Exception e) { + // return unenriched result + } + } + return result; + })); + register(tool("get_top_processors", "Show top processor statistics: which processors are slowest and most active.") .executor((ctx, args) -> ctx.executeAction("top-processors", null))); @@ -979,6 +1008,115 @@ public final class ToolRegistry { })); } + // ---- Processor detail enrichment ---- + + private static void enrichWithDocs(JsonObject json, ToolContext ctx) { + CamelCatalog cat = ctx.catalog(); + JsonArray routes = (JsonArray) json.get("routes"); + if (routes != null) { + for (Object routeObj : routes) { + if (routeObj instanceof JsonObject routeJson) { + enrichRouteProcessors(routeJson, cat); + } + } + } else { + enrichRouteProcessors(json, cat); + } + } + + private static void enrichRouteProcessors(JsonObject routeJson, CamelCatalog cat) { + JsonArray processors = (JsonArray) routeJson.get("processors"); + if (processors == null) { + return; + } + for (Object obj : processors) { + if (!(obj instanceof JsonObject processor)) { + continue; + } + String type = processor.getString("type"); + if (type == null) { + continue; + } + JsonObject opts = processor.getMap("options"); + if ("from".equals(type) || "to".equals(type) || "toD".equals(type) || "wireTap".equals(type) + || "enrich".equals(type) || "pollEnrich".equals(type)) { + enrichComponentOptions(processor, opts, cat); + } else { + enrichEipOptions(processor, type, opts, cat); + } + } + } + + private static void enrichComponentOptions(JsonObject processor, JsonObject opts, CamelCatalog cat) { + String uri = processor.getString("endpointUri"); + if (uri == null) { + uri = opts != null ? opts.getString("uri") : null; + } + if (uri == null) { + return; + } + String scheme = uri.contains(":") ? uri.substring(0, uri.indexOf(':')) : uri; + ComponentModel model = cat.componentModel(scheme); + if (model == null) { + return; + } + processor.put("componentDescription", model.getDescription()); + + if (opts != null && model.getEndpointOptions() != null) { + JsonObject optionDocs = new JsonObject(); + for (BaseOptionModel opt : model.getEndpointOptions()) { + if (opts.containsKey(opt.getName())) { + optionDocs.put(opt.getName(), buildOptionDoc(opt)); + } + } + if (!optionDocs.isEmpty()) { + processor.put("optionDocs", optionDocs); + } + } + } + + private static void enrichEipOptions(JsonObject processor, String type, JsonObject opts, CamelCatalog cat) { + EipModel model = cat.eipModel(type); + if (model == null) { + return; + } + processor.put("eipDescription", model.getDescription()); + + if (opts != null && model.getOptions() != null) { + JsonObject optionDocs = new JsonObject(); + for (BaseOptionModel opt : model.getOptions()) { + if (opts.containsKey(opt.getName())) { + optionDocs.put(opt.getName(), buildOptionDoc(opt)); + } + } + if (!optionDocs.isEmpty()) { + processor.put("optionDocs", optionDocs); + } + } + } + + private static JsonObject buildOptionDoc(BaseOptionModel opt) { + JsonObject doc = new JsonObject(); + doc.put("description", opt.getDescription()); + doc.put("type", opt.getType()); + if (opt.getGroup() != null && !opt.getGroup().isEmpty()) { + doc.put("group", opt.getGroup()); + } + if (opt.getDefaultValue() != null) { + doc.put("defaultValue", opt.getDefaultValue().toString()); + } + if (opt.isRequired()) { + doc.put("required", true); + } + if (opt.isDeprecated()) { + doc.put("deprecated", true); + } + if (opt.getEnums() != null && !opt.getEnums().isEmpty()) { + doc.put("enum", opt.getEnums()); + } + return doc; + } + // ---- Utility ---- static boolean matchesFilter(String name, String title, String description, String filter) { diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java index e89bfa66ce23..3b0d4652a899 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RuntimeTools.java @@ -221,6 +221,24 @@ public class RuntimeTools { Map.of("routeId", routeId != null ? routeId : "*")); } + @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), + description = """ + Show configured options for all processors in a route. \ + Returns each processor's type, id, and configured options (attributes, expressions). \ + Use includeDocs=true to enrich the response with documentation from the Camel catalog \ + for each EIP option and component endpoint option.""") + public JsonObject camel_runtime_processor_detail( + @ToolArg(description = NAME_OR_PID_DESC) String nameOrPid, + @ToolArg(description = "Route ID to inspect (use * for all routes)") String routeId, + @ToolArg(description = "If true, enrich each processor's options with documentation from the Camel catalog") Boolean includeDocs) { + Map<String, String> args = new HashMap<>(); + args.put("routeId", routeId != null ? routeId : "*"); + if (includeDocs != null && includeDocs) { + args.put("includeDocs", "true"); + } + return delegateToRegistry("get_processor_detail", nameOrPid, args); + } + @Tool(annotations = @Tool.Annotations(readOnlyHint = false, destructiveHint = true, openWorldHint = false), description = "Control a route: start, stop, suspend, or resume it.") public JsonObject camel_runtime_route_control(
