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


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java:
##########
@@ -0,0 +1,105 @@
+/*
+ * 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.List;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * Shared execution context for AI tools. Wraps {@link RuntimeHelper} (process 
IPC) and {@link CamelCatalog} (component
+ * metadata). This class intentionally has no dependency on MCP or Quarkus so 
it can be used by both the Agent REPL and
+ * the MCP server.
+ */
+public class ToolContext {
+
+    private long pid = -1;
+    private CamelCatalog catalog;
+
+    public long pid() {
+        return pid;
+    }
+
+    public void selectProcess(long pid) {
+        this.pid = pid;
+    }
+
+    public boolean hasProcess() {
+        return pid >= 0;
+    }
+
+    public CamelCatalog catalog() {
+        if (catalog == null) {
+            catalog = new DefaultCamelCatalog();
+        }
+        return catalog;
+    }
+

Review Comment:
   This lazy initialization is not thread-safe. If `ToolContext` instances can 
be shared across threads (e.g., concurrent MCP server requests), two threads 
could create separate `DefaultCamelCatalog` instances. If single-threaded use 
is the intent, a brief comment would clarify that assumption. Otherwise, 
`synchronized` or a holder pattern would be safer.
   
   _This review was generated by an AI agent and may contain inaccuracies. 
Please verify all suggestions before applying._



##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java:
##########
@@ -0,0 +1,950 @@
+/*
+ * 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.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+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.ComponentModel;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+import static org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor.tool;
+
+/**
+ * Central registry of all shared AI tool descriptors. Tools registered here 
are available to both the Agent REPL
+ * ({@code AskTools}) and the MCP server. CLI-specific and file-system tools 
that depend on {@code CamelJBangMain} or
+ * direct filesystem access remain in {@code AskTools}.
+ */
+public final class ToolRegistry {
+
+    private static final List<ToolDescriptor> TOOLS = new ArrayList<>();
+    private static final Map<String, ToolDescriptor> BY_NAME = new 
LinkedHashMap<>();
+
+    static {
+        registerProcessTools();
+        registerRuntimeStatusTools();
+        registerRuntimeActionTools();
+        registerDevConsoleTools();
+        registerAnalysisTools();
+        registerCatalogTools();
+        registerExampleTools();
+    }
+
+    private ToolRegistry() {
+    }
+
+    public static List<ToolDescriptor> allTools() {
+        return Collections.unmodifiableList(TOOLS);
+    }
+
+    public static ToolDescriptor findTool(String name) {
+        return BY_NAME.get(name);
+    }
+
+    public static Object execute(String name, ToolContext ctx, Map<String, 
String> args) {
+        ToolDescriptor desc = BY_NAME.get(name);
+        if (desc == null) {
+            throw new ToolExecutionException("Unknown tool: " + name);
+        }
+        return desc.executor().execute(ctx, args);
+    }
+
+    private static void register(ToolDescriptor descriptor) {
+        TOOLS.add(descriptor);
+        BY_NAME.put(descriptor.name(), descriptor);
+    }
+
+    // ---- Process discovery and selection ----
+
+    private static void registerProcessTools() {
+        register(tool("list_processes",
+                "List all running Camel processes with their PID and name. Use 
this to discover available processes before selecting one.")
+                .executor((ctx, args) -> {
+                    List<RuntimeHelper.ProcessInfo> processes = 
ctx.discoverProcesses();
+                    if (processes.isEmpty()) {
+                        return "No running Camel processes found. Start one 
with: camel run <file>";
+                    }
+                    JsonObject response = new JsonObject();
+                    response.put("count", processes.size());
+                    List<JsonObject> list = new ArrayList<>();
+                    for (RuntimeHelper.ProcessInfo p : processes) {
+                        JsonObject entry = new JsonObject();
+                        entry.put("pid", p.pid());
+                        entry.put("name", p.name());
+                        entry.put("selected", p.pid() == ctx.pid());
+                        list.add(entry);
+                    }
+                    response.put("processes", list);
+                    if (!ctx.hasProcess()) {
+                        response.put("hint", "No process selected. Use 
select_process to connect to one.");
+                    }
+                    return response.toJson();
+                }));
+
+        register(tool("select_process",
+                "Select a running Camel process by name or PID to inspect. 
Required when multiple processes are running.")
+                .param("name", "string", "Name or PID of the Camel process to 
connect to", true)
+                .executor((ctx, args) -> {
+                    String name = args.get("name");
+                    if (name == null || name.isBlank()) {
+                        throw new ToolExecutionException("name or PID is 
required");
+                    }
+                    RuntimeHelper.ProcessInfo found = ctx.findProcess(name);
+                    if (found == null) {
+                        List<RuntimeHelper.ProcessInfo> processes = 
ctx.discoverProcesses();
+                        if (processes.isEmpty()) {
+                            return "No running Camel processes found.";
+                        }
+                        StringBuilder sb
+                                = new StringBuilder("No process found 
matching: " + name + ". Available:\n");
+                        processes.forEach(
+                                p -> sb.append("  ").append(p.name()).append(" 
(PID ").append(p.pid()).append(")\n"));
+                        return sb.toString();
+                    }
+                    ctx.selectProcess(found.pid());
+                    return "Connected to " + found.name() + " (PID " + 
found.pid()
+                           + "). Runtime tools are now active.";
+                }));
+    }
+
+    // ---- Runtime status (simple read-section) tools ----
+
+    private static void registerRuntimeStatusTools() {
+        registerStatusTool("get_context", "context",
+                "Get Camel context info: name, version, state, uptime, route 
count, exchange statistics.");
+        registerStatusTool("get_routes", "routes",
+                "List all routes with their state, uptime, messages processed, 
last error, and throughput.");
+        registerStatusTool("get_health", "healthChecks",
+                "Get health check status for the Camel application.");
+        registerStatusTool("get_endpoints", "endpoints",
+                "List all endpoints registered in the Camel context with URIs 
and usage stats.");
+        registerStatusTool("get_inflight", "inflight",
+                "Show currently in-flight exchanges (messages being 
processed).");
+        registerStatusTool("get_blocked", "blocked",
+                "Show blocked exchanges that are stuck or waiting.");
+        registerStatusTool("get_consumers", "consumers",
+                "Show consumer statistics (polling and event-driven 
consumers).");
+        registerStatusTool("get_properties", "properties",
+                "Show configuration properties of the running Camel 
application.");
+        registerStatusTool("get_memory", "memory",
+                "Show JVM memory usage (heap/non-heap), garbage collection 
stats, and thread counts.");
+        registerStatusTool("get_variables", "variables",
+                "Show exchange variables in the Camel context.");
+        registerStatusTool("get_services", "services",
+                "Show services registered in the Camel service registry.");
+    }
+
+    private static void registerStatusTool(String name, String section, String 
description) {
+        register(tool(name, description)
+                .executor((ctx, args) -> ctx.readStatus(section)));
+    }
+
+    // ---- Runtime action tools (with parameters) ----
+
+    private static void registerRuntimeActionTools() {
+        register(tool("get_errors",
+                "Get captured routing errors from the running Camel 
application.")
+                .executor((ctx, args) -> {
+                    JsonObject errors = ctx.readErrorFile();
+                    return errors != null ? errors.toJson() : "No errors 
captured.";
+                }));
+
+        register(tool("get_history",
+                "Get the message history trace of the last completed 
exchange.")
+                .executor((ctx, args) -> {
+                    JsonObject history = ctx.readHistoryFile();
+                    return history != null ? history.toJson() : "No message 
history available.";
+                }));
+
+        register(tool("get_route_source",
+                "Get the source code of routes. Use filter to limit by 
filename (supports wildcards).")
+                .param("filter", "string",
+                        "Filter source files by name (supports wildcards). Use 
* for all.", false)
+                .executor((ctx, args) -> {
+                    String filter = args.get("filter");
+                    return ctx.executeAction("source",
+                            root -> root.put("filter", filter != null ? filter 
: "*"));
+                }));
+
+        register(tool("get_route_dump",
+                "Dump route definitions in XML or YAML format.")
+                .param("routeId", "string", "Route ID to dump (use * for all 
routes)", false)
+                .param("format", "string", "Output format: xml or yaml 
(default: yaml)", false)
+                .executor((ctx, args) -> {
+                    String routeId = args.get("routeId");
+                    String format = args.get("format");
+                    return ctx.executeAction("route-dump", root -> {
+                        root.put("id", routeId != null ? routeId : "*");
+                        root.put("format", format != null ? format : "yaml");
+                    });
+                }));
+
+        register(tool("get_route_structure",
+                "Show the route structure as a tree of processors.")
+                .param("routeId", "string", "Route ID to inspect (use * for 
all routes)", false)
+                .executor((ctx, args) -> {
+                    String routeId = args.get("routeId");
+                    return ctx.executeAction("route-structure",
+                            root -> root.put("id", routeId != null ? routeId : 
"*"));
+                }));
+
+        register(tool("get_top_processors",
+                "Show top processor statistics: which processors are slowest 
and most active.")
+                .executor((ctx, args) -> ctx.executeAction("top-processors", 
null)));
+
+        register(tool("get_route_topology",
+                "Get the inter-route topology showing how routes connect to 
each other and to external endpoints.")
+                .executor((ctx, args) -> ctx.executeAction("route-topology", 
root -> {
+                    root.put("metric", "true");
+                    root.put("external", "true");
+                })));
+
+        register(tool("trace_control",
+                "Enable, disable, or dump message tracing.")
+                .param("action", "string", "Action: enable, disable, or dump", 
true)
+                .executor((ctx, args) -> {
+                    String action = args.get("action");
+                    if (action == null) {
+                        throw new ToolExecutionException("action is required 
(enable, disable, dump)");
+                    }
+                    return ctx.executeAction("trace", root -> {
+                        switch (action.toLowerCase()) {
+                            case "enable" -> root.put("enabled", "true");
+                            case "disable" -> root.put("enabled", "false");
+                            case "dump" -> root.put("dump", "true");
+                            default -> root.put("enabled", action);
+                        }
+                    });
+                }));
+
+        register(tool("send_message",
+                "Send a test message to a Camel endpoint in the running 
application.")
+                .param("endpoint", "string",
+                        "Endpoint URI to send to (e.g., direct:myRoute, 
seda:queue)", true)
+                .param("body", "string", "Message body to send", false)
+                .param("headers", "string",
+                        "Message headers as key=value pairs separated by 
newlines", false)
+                .readOnly(false).destructive(false)
+                .executor((ctx, args) -> {
+                    String endpoint = args.get("endpoint");
+                    if (endpoint == null || endpoint.isBlank()) {
+                        throw new ToolExecutionException("endpoint is 
required");
+                    }
+                    return ctx.sendMessage(endpoint, args.get("body"), 
args.get("headers")).toJson();
+                }));
+
+        register(tool("eval_expression",
+                "Evaluate a Camel expression in the given language against the 
running context.")
+                .param("language", "string",
+                        "Expression language (e.g., simple, jsonpath, xpath, 
jq)", true)
+                .param("expression", "string", "Expression to evaluate", true)
+                .executor((ctx, args) -> {
+                    String language = args.get("language");
+                    String expression = args.get("expression");
+                    if (language == null || language.isBlank()) {
+                        throw new ToolExecutionException("language is 
required");
+                    }
+                    if (expression == null || expression.isBlank()) {
+                        throw new ToolExecutionException("expression is 
required");
+                    }
+                    return ctx.executeAction("eval", root -> {
+                        root.put("language", language);
+                        root.put("predicate", "false");
+                        root.put("template", Jsoner.escape(expression));
+                    });
+                }));
+
+        register(tool("browse_endpoint",
+                "Browse messages in a Camel endpoint (e.g., browse messages 
queued in a SEDA endpoint).")
+                .param("endpoint", "string", "Endpoint URI to browse (e.g., 
seda:queue)", true)
+                .param("limit", "string",
+                        "Maximum number of messages to return (default: 50)", 
false)
+                .executor((ctx, args) -> {
+                    String endpoint = args.get("endpoint");
+                    if (endpoint == null || endpoint.isBlank()) {
+                        throw new ToolExecutionException("endpoint is 
required");
+                    }
+                    int limit = 50;
+                    String limitStr = args.get("limit");
+                    if (limitStr != null && !limitStr.isBlank()) {
+                        try {
+                            limit = Integer.parseInt(limitStr);
+                        } catch (NumberFormatException e) {
+                            // use default
+                        }
+                    }
+                    int browseLimit = limit;
+                    return ctx.executeAction("browse", root -> {
+                        root.put("filter", endpoint);
+                        root.put("limit", browseLimit);
+                    });
+                }));
+
+        register(tool("get_thread_dump",
+                "Get a JVM thread dump showing thread names, states, and stack 
traces.")
+                .executor((ctx, args) -> ctx.executeAction("thread-dump", 
null)));
+
+        // Route control
+        registerRouteControlTool("stop_route", "stop",
+                "Gracefully stop a route. The route will finish processing 
in-flight exchanges before stopping.");
+        registerRouteControlTool("start_route", "start", "Start a stopped 
route.");
+        registerRouteControlTool("suspend_route", "suspend",
+                "Suspend a route (pauses the consumer but keeps the route 
loaded).");
+        registerRouteControlTool("resume_route", "resume", "Resume a suspended 
route.");
+
+        register(tool("stop_application",
+                "Gracefully stop the Camel application. Finishes in-flight 
exchanges then shuts down cleanly.")
+                .readOnly(false).destructive(true)
+                .executor((ctx, args) -> ctx.stopApplication()));
+    }
+
+    private static void registerRouteControlTool(String name, String command, 
String description) {
+        register(tool(name, description)
+                .param("routeId", "string", "The ID of the route", true)
+                .readOnly(false).destructive(command.equals("stop"))
+                .executor((ctx, args) -> {
+                    String routeId = args.get("routeId");
+                    if (routeId == null || routeId.isBlank()) {
+                        throw new ToolExecutionException("routeId is 
required");
+                    }
+                    return ctx.executeAction("route", root -> {
+                        root.put("id", routeId);
+                        root.put("command", command);
+                    });
+                }));
+    }
+
+    // ---- DevConsole tools (data available in TUI, now surfaced for AI) ----
+
+    private static void registerDevConsoleTools() {
+        register(tool("get_circuit_breakers",
+                "Get circuit breaker status from the running Camel 
application. Shows state (CLOSED/OPEN/HALF_OPEN), "
+                                              + "call counts, failure rates, 
and not-permitted calls for each breaker. "
+                                              + "Supports Resilience4j and 
MicroProfile Fault Tolerance implementations.")
+                .executor((ctx, args) -> {
+                    JsonObject root = ctx.readFullStatus();
+                    if (root == null) {
+                        return "No status available.";
+                    }
+                    // Circuit breaker data can be under resilience4j, 
fault-tolerance, or circuit-breaker sections
+                    JsonArray allBreakers = new JsonArray();
+                    collectCircuitBreakers(root, "resilience4j", allBreakers);
+                    collectCircuitBreakers(root, "fault-tolerance", 
allBreakers);
+                    collectCircuitBreakers(root, "circuit-breaker", 
allBreakers);
+
+                    JsonObject response = new JsonObject();
+                    response.put("count", allBreakers.size());
+                    response.put("circuitBreakers", allBreakers);
+                    if (allBreakers.isEmpty()) {
+                        response.put("hint",
+                                "No circuit breakers found. Add 
camel-resilience4j or camel-microprofile-fault-tolerance to use circuit 
breakers.");
+                    }
+                    return response.toJson();
+                }));
+
+        register(tool("get_startup_steps",
+                "Get startup recorder steps showing component initialization 
timing. "
+                                           + "Shows each startup step with 
duration, level, and type. "
+                                           + "Useful for diagnosing slow 
application startup. "
+                                           + "Requires startup recording to be 
enabled (camel.main.startup-recorder=true).")
+                .executor((ctx, args) -> ctx.executeAction("startup-recorder", 
null)));
+
+        register(tool("get_datasources",
+                "Get datasource connection pool status. Shows active, idle, 
and total connections, "
+                                         + "max pool size, and waiting threads 
for each datasource. "
+                                         + "Supports HikariCP and Agroal 
connection pools.")
+                .executor((ctx, args) -> ctx.readStatus("dataSources")));
+
+        register(tool("sql_query",
+                "Execute a SQL query against a datasource in the running Camel 
application. "
+                                   + "Returns column names, rows, and 
execution time. "
+                                   + "Only works if the application has a 
datasource configured. "
+                                   + "CAUTION: This executes real SQL against 
the application's database.")
+                .param("sql", "string", "SQL query to execute (e.g., SELECT * 
FROM users LIMIT 10)", true)
+                .param("datasource", "string",
+                        "Datasource name to query (optional if only one 
datasource exists)", false)
+                .param("maxRows", "string", "Maximum rows to return (default: 
100)", false)
+                .readOnly(false)

Review Comment:
   The `sql_query` tool can execute arbitrary SQL — including DDL/DML (`DROP 
TABLE`, `DELETE`, `UPDATE`). It is currently marked `.readOnly(false)` but 
`destructive` defaults to `false`. Given the blast radius, this should be 
`.destructive(true)` to correctly signal risk to MCP clients.
   
   ```suggestion
                   .readOnly(false).destructive(true)
   ```
   
   _This review was generated by an AI agent and may contain inaccuracies. 
Please verify all suggestions before applying._



-- 
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