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]
