This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 95d5e757391a CAMEL-23873: Introduce shared ToolRegistry for AI tools
(#24337)
95d5e757391a is described below
commit 95d5e757391afb1a1930e99b5b2d00aae6fa494b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Jul 2 09:43:29 2026 +0200
CAMEL-23873: Introduce shared ToolRegistry for AI tools (#24337)
Add a shared ToolRegistry in camel-jbang-core that serves as the single
source of truth for AI tool definitions and execution logic, eliminating
duplication between the MCP server and Agent REPL.
New classes:
- ToolRegistry: central registry of 50 shared tool descriptors with
ToolDescriptor, ToolContext, and ToolExecutor functional interface
- ToolDescriptor: builder-style descriptor bundling metadata + executor
- ToolContext: shared execution context (RuntimeHelper + CamelCatalog)
- ToolExecutionException: unchecked exception for tool failures
Refactored consumers:
- AskTools (Agent REPL): delegates to ToolRegistry for shared tools,
keeps CLI/file tools locally (995 -> 478 lines)
- RuntimeTools (MCP server): thin MCP wrappers delegating to ToolRegistry
via delegateToRegistry() helper. Only camel_runtime_processes and
camel_runtime_receive keep their own implementations.
New tools added to ToolRegistry:
- DevConsole: circuit breakers, startup steps, datasources, spans, metrics
- Analysis: route analysis with anti-pattern hints, EIP stats,
config drift detection
- Catalog: dataformats, languages, EIP doc (previously MCP-only)
New MCP prompts:
- camel_diagnose_route: 7-step guided diagnosis workflow
- camel_optimize_route: 6-step guided optimization workflow
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../camel/dsl/jbang/core/commands/AskTools.java | 673 ++------------
.../dsl/jbang/core/commands/ai/ToolContext.java | 121 +++
.../dsl/jbang/core/commands/ai/ToolDescriptor.java | 102 +++
.../core/commands/ai/ToolExecutionException.java | 31 +
.../dsl/jbang/core/commands/ai/ToolRegistry.java | 972 +++++++++++++++++++++
.../jbang/core/commands/ai/ToolRegistryTest.java | 166 ++++
.../jbang/core/commands/mcp/PromptDefinitions.java | 173 ++++
.../dsl/jbang/core/commands/mcp/RuntimeTools.java | 234 ++---
8 files changed, 1746 insertions(+), 726 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
index aa4097c77503..89902c6c7766 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/AskTools.java
@@ -27,15 +27,13 @@ import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.apache.camel.catalog.CamelCatalog;
-import org.apache.camel.catalog.DefaultCamelCatalog;
-import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolDescriptor;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
import org.apache.camel.dsl.jbang.core.common.Printer;
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.JsonObject;
import org.apache.camel.util.json.Jsoner;
@@ -43,18 +41,24 @@ import org.apache.camel.util.json.Jsoner;
/**
* Shared tool definitions and execution logic for the Camel AI assistant.
Used by both the {@code camel ask} CLI
* command and the TUI AI panel.
+ *
+ * <p>
+ * Delegates shared tool execution (process discovery, runtime inspection,
catalog, examples) to
+ * {@link ToolRegistry}/{@link ToolContext}. CLI-specific and file-system
tools remain here because they depend on
+ * {@link CamelJBangMain} and direct filesystem access.
*/
public class AskTools {
- private static final String NO_PROCESS
- = "No running Camel process connected. Start one with: camel run
<file>";
-
private long targetPid;
- private CamelCatalog catalog;
+ private final ToolContext ctx;
private volatile List<JsonObject> commandMetadataCache;
public AskTools(long targetPid) {
this.targetPid = targetPid;
+ this.ctx = new ToolContext();
+ if (targetPid >= 0) {
+ ctx.selectProcess(targetPid);
+ }
}
public long getTargetPid() {
@@ -63,6 +67,9 @@ public class AskTools {
public void setTargetPid(long targetPid) {
this.targetPid = targetPid;
+ if (targetPid >= 0) {
+ ctx.selectProcess(targetPid);
+ }
}
// ---- Tool definitions ----
@@ -70,197 +77,22 @@ public class AskTools {
public List<LlmClient.ToolDef> buildToolDefinitions() {
List<LlmClient.ToolDef> tools = new ArrayList<>();
- tools.add(new LlmClient.ToolDef(
- "list_processes",
- "List all running Camel processes with their PID and name. Use
this to discover available processes before selecting one.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "select_process",
- "Select a running Camel process by name or PID to inspect.
Required when multiple processes are running. After selection, all runtime
tools (get_routes, get_context, etc.) will target this process.",
- objectParams(Map.of(
- "name", stringProp("Name or PID of the Camel process
to connect to")))));
-
- tools.add(new LlmClient.ToolDef(
- "get_context",
- "Get Camel context info: name, version, state, uptime, route
count, exchange statistics.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_routes",
- "List all routes with their state, uptime, messages processed,
last error, and throughput.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_health",
- "Get health check status for the Camel application.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_endpoints",
- "List all endpoints registered in the Camel context with URIs
and usage stats.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_inflight",
- "Show currently in-flight exchanges (messages being
processed).",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_blocked",
- "Show blocked exchanges that are stuck or waiting.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_consumers",
- "Show consumer statistics (polling and event-driven
consumers).",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_properties",
- "Show configuration properties of the running Camel
application.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_memory",
- "Show JVM memory usage (heap/non-heap), garbage collection
stats, and thread counts.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_errors",
- "Get captured routing errors from the running Camel
application. Returns error details including exception, exchange context, and
route information.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_history",
- "Get the message history trace of the last completed exchange.
Shows the route path, processors visited, headers, body, and timing.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_variables",
- "Show exchange variables in the Camel context.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_services",
- "Show services registered in the Camel service registry.",
- emptyParams()));
-
- tools.add(new LlmClient.ToolDef(
- "get_route_source",
- "Get the source code of routes. Use filter to limit by
filename (supports wildcards).",
- objectParams(Map.of(
- "filter", stringProp("Filter source files by name
(supports wildcards). Use * for all.")))));
- tools.add(new LlmClient.ToolDef(
- "get_route_dump",
- "Dump route definitions in XML or YAML format.",
- objectParams(Map.of(
- "routeId", stringProp("Route ID to dump (use * for all
routes)"),
- "format", stringProp("Output format: xml or yaml
(default: yaml)")))));
- tools.add(new LlmClient.ToolDef(
- "get_route_structure",
- "Show the route structure as a tree of processors.",
- objectParams(Map.of(
- "routeId", stringProp("Route ID to inspect (use * for
all routes)")))));
- tools.add(new LlmClient.ToolDef(
- "get_top_processors",
- "Show top processor statistics: which processors are slowest
and most active.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "get_route_topology",
- "Get the inter-route topology showing how routes connect to
each other and to external endpoints.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "trace_control",
- "Enable, disable, or dump message tracing.",
- objectParams(Map.of(
- "action", stringProp("Action: enable, disable, or
dump")))));
-
- tools.add(new LlmClient.ToolDef(
- "send_message",
- "Send a test message to a Camel endpoint in the running
application.",
- objectParams(Map.of(
- "endpoint", stringProp("Endpoint URI to send to (e.g.,
direct:myRoute, seda:queue)"),
- "body", stringProp("Message body to send"),
- "headers", stringProp("Message headers as key=value
pairs separated by newlines")))));
- tools.add(new LlmClient.ToolDef(
- "eval_expression",
- "Evaluate a Camel expression in the given language (e.g.,
simple, jsonpath, xpath) against the running context.",
- objectParams(Map.of(
- "language", stringProp("Expression language (e.g.,
simple, jsonpath, xpath, jq)"),
- "expression", stringProp("Expression to evaluate")))));
- tools.add(new LlmClient.ToolDef(
- "browse_endpoint",
- "Browse messages in a Camel endpoint (e.g., browse messages
queued in a SEDA endpoint).",
- objectParams(Map.of(
- "endpoint", stringProp("Endpoint URI to browse (e.g.,
seda:queue)"),
- "limit", stringProp("Maximum number of messages to
return (default: 50)")))));
- tools.add(new LlmClient.ToolDef(
- "get_sql_trace",
- "Get traced SQL query executions flowing through camel-sql and
camel-jdbc components. "
- + "Returns per-query timing, row counts,
category (SELECT/INSERT/UPDATE/DELETE), "
- + "route ID, and failure status. Includes
summary statistics: total queries, "
- + "average time, slowest time, slow count
(>=100ms), and failed count. "
- + "Use to identify slow queries, fastest
queries, most frequent queries, "
- + "and failed SQL executions.",
- emptyParams()));
- tools.add(new LlmClient.ToolDef(
- "execute_sql",
- "Execute a SQL query against a DataSource in the running Camel
application. "
- + "Returns structured JSON with columns, rows,
and metadata for SELECT queries, "
- + "or an update count for
INSERT/UPDATE/DELETE.",
- objectParams(Map.of(
- "query", stringProp("The SQL query to execute"),
- "datasource", stringProp("Name of the DataSource bean
(auto-detected if only one exists)"),
- "maxRows", stringProp("Maximum number of rows to
return (default: 100)")))));
- tools.add(new LlmClient.ToolDef(
- "get_thread_dump",
- "Get a JVM thread dump showing thread names, states, and stack
traces.",
- emptyParams()));
-
- tools.add(new LlmClient.ToolDef(
- "stop_route",
- "Gracefully stop a route. The route will finish processing
in-flight exchanges before stopping.",
- objectParams(Map.of(
- "routeId", stringProp("The ID of the route to
stop")))));
- tools.add(new LlmClient.ToolDef(
- "start_route",
- "Start a stopped route.",
- objectParams(Map.of(
- "routeId", stringProp("The ID of the route to
start")))));
- tools.add(new LlmClient.ToolDef(
- "suspend_route",
- "Suspend a route (pauses the consumer but keeps the route
loaded).",
- objectParams(Map.of(
- "routeId", stringProp("The ID of the route to
suspend")))));
- tools.add(new LlmClient.ToolDef(
- "resume_route",
- "Resume a suspended route.",
- objectParams(Map.of(
- "routeId", stringProp("The ID of the route to
resume")))));
-
- tools.add(new LlmClient.ToolDef(
- "stop_application",
- "Gracefully stop the Camel application. The application will
finish processing in-flight exchanges and shut down cleanly. Use this instead
of kill.",
- emptyParams()));
-
- tools.add(new LlmClient.ToolDef(
- "catalog_components",
- "Search the Camel component catalog by name or label. Returns
component name, title, description, and labels.",
- objectParams(Map.of(
- "filter", stringProp("Filter by name, title, or
description (case-insensitive substring)"),
- "label", stringProp("Filter by category label (e.g.,
cloud, messaging, database, file)")))));
- tools.add(new LlmClient.ToolDef(
- "catalog_component_doc",
- "Get detailed documentation for a Camel component: URI syntax
and endpoint options.",
- objectParams(Map.of(
- "component", stringProp("Component name (e.g., kafka,
http, file, timer)")))));
- tools.add(new LlmClient.ToolDef(
- "catalog_eips",
- "Search EIPs (Enterprise Integration Patterns) like split,
aggregate, filter, choice, multicast.",
- objectParams(Map.of(
- "filter", stringProp("Filter by name, title, or
description (case-insensitive substring)")))));
-
- tools.add(new LlmClient.ToolDef(
- "list_examples",
- "List available Camel CLI examples. Returns name, title,
description, difficulty level, and tags.",
- objectParams(Map.of(
- "filter", stringProp("Filter by name, description, or
tag (case-insensitive)"),
- "level", stringProp("Filter by difficulty: beginner,
intermediate, or advanced")))));
- tools.add(new LlmClient.ToolDef(
- "get_example_file",
- "Get the content of a file from a bundled Camel CLI example.
Use list_examples first to find available examples.",
- objectParams(Map.of(
- "example", stringProp("Example name (e.g., timer-log,
rest-api, circuit-breaker)"),
- "file", stringProp("File name within the example
(e.g., route.camel.yaml)")))));
+ // Build definitions from the shared registry
+ for (ToolDescriptor td : ToolRegistry.allTools()) {
+ JsonObject params;
+ if (td.params().isEmpty()) {
+ params = emptyParams();
+ } else {
+ Map<String, JsonObject> props = new LinkedHashMap<>();
+ for (ToolDescriptor.Param p : td.params()) {
+ props.put(p.name(), stringProp(p.description()));
+ }
+ params = objectParams(props);
+ }
+ tools.add(new LlmClient.ToolDef(td.name(), td.description(),
params));
+ }
+ // CLI tools (AskTools-specific, depend on CamelJBangMain)
tools.add(new LlmClient.ToolDef(
"cli_list_commands",
"List available Camel CLI commands. Returns command names and
descriptions. Use filter to narrow results.",
@@ -278,6 +110,7 @@ public class AskTools {
"command", stringProp(
"The full command line to execute (e.g., 'get
error --diagram', 'catalog component --filter=kafka')")))));
+ // File tools (AskTools-specific, depend on filesystem access)
tools.add(new LlmClient.ToolDef(
"list_files",
"List files in a directory (up to 2 levels deep). Defaults to
current working directory.",
@@ -302,62 +135,31 @@ public class AskTools {
public String executeTool(String name, JsonObject args) {
try {
+ // Try shared registry first
+ ToolDescriptor td = ToolRegistry.findTool(name);
+ if (td != null) {
+ // Convert JsonObject args to Map<String, String> for the
registry
+ Map<String, String> argMap = new LinkedHashMap<>();
+ if (args != null) {
+ for (String key : args.keySet()) {
+ Object val = args.get(key);
+ if (val != null) {
+ argMap.put(key, val.toString());
+ }
+ }
+ }
+
+ // Special handling: select_process must also update our
targetPid
+ Object result = ToolRegistry.execute(name, ctx, argMap);
+ if ("select_process".equals(name) && ctx.hasProcess()) {
+ this.targetPid = ctx.pid();
+ }
+
+ return result != null ? result.toString() : "";
+ }
+
+ // Fall through to CLI and file tools
return switch (name) {
- case "list_processes" -> executeListProcesses();
- case "select_process" -> executeSelectProcess(args);
- case "get_context" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "context");
- case "get_routes" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "routes");
- case "get_health" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "healthChecks");
- case "get_endpoints" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "endpoints");
- case "get_inflight" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "inflight");
- case "get_blocked" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "blocked");
- case "get_consumers" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "consumers");
- case "get_properties" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "properties");
- case "get_memory" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "memory");
- case "get_errors" -> targetPid < 0 ? NO_PROCESS :
executeGetErrors();
- case "get_history" -> targetPid < 0 ? NO_PROCESS :
executeGetHistory();
- case "get_variables" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "variables");
- case "get_services" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "services");
- case "get_route_source" -> targetPid < 0 ? NO_PROCESS :
executeRouteSource(args);
- case "get_route_dump" -> targetPid < 0 ? NO_PROCESS :
executeRouteDump(args);
- case "get_route_structure" -> targetPid < 0 ? NO_PROCESS :
executeRouteStructure(args);
- case "get_top_processors" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.executeAction(targetPid, "top-processors", null);
- case "get_route_topology" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.executeAction(targetPid, "route-topology", root -> {
- root.put("metric", "true");
- root.put("external", "true");
- });
- case "trace_control" -> targetPid < 0 ? NO_PROCESS :
executeTraceControl(args);
- case "send_message" -> targetPid < 0 ? NO_PROCESS :
executeSendMessage(args);
- case "eval_expression" -> targetPid < 0 ? NO_PROCESS :
executeEvalExpression(args);
- case "browse_endpoint" -> targetPid < 0 ? NO_PROCESS :
executeBrowseEndpoint(args);
- case "get_sql_trace" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.readStatusSection(targetPid, "sqlTrace");
- case "execute_sql" -> targetPid < 0 ? NO_PROCESS :
executeSQL(args);
- case "get_thread_dump" ->
- targetPid < 0 ? NO_PROCESS :
RuntimeHelper.executeAction(targetPid, "thread-dump", null);
- case "stop_route" -> targetPid < 0 ? NO_PROCESS :
executeRouteCommand(args, "stop");
- case "start_route" -> targetPid < 0 ? NO_PROCESS :
executeRouteCommand(args, "start");
- case "suspend_route" -> targetPid < 0 ? NO_PROCESS :
executeRouteCommand(args, "suspend");
- case "resume_route" -> targetPid < 0 ? NO_PROCESS :
executeRouteCommand(args, "resume");
- case "stop_application" -> targetPid < 0 ? NO_PROCESS :
RuntimeHelper.stopApplication(targetPid);
- case "catalog_components" -> executeCatalogComponents(args);
- case "catalog_component_doc" ->
executeCatalogComponentDoc(args);
- case "catalog_eips" -> executeCatalogEips(args);
- case "list_examples" -> executeListExamples(args);
- case "get_example_file" -> executeGetExampleFile(args);
case "cli_list_commands" -> executeCliListCommands(args);
case "cli_command_help" -> executeCliCommandHelp(args);
case "cli_exec" -> executeCliExec(args);
@@ -411,349 +213,7 @@ public class AskTools {
return sb.toString();
}
- // ---- Runtime tool execution ----
-
- private String executeListProcesses() {
- List<RuntimeHelper.ProcessInfo> processes =
RuntimeHelper.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() == targetPid);
- list.add(entry);
- }
- response.put("processes", list);
- if (targetPid < 0) {
- response.put("hint", "No process selected. Use select_process to
connect to one.");
- }
- return response.toJson();
- }
-
- private String executeSelectProcess(JsonObject args) {
- String name = args.getString("name");
- if (name == null || name.isBlank()) {
- return "Error: name or PID is required";
- }
- RuntimeHelper.ProcessInfo found = RuntimeHelper.findProcess(name);
- if (found == null) {
- List<RuntimeHelper.ProcessInfo> processes =
RuntimeHelper.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();
- }
- targetPid = found.pid();
- return "Connected to " + found.name() + " (PID " + found.pid() + ").
Runtime tools are now active.";
- }
-
- private String executeRouteSource(JsonObject args) {
- String filter = args.getString("filter");
- return RuntimeHelper.executeAction(targetPid, "source",
- root -> root.put("filter", filter != null ? filter : "*"));
- }
-
- private String executeRouteDump(JsonObject args) {
- String routeId = args.getString("routeId");
- String format = args.getString("format");
- return RuntimeHelper.executeAction(targetPid, "route-dump", root -> {
- root.put("id", routeId != null ? routeId : "*");
- root.put("format", format != null ? format : "yaml");
- });
- }
-
- private String executeRouteStructure(JsonObject args) {
- String routeId = args.getString("routeId");
- return RuntimeHelper.executeAction(targetPid, "route-structure",
- root -> root.put("id", routeId != null ? routeId : "*"));
- }
-
- private String executeTraceControl(JsonObject args) {
- String action = args.getString("action");
- if (action == null) {
- return "Error: action is required (enable, disable, dump)";
- }
- return RuntimeHelper.executeAction(targetPid, "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);
- }
- });
- }
-
- private String executeRouteCommand(JsonObject args, String command) {
- String routeId = args.getString("routeId");
- if (routeId == null || routeId.isBlank()) {
- return "Error: routeId is required";
- }
- return RuntimeHelper.executeAction(targetPid, "route", root -> {
- root.put("id", routeId);
- root.put("command", command);
- });
- }
-
- private String executeGetErrors() {
- JsonObject errors = RuntimeHelper.readErrorFile(targetPid);
- if (errors == null) {
- return "No errors captured.";
- }
- return errors.toJson();
- }
-
- private String executeGetHistory() {
- JsonObject history = RuntimeHelper.readHistoryFile(targetPid);
- if (history == null) {
- return "No message history available.";
- }
- return history.toJson();
- }
-
- private String executeSendMessage(JsonObject args) {
- String endpoint = args.getString("endpoint");
- if (endpoint == null || endpoint.isBlank()) {
- return "Error: endpoint is required";
- }
- String body = args.getString("body");
- String headers = args.getString("headers");
- JsonObject result = RuntimeHelper.sendMessage(targetPid, endpoint,
body, headers);
- return result.toJson();
- }
-
- private String executeEvalExpression(JsonObject args) {
- String language = args.getString("language");
- String expression = args.getString("expression");
- if (language == null || language.isBlank()) {
- return "Error: language is required";
- }
- if (expression == null || expression.isBlank()) {
- return "Error: expression is required";
- }
- return RuntimeHelper.executeAction(targetPid, "eval", root -> {
- root.put("language", language);
- root.put("predicate", "false");
- root.put("template", Jsoner.escape(expression));
- });
- }
-
- private String executeBrowseEndpoint(JsonObject args) {
- String endpoint = args.getString("endpoint");
- if (endpoint == null || endpoint.isBlank()) {
- return "Error: endpoint is required";
- }
- String limitStr = args.getString("limit");
- int limit = 50;
- if (limitStr != null && !limitStr.isBlank()) {
- try {
- limit = Integer.parseInt(limitStr);
- } catch (NumberFormatException e) {
- // use default
- }
- }
- int browseLimit = limit;
- return RuntimeHelper.executeAction(targetPid, "browse", root -> {
- root.put("filter", endpoint);
- root.put("limit", browseLimit);
- });
- }
-
- private String executeSQL(JsonObject args) {
- String sql = args.getString("query");
- if (sql == null || sql.isBlank()) {
- return "Error: 'query' parameter is required";
- }
- String datasource = args.getString("datasource");
- int maxRows = 100;
- String maxRowsStr = args.getString("maxRows");
- if (maxRowsStr != null) {
- try {
- maxRows = Integer.parseInt(maxRowsStr);
- } catch (NumberFormatException e) {
- // use default
- }
- }
- JsonObject result = RuntimeHelper.executeSqlQuery(targetPid, sql,
datasource, maxRows, 30);
- return Jsoner.serialize(result);
- }
-
- // ---- Catalog tools ----
-
- private String executeCatalogComponents(JsonObject args) {
- String filter = args.getString("filter");
- String label = args.getString("label");
- CamelCatalog cat = getCatalog();
-
- List<JsonObject> results = cat.findComponentNames().stream()
- .map(cat::componentModel)
- .filter(m -> m != null)
- .filter(m -> matchesFilter(m.getScheme(), m.getTitle(),
m.getDescription(), filter))
- .filter(m -> label == null || label.isBlank()
- || (m.getLabel() != null &&
m.getLabel().toLowerCase().contains(label.toLowerCase())))
- .limit(20)
- .map(m -> {
- JsonObject jo = new JsonObject();
- jo.put("name", m.getScheme());
- jo.put("title", m.getTitle());
- jo.put("description", m.getDescription());
- jo.put("label", m.getLabel());
- return jo;
- })
- .collect(Collectors.toList());
-
- JsonObject response = new JsonObject();
- response.put("count", results.size());
- response.put("components", results);
- return response.toJson();
- }
-
- private String executeCatalogComponentDoc(JsonObject args) {
- String component = args.getString("component");
- if (component == null || component.isBlank()) {
- return "Error: component name is required";
- }
- CamelCatalog cat = getCatalog();
- ComponentModel model = cat.componentModel(component);
- if (model == null) {
- return "Component not found: " + component;
- }
-
- JsonObject response = new JsonObject();
- response.put("name", model.getScheme());
- response.put("title", model.getTitle());
- response.put("description", model.getDescription());
- response.put("syntax", model.getSyntax());
- response.put("consumerOnly", model.isConsumerOnly());
- response.put("producerOnly", model.isProducerOnly());
-
- List<JsonObject> options = new ArrayList<>();
- if (model.getEndpointOptions() != null) {
- model.getEndpointOptions().stream()
- .filter(opt -> !opt.isDeprecated())
- .forEach(opt -> {
- JsonObject jo = new JsonObject();
- jo.put("name", opt.getName());
- jo.put("description", opt.getDescription());
- jo.put("type", opt.getType());
- jo.put("required", opt.isRequired());
- if (opt.getDefaultValue() != null) {
- jo.put("defaultValue",
opt.getDefaultValue().toString());
- }
- options.add(jo);
- });
- }
- response.put("options", options);
- return response.toJson();
- }
-
- private String executeCatalogEips(JsonObject args) {
- String filter = args.getString("filter");
- CamelCatalog cat = getCatalog();
-
- List<JsonObject> results = cat.findModelNames().stream()
- .map(cat::eipModel)
- .filter(m -> m != null)
- .filter(m -> matchesFilter(m.getName(), m.getTitle(),
m.getDescription(), filter))
- .limit(20)
- .map(m -> {
- JsonObject jo = new JsonObject();
- jo.put("name", m.getName());
- jo.put("title", m.getTitle());
- jo.put("description", m.getDescription());
- jo.put("label", m.getLabel());
- return jo;
- })
- .collect(Collectors.toList());
-
- JsonObject response = new JsonObject();
- response.put("count", results.size());
- response.put("eips", results);
- return response.toJson();
- }
-
- // ---- Example tools ----
-
- @SuppressWarnings("unchecked")
- private String executeListExamples(JsonObject args) {
- String filter = args.getString("filter");
- String level = args.getString("level");
-
- List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
- List<JsonObject> filtered = ExampleHelper.filterExamples(catalog2,
filter);
-
- List<JsonObject> results = new ArrayList<>();
- for (JsonObject entry : filtered) {
- if (level != null && !level.isBlank()) {
- String entryLevel = entry.getString("level");
- if (entryLevel == null || !entryLevel.equalsIgnoreCase(level))
{
- continue;
- }
- }
- JsonObject jo = new JsonObject();
- jo.put("name", entry.getString("name"));
- jo.put("title", entry.getString("title"));
- jo.put("description", entry.getString("description"));
- jo.put("level", entry.getString("level"));
- jo.put("tags", entry.get("tags"));
- jo.put("bundled", ExampleHelper.isBundled(entry));
- jo.put("files", ExampleHelper.getFiles(entry));
- results.add(jo);
-
- if (results.size() >= 20) {
- break;
- }
- }
-
- JsonObject response = new JsonObject();
- response.put("count", results.size());
- response.put("examples", results);
- return response.toJson();
- }
-
- private String executeGetExampleFile(JsonObject args) {
- String example = args.getString("example");
- String file = args.getString("file");
- if (example == null || example.isBlank()) {
- return "Error: example name is required";
- }
- if (file == null || file.isBlank()) {
- return "Error: file name is required";
- }
-
- List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
- JsonObject entry = ExampleHelper.findExample(catalog2, example);
- if (entry == null) {
- return "Example not found: " + example;
- }
-
- List<String> files = ExampleHelper.getFiles(entry);
- if (!files.contains(file)) {
- return "File '" + file + "' not found in example '" + example +
"'. Available files: " + files;
- }
-
- if (ExampleHelper.isBundled(entry)) {
- String resourcePath = "examples/" + example + "/" + file;
- try (InputStream is =
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
- if (is != null) {
- return IOHelper.loadText(is);
- }
- } catch (Exception e) {
- return "Error reading file: " + e.getMessage();
- }
- return "Could not read bundled file: " + resourcePath;
- } else {
- return "This example is not bundled. View it on GitHub: " +
ExampleHelper.getGithubUrl(entry) + "/" + file;
- }
- }
-
- // ---- CLI tools ----
+ // ---- CLI tools (AskTools-specific) ----
@SuppressWarnings("unchecked")
private List<JsonObject> loadCommandMetadata() {
@@ -914,7 +374,7 @@ public class AskTools {
}
}
- // ---- File tools ----
+ // ---- File tools (AskTools-specific) ----
private String executeListFiles(JsonObject args) throws IOException {
String pathStr = args.getString("path");
@@ -988,13 +448,6 @@ public class AskTools {
return "File written: " + cwd.relativize(filePath);
}
- private CamelCatalog getCatalog() {
- if (catalog == null) {
- catalog = new DefaultCamelCatalog();
- }
- return catalog;
- }
-
// ---- JSON schema helpers ----
public static JsonObject emptyParams() {
@@ -1022,14 +475,4 @@ public class AskTools {
prop.put("description", description);
return prop;
}
-
- static boolean matchesFilter(String name, String title, String
description, String filter) {
- if (filter == null || filter.isBlank()) {
- return true;
- }
- String lf = filter.toLowerCase();
- return (name != null && name.toLowerCase().contains(lf))
- || (title != null && title.toLowerCase().contains(lf))
- || (description != null &&
description.toLowerCase().contains(lf));
- }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
new file mode 100644
index 000000000000..85105260eb75
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolContext.java
@@ -0,0 +1,121 @@
+/*
+ * 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 volatile long pid = -1;
+ private volatile CamelCatalog catalog;
+
+ public long pid() {
+ return pid;
+ }
+
+ public void selectProcess(long pid) {
+ this.pid = pid;
+ }
+
+ public boolean hasProcess() {
+ return pid >= 0;
+ }
+
+ /**
+ * Returns the shared {@link CamelCatalog} instance, creating it lazily on
first access. Thread-safe: concurrent
+ * callers may race but will all see a fully constructed catalog.
+ */
+ public CamelCatalog catalog() {
+ CamelCatalog result = catalog;
+ if (result == null) {
+ synchronized (this) {
+ result = catalog;
+ if (result == null) {
+ result = new DefaultCamelCatalog();
+ catalog = result;
+ }
+ }
+ }
+ return result;
+ }
+
+ public String readStatus(String section) {
+ requireProcess();
+ return RuntimeHelper.readStatusSection(pid, section);
+ }
+
+ public JsonObject readFullStatus() {
+ requireProcess();
+ return RuntimeHelper.readStatus(pid);
+ }
+
+ public String executeAction(String action, Consumer<JsonObject> configure)
{
+ requireProcess();
+ return RuntimeHelper.executeAction(pid, action, configure);
+ }
+
+ public JsonObject readErrorFile() {
+ requireProcess();
+ return RuntimeHelper.readErrorFile(pid);
+ }
+
+ public JsonObject readHistoryFile() {
+ requireProcess();
+ return RuntimeHelper.readHistoryFile(pid);
+ }
+
+ public String stopApplication() {
+ requireProcess();
+ return RuntimeHelper.stopApplication(pid);
+ }
+
+ public JsonObject sendMessage(String endpoint, String body, String
headers) {
+ requireProcess();
+ return RuntimeHelper.sendMessage(pid, endpoint, body, headers);
+ }
+
+ public List<RuntimeHelper.ProcessInfo> discoverProcesses() {
+ return RuntimeHelper.discoverProcesses();
+ }
+
+ public RuntimeHelper.ProcessInfo findProcess(String name) {
+ return RuntimeHelper.findProcess(name);
+ }
+
+ public JsonObject executeSqlQuery(String sql, String datasource, int
maxRows, int queryTimeout) {
+ requireProcess();
+ return RuntimeHelper.executeSqlQuery(pid, sql, datasource, maxRows,
queryTimeout);
+ }
+
+ public void requireProcess() {
+ if (pid < 0) {
+ throw new ToolExecutionException(
+ "No running Camel process connected. Start one with: camel
run <file>");
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
new file mode 100644
index 000000000000..6f24714f4a21
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolDescriptor.java
@@ -0,0 +1,102 @@
+/*
+ * 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.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Describes a single AI tool: its name, description, parameters, and
execution logic. Used by both the Agent REPL and
+ * the MCP server to avoid duplicating tool definitions.
+ */
+public class ToolDescriptor {
+
+ private final String name;
+ private final String description;
+ private final List<Param> params;
+ private ToolExecutor executor;
+ private boolean readOnly = true;
+ private boolean destructive = false;
+
+ public record Param(String name, String type, String description, boolean
required) {
+ }
+
+ @FunctionalInterface
+ public interface ToolExecutor {
+ Object execute(ToolContext ctx, Map<String, String> args) throws
ToolExecutionException;
+ }
+
+ private ToolDescriptor(String name, String description) {
+ this.name = name;
+ this.description = description;
+ this.params = new ArrayList<>();
+ }
+
+ public static ToolDescriptor tool(String name, String description) {
+ return new ToolDescriptor(name, description);
+ }
+
+ // Builder methods
+
+ public ToolDescriptor param(String name, String type, String description,
boolean required) {
+ params.add(new Param(name, type, description, required));
+ return this;
+ }
+
+ public ToolDescriptor readOnly(boolean v) {
+ readOnly = v;
+ return this;
+ }
+
+ public ToolDescriptor destructive(boolean v) {
+ destructive = v;
+ return this;
+ }
+
+ public ToolDescriptor executor(ToolExecutor exec) {
+ this.executor = exec;
+ return this;
+ }
+
+ // Getters
+
+ public String name() {
+ return name;
+ }
+
+ public String description() {
+ return description;
+ }
+
+ public List<Param> params() {
+ return Collections.unmodifiableList(params);
+ }
+
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ public boolean isDestructive() {
+ return destructive;
+ }
+
+ public ToolExecutor executor() {
+ return executor;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolExecutionException.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolExecutionException.java
new file mode 100644
index 000000000000..6ffac7aad6ff
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolExecutionException.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * Unchecked exception thrown when a tool execution fails.
+ */
+public class ToolExecutionException extends RuntimeException {
+
+ public ToolExecutionException(String message) {
+ super(message);
+ }
+
+ public ToolExecutionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
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
new file mode 100644
index 000000000000..53aa43a55795
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistry.java
@@ -0,0 +1,972 @@
+/*
+ * 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.");
+ // get_memory combines memory, gc, and threads sections for a complete
JVM resource view
+ register(tool("get_memory",
+ "Show JVM memory usage (heap/non-heap), garbage collection
stats, and thread counts.")
+ .executor((ctx, args) -> {
+ JsonObject root = ctx.readFullStatus();
+ if (root == null) {
+ return "{}";
+ }
+ JsonObject result = new JsonObject();
+ if (root.containsKey("memory")) {
+ result.put("memory", root.get("memory"));
+ }
+ if (root.containsKey("gc")) {
+ result.put("gc", root.get("gc"));
+ }
+ if (root.containsKey("threads")) {
+ result.put("threads", root.get("threads"));
+ }
+ return result.toJson();
+ }));
+ registerStatusTool("get_variables", "variables",
+ "Show exchange variables in the Camel context.");
+ registerStatusTool("get_services", "services",
+ "Show services registered in the Camel service registry.");
+ registerStatusTool("get_sql_trace", "sqlTrace",
+ "Get traced SQL query executions flowing through camel-sql and
camel-jdbc components. "
+ + "Returns per-query
timing, row counts, category (SELECT/INSERT/UPDATE/DELETE), "
+ + "route ID, and
failure status. Includes summary statistics.");
+ }
+
+ 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.")
+ .param("metric", "string", "Include live metrics on nodes and
edges (default: true)", false)
+ .param("external", "string", "Include external systems as
nodes (default: true)", false)
+ .executor((ctx, args) -> ctx.executeAction("route-topology",
root -> {
+ root.put("metric", args.getOrDefault("metric", "true"));
+ root.put("external", args.getOrDefault("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 -> throw new ToolExecutionException(
+ "Unknown trace action: " + action + ". Use
'enable', 'disable', or 'dump'.");
+ }
+ });
+ }));
+
+ 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)));
+
+ register(tool("get_heap_histogram",
+ "Get a class-level heap histogram showing instance counts and
byte usage per class. "
+ + "Useful for diagnosing memory
leaks and understanding which classes dominate heap usage.")
+ .executor((ctx, args) -> ctx.executeAction("heap-histogram",
null)));
+
+ register(tool("execute_sql",
+ "Execute a SQL query against a DataSource in the running Camel
application. "
+ + "Returns structured JSON with columns,
rows, and metadata for SELECT queries, "
+ + "or an update count for
INSERT/UPDATE/DELETE.")
+ .param("query", "string", "The SQL query to execute", true)
+ .param("datasource", "string",
+ "Name of the DataSource bean (auto-detected if only
one exists)", false)
+ .param("maxRows", "string", "Maximum number of rows to return
(default: 100)", false)
+ .readOnly(false).destructive(true)
+ .executor((ctx, args) -> {
+ String sql = args.get("query");
+ if (sql == null || sql.isBlank()) {
+ throw new ToolExecutionException("'query' parameter is
required");
+ }
+ String datasource = args.get("datasource");
+ int maxRows = 100;
+ String maxRowsStr = args.get("maxRows");
+ if (maxRowsStr != null && !maxRowsStr.isBlank()) {
+ try {
+ maxRows = Integer.parseInt(maxRowsStr);
+ } catch (NumberFormatException e) {
+ // use default
+ }
+ }
+ JsonObject result = ctx.executeSqlQuery(sql, datasource,
maxRows, 30);
+ return result.toJson();
+ }));
+
+ // 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("get_spans",
+ "Get OpenTelemetry trace spans from the running Camel
application. "
+ + "Shows trace IDs, span names, durations,
route IDs, and status. "
+ + "Requires OpenTelemetry tracing to be
enabled (run with --observe).")
+ .param("limit", "string", "Maximum number of spans to return
(default: 100)", false)
+ .executor((ctx, args) -> {
+ String limitStr = args.get("limit");
+ int limit = 100;
+ if (limitStr != null && !limitStr.isBlank()) {
+ try {
+ limit = Integer.parseInt(limitStr);
+ } catch (NumberFormatException e) {
+ // use default
+ }
+ }
+ int spanLimit = limit;
+ return ctx.executeAction("span", root -> {
+ root.put("dump", "true");
+ root.put("limit", String.valueOf(spanLimit));
+ });
+ }));
+
+ register(tool("get_metrics",
+ "Get Micrometer metrics from the running Camel application. "
+ + "Shows counters, gauges, timers,
long-task timers, and distributions. "
+ + "Useful for monitoring throughput,
latency, and resource usage. "
+ + "Requires micrometer to be enabled (run
with --observe or add camel-micrometer).")
+ .executor((ctx, args) -> ctx.readStatus("micrometer")));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void collectCircuitBreakers(JsonObject root, String
sectionKey, JsonArray target) {
+ Object section = root.get(sectionKey);
+ if (!(section instanceof JsonObject sectionObj)) {
+ return;
+ }
+ Object breakers = sectionObj.get("circuitBreakers");
+ if (!(breakers instanceof List<?> breakerList)) {
+ return;
+ }
+ String component = switch (sectionKey) {
+ case "resilience4j" -> "resilience4j";
+ case "fault-tolerance" -> "fault-tolerance";
+ default -> "core";
+ };
+ for (Object b : breakerList) {
+ if (b instanceof JsonObject bj) {
+ JsonObject entry = new JsonObject();
+ entry.put("component", component);
+ entry.put("routeId", bj.get("routeId"));
+ entry.put("id", bj.get("id"));
+ entry.put("state", bj.get("state"));
+ entry.put("bufferedCalls", bj.get("bufferedCalls"));
+ entry.put("successfulCalls", bj.get("successfulCalls"));
+ entry.put("failedCalls", bj.get("failedCalls"));
+ entry.put("notPermittedCalls", bj.get("notPermittedCalls"));
+ entry.put("failureRate", bj.get("failureRate"));
+ target.add(entry);
+ }
+ }
+ }
+
+ // ---- Analysis tools (higher-level diagnostic tools) ----
+
+ private static void registerAnalysisTools() {
+ register(tool("get_route_analysis",
+ "Analyze route definitions for common anti-patterns and
structural issues. "
+ + "Checks each route for: error
handler configuration, dead letter channel, "
+ + "route complexity (processor
count and nesting depth), component usage, "
+ + "and potential issues. Returns
structured analysis results per route.")
+ .param("routeId", "string", "Route ID to analyze (use * for
all routes, default: *)", false)
+ .executor((ctx, args) -> {
+ String routeId = args.get("routeId");
+ // Get route structure for analysis
+ String structureJson = ctx.executeAction("route-structure",
+ root -> root.put("id", routeId != null ? routeId :
"*"));
+ // Get route dump for error handler info
+ String dumpJson = ctx.executeAction("route-dump", root -> {
+ root.put("id", routeId != null ? routeId : "*");
+ root.put("format", "yaml");
+ });
+ JsonObject response = new JsonObject();
+ response.put("routeStructure", structureJson);
+ response.put("routeDump", dumpJson);
+ response.put("analysisHints", buildAnalysisHints());
+ return response.toJson();
+ }));
+
+ register(tool("get_eip_stats",
+ "Aggregate EIP (Enterprise Integration Pattern) usage
statistics across all routes. "
+ + "Shows which processors are used, how
often, and their performance metrics "
+ + "(total exchanges, failures, mean/max
processing time). "
+ + "Useful for understanding route
complexity and identifying bottlenecks.")
+ .executor((ctx, args) -> {
+ // Get top processor stats which include per-processor
metrics
+ String topJson = ctx.executeAction("top-processors", null);
+ // Get route structure for EIP type counting
+ String structureJson = ctx.executeAction("route-structure",
+ root -> root.put("id", "*"));
+ JsonObject response = new JsonObject();
+ response.put("processorStats", topJson);
+ response.put("routeStructures", structureJson);
+ return response.toJson();
+ }));
+
+ register(tool("detect_config_drift",
+ "Compare the running route configuration with the original
source files. "
+ + "Dumps the running route
definitions and the source files "
+ + "so they can be compared to
detect configuration drift. "
+ + "Useful for verifying that
runtime matches deployment artifacts.")
+ .param("format", "string", "Dump format for comparison: yaml
or xml (default: yaml)", false)
+ .executor((ctx, args) -> {
+ String format = args.get("format");
+ if (format == null || format.isBlank()) {
+ format = "yaml";
+ }
+ // Get running route definitions
+ String dumpFormat = format;
+ String runningRoutes = ctx.executeAction("route-dump",
root -> {
+ root.put("id", "*");
+ root.put("format", dumpFormat);
+ });
+ // Get source files
+ String sourceFiles = ctx.executeAction("source",
+ root -> root.put("filter", "*"));
+ JsonObject response = new JsonObject();
+ response.put("runningRoutes", runningRoutes);
+ response.put("sourceFiles", sourceFiles);
+ response.put("format", dumpFormat);
+ response.put("hint",
+ "Compare the running route definitions with the
source files to identify any differences. "
+ + "Common drift causes: dynamic route
modification, runtime property resolution, "
+ + "and hot-reloading changes.");
+ return response.toJson();
+ }));
+ }
+
+ private static JsonObject buildAnalysisHints() {
+ JsonObject hints = new JsonObject();
+ hints.put("errorHandling",
+ "Check if routes define an errorHandler or onException clause.
"
+ + "Routes without error handling will
propagate exceptions to the caller.");
+ hints.put("deadLetterChannel",
+ "Routes processing messages from queues should have a
deadLetterChannel "
+ + "to avoid losing messages on repeated
failures.");
+ hints.put("timeout",
+ "Routes calling external services (http, rest, soap) should
configure "
+ + "connection and read timeouts to avoid hanging
threads.");
+ hints.put("idempotency",
+ "Routes consuming from non-idempotent sources (JMS, Kafka
without exactly-once) "
+ + "should use an idempotentConsumer to handle
duplicate messages.");
+ hints.put("circuitBreaker",
+ "Routes calling unreliable external services should wrap calls
in a "
+ + "circuitBreaker (resilience4j) to
prevent cascade failures.");
+ hints.put("logging",
+ "Routes should include appropriate logging (log EIP) at key
points "
+ + "for operational visibility, especially on
error paths.");
+ return hints;
+ }
+
+ // ---- Catalog tools ----
+
+ private static void registerCatalogTools() {
+ register(tool("catalog_components",
+ "Search the Camel component catalog by name or label.")
+ .param("filter", "string",
+ "Filter by name, title, or description
(case-insensitive substring)", false)
+ .param("label", "string",
+ "Filter by category label (e.g., cloud, messaging,
database, file)", false)
+ .executor((ctx, args) -> {
+ String filter = args.get("filter");
+ String label = args.get("label");
+ CamelCatalog cat = ctx.catalog();
+ List<JsonObject> results =
cat.findComponentNames().stream()
+ .map(cat::componentModel)
+ .filter(Objects::nonNull)
+ .filter(m -> matchesFilter(m.getScheme(),
m.getTitle(), m.getDescription(), filter))
+ .filter(m -> label == null || label.isBlank()
+ || (m.getLabel() != null
+ &&
m.getLabel().toLowerCase().contains(label.toLowerCase())))
+ .limit(20)
+ .map(m -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", m.getScheme());
+ jo.put("title", m.getTitle());
+ jo.put("description", m.getDescription());
+ jo.put("label", m.getLabel());
+ return jo;
+ })
+ .collect(Collectors.toList());
+ JsonObject response = new JsonObject();
+ response.put("count", results.size());
+ response.put("components", results);
+ return response.toJson();
+ }));
+
+ register(tool("catalog_component_doc",
+ "Get detailed documentation for a Camel component: URI syntax
and endpoint options.")
+ .param("component", "string",
+ "Component name (e.g., kafka, http, file, timer)",
true)
+ .executor((ctx, args) -> {
+ String component = args.get("component");
+ if (component == null || component.isBlank()) {
+ throw new ToolExecutionException("component name is
required");
+ }
+ CamelCatalog cat = ctx.catalog();
+ ComponentModel model = cat.componentModel(component);
+ if (model == null) {
+ return "Component not found: " + component;
+ }
+ JsonObject response = new JsonObject();
+ response.put("name", model.getScheme());
+ response.put("title", model.getTitle());
+ response.put("description", model.getDescription());
+ response.put("syntax", model.getSyntax());
+ response.put("consumerOnly", model.isConsumerOnly());
+ response.put("producerOnly", model.isProducerOnly());
+ List<JsonObject> options = new ArrayList<>();
+ if (model.getEndpointOptions() != null) {
+ model.getEndpointOptions().stream()
+ .filter(opt -> !opt.isDeprecated())
+ .forEach(opt -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", opt.getName());
+ jo.put("description",
opt.getDescription());
+ jo.put("type", opt.getType());
+ jo.put("required", opt.isRequired());
+ if (opt.getDefaultValue() != null) {
+ jo.put("defaultValue",
opt.getDefaultValue().toString());
+ }
+ options.add(jo);
+ });
+ }
+ response.put("options", options);
+ return response.toJson();
+ }));
+
+ register(tool("catalog_eips",
+ "Search EIPs (Enterprise Integration Patterns) like split,
aggregate, filter, choice, multicast.")
+ .param("filter", "string",
+ "Filter by name, title, or description
(case-insensitive substring)", false)
+ .executor((ctx, args) -> {
+ String filter = args.get("filter");
+ CamelCatalog cat = ctx.catalog();
+ List<JsonObject> results = cat.findModelNames().stream()
+ .map(cat::eipModel)
+ .filter(Objects::nonNull)
+ .filter(m -> matchesFilter(m.getName(),
m.getTitle(), m.getDescription(), filter))
+ .limit(20)
+ .map(m -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", m.getName());
+ jo.put("title", m.getTitle());
+ jo.put("description", m.getDescription());
+ jo.put("label", m.getLabel());
+ return jo;
+ })
+ .collect(Collectors.toList());
+ JsonObject response = new JsonObject();
+ response.put("count", results.size());
+ response.put("eips", results);
+ return response.toJson();
+ }));
+
+ // New catalog tools (previously MCP-only)
+
+ register(tool("catalog_dataformats",
+ "List available Camel data formats (e.g., json, xml, csv,
avro).")
+ .param("filter", "string", "Filter by name or description",
false)
+ .executor((ctx, args) -> {
+ String filter = args.get("filter");
+ CamelCatalog cat = ctx.catalog();
+ List<JsonObject> results =
cat.findDataFormatNames().stream()
+ .map(cat::dataFormatModel)
+ .filter(Objects::nonNull)
+ .filter(m -> matchesFilter(m.getName(),
m.getTitle(), m.getDescription(), filter))
+ .limit(20)
+ .map(m -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", m.getName());
+ jo.put("title", m.getTitle());
+ jo.put("description", m.getDescription());
+ return jo;
+ })
+ .collect(Collectors.toList());
+ JsonObject response = new JsonObject();
+ response.put("count", results.size());
+ response.put("dataformats", results);
+ return response.toJson();
+ }));
+
+ register(tool("catalog_dataformat_doc",
+ "Get detailed documentation for a Camel data format.")
+ .param("dataformat", "string",
+ "Data format name (e.g., jackson, jaxb, csv)", true)
+ .executor((ctx, args) -> {
+ String name = args.get("dataformat");
+ if (name == null || name.isBlank()) {
+ throw new ToolExecutionException("dataformat name is
required");
+ }
+ CamelCatalog cat = ctx.catalog();
+ var model = cat.dataFormatModel(name);
+ if (model == null) {
+ return "Data format not found: " + name;
+ }
+ JsonObject response = new JsonObject();
+ response.put("name", model.getName());
+ response.put("title", model.getTitle());
+ response.put("description", model.getDescription());
+ response.put("modelJavaType", model.getModelJavaType());
+ if (model.getOptions() != null) {
+ List<JsonObject> options = model.getOptions().stream()
+ .filter(opt -> !opt.isDeprecated())
+ .map(opt -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", opt.getName());
+ jo.put("description",
opt.getDescription());
+ jo.put("type", opt.getType());
+ jo.put("required", opt.isRequired());
+ if (opt.getDefaultValue() != null) {
+ jo.put("defaultValue",
opt.getDefaultValue().toString());
+ }
+ return jo;
+ })
+ .collect(Collectors.toList());
+ response.put("options", options);
+ }
+ return response.toJson();
+ }));
+
+ register(tool("catalog_languages",
+ "List available Camel expression languages (e.g., simple,
jsonpath, xpath, jq).")
+ .param("filter", "string", "Filter by name or description",
false)
+ .executor((ctx, args) -> {
+ String filter = args.get("filter");
+ CamelCatalog cat = ctx.catalog();
+ List<JsonObject> results = cat.findLanguageNames().stream()
+ .map(cat::languageModel)
+ .filter(Objects::nonNull)
+ .filter(m -> matchesFilter(m.getName(),
m.getTitle(), m.getDescription(), filter))
+ .limit(20)
+ .map(m -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", m.getName());
+ jo.put("title", m.getTitle());
+ jo.put("description", m.getDescription());
+ return jo;
+ })
+ .collect(Collectors.toList());
+ JsonObject response = new JsonObject();
+ response.put("count", results.size());
+ response.put("languages", results);
+ return response.toJson();
+ }));
+
+ register(tool("catalog_language_doc",
+ "Get detailed documentation for a Camel expression language.")
+ .param("language", "string",
+ "Language name (e.g., simple, jsonpath, xpath, jq)",
true)
+ .executor((ctx, args) -> {
+ String name = args.get("language");
+ if (name == null || name.isBlank()) {
+ throw new ToolExecutionException("language name is
required");
+ }
+ CamelCatalog cat = ctx.catalog();
+ var model = cat.languageModel(name);
+ if (model == null) {
+ return "Language not found: " + name;
+ }
+ JsonObject response = new JsonObject();
+ response.put("name", model.getName());
+ response.put("title", model.getTitle());
+ response.put("description", model.getDescription());
+ response.put("modelJavaType", model.getModelJavaType());
+ if (model.getOptions() != null) {
+ List<JsonObject> options = model.getOptions().stream()
+ .filter(opt -> !opt.isDeprecated())
+ .map(opt -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", opt.getName());
+ jo.put("description",
opt.getDescription());
+ jo.put("type", opt.getType());
+ jo.put("required", opt.isRequired());
+ if (opt.getDefaultValue() != null) {
+ jo.put("defaultValue",
opt.getDefaultValue().toString());
+ }
+ return jo;
+ })
+ .collect(Collectors.toList());
+ response.put("options", options);
+ }
+ return response.toJson();
+ }));
+
+ register(tool("catalog_eip_doc",
+ "Get detailed documentation for a Camel EIP (Enterprise
Integration Pattern).")
+ .param("eip", "string",
+ "EIP name (e.g., split, aggregate, filter, choice)",
true)
+ .executor((ctx, args) -> {
+ String name = args.get("eip");
+ if (name == null || name.isBlank()) {
+ throw new ToolExecutionException("EIP name is
required");
+ }
+ CamelCatalog cat = ctx.catalog();
+ var model = cat.eipModel(name);
+ if (model == null) {
+ return "EIP not found: " + name;
+ }
+ JsonObject response = new JsonObject();
+ response.put("name", model.getName());
+ response.put("title", model.getTitle());
+ response.put("description", model.getDescription());
+ response.put("label", model.getLabel());
+ response.put("input", model.isInput());
+ response.put("output", model.isOutput());
+ if (model.getOptions() != null) {
+ List<JsonObject> options = model.getOptions().stream()
+ .filter(opt -> !opt.isDeprecated())
+ .map(opt -> {
+ JsonObject jo = new JsonObject();
+ jo.put("name", opt.getName());
+ jo.put("description",
opt.getDescription());
+ jo.put("type", opt.getType());
+ jo.put("required", opt.isRequired());
+ if (opt.getDefaultValue() != null) {
+ jo.put("defaultValue",
opt.getDefaultValue().toString());
+ }
+ return jo;
+ })
+ .collect(Collectors.toList());
+ response.put("options", options);
+ }
+ return response.toJson();
+ }));
+ }
+
+ // ---- Example tools ----
+
+ private static void registerExampleTools() {
+ register(tool("list_examples",
+ "List available Camel CLI examples. Returns name, title,
description, difficulty level, and tags.")
+ .param("filter", "string",
+ "Filter by name, description, or tag
(case-insensitive)", false)
+ .param("level", "string",
+ "Filter by difficulty: beginner, intermediate, or
advanced", false)
+ .executor((ctx, args) -> {
+ String filter = args.get("filter");
+ String level = args.get("level");
+ List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
+ List<JsonObject> filtered =
ExampleHelper.filterExamples(catalog2, filter);
+ List<JsonObject> results = new ArrayList<>();
+ for (JsonObject entry : filtered) {
+ if (level != null && !level.isBlank()) {
+ String entryLevel = entry.getString("level");
+ if (entryLevel == null ||
!entryLevel.equalsIgnoreCase(level)) {
+ continue;
+ }
+ }
+ JsonObject jo = new JsonObject();
+ jo.put("name", entry.getString("name"));
+ jo.put("title", entry.getString("title"));
+ jo.put("description", entry.getString("description"));
+ jo.put("level", entry.getString("level"));
+ jo.put("tags", entry.get("tags"));
+ jo.put("bundled", ExampleHelper.isBundled(entry));
+ jo.put("files", ExampleHelper.getFiles(entry));
+ results.add(jo);
+ if (results.size() >= 20) {
+ break;
+ }
+ }
+ JsonObject response = new JsonObject();
+ response.put("count", results.size());
+ response.put("examples", results);
+ return response.toJson();
+ }));
+
+ register(tool("get_example_file",
+ "Get the content of a file from a bundled Camel CLI example.")
+ .param("example", "string",
+ "Example name (e.g., timer-log, rest-api,
circuit-breaker)", true)
+ .param("file", "string",
+ "File name within the example (e.g.,
route.camel.yaml)", true)
+ .executor((ctx, args) -> {
+ String example = args.get("example");
+ String file = args.get("file");
+ if (example == null || example.isBlank()) {
+ throw new ToolExecutionException("example name is
required");
+ }
+ if (file == null || file.isBlank()) {
+ throw new ToolExecutionException("file name is
required");
+ }
+ List<JsonObject> catalog2 = ExampleHelper.loadCatalog();
+ JsonObject entry = ExampleHelper.findExample(catalog2,
example);
+ if (entry == null) {
+ return "Example not found: " + example;
+ }
+ List<String> files = ExampleHelper.getFiles(entry);
+ if (!files.contains(file)) {
+ return "File '" + file + "' not found in example '" +
example + "'. Available: " + files;
+ }
+ if (ExampleHelper.isBundled(entry)) {
+ String resourcePath = "examples/" + example + "/" +
file;
+ try (InputStream is
+ =
ExampleHelper.class.getClassLoader().getResourceAsStream(resourcePath)) {
+ if (is != null) {
+ return IOHelper.loadText(is);
+ }
+ } catch (Exception e) {
+ return "Error reading file: " + e.getMessage();
+ }
+ return "Could not read bundled file: " + resourcePath;
+ } else {
+ return "This example is not bundled. View it on
GitHub: "
+ + ExampleHelper.getGithubUrl(entry) + "/" +
file;
+ }
+ }));
+ }
+
+ // ---- Utility ----
+
+ static boolean matchesFilter(String name, String title, String
description, String filter) {
+ if (filter == null || filter.isBlank()) {
+ return true;
+ }
+ String lf = filter.toLowerCase();
+ return (name != null && name.toLowerCase().contains(lf))
+ || (title != null && title.toLowerCase().contains(lf))
+ || (description != null &&
description.toLowerCase().contains(lf));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
new file mode 100644
index 000000000000..514e2d77c0a9
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ToolRegistryTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ToolRegistryTest {
+
+ @Test
+ void allToolsHaveUniqueNames() {
+ List<String> names = ToolRegistry.allTools().stream()
+ .map(ToolDescriptor::name).toList();
+ Set<String> unique = new HashSet<>(names);
+ assertEquals(unique.size(), names.size(), "Duplicate tool names
found");
+ }
+
+ @Test
+ void allToolsHaveDescriptions() {
+ for (ToolDescriptor t : ToolRegistry.allTools()) {
+ assertNotNull(t.description(), t.name() + " has no description");
+ assertFalse(t.description().isBlank(), t.name() + " has blank
description");
+ }
+ }
+
+ @Test
+ void allToolsHaveExecutors() {
+ for (ToolDescriptor t : ToolRegistry.allTools()) {
+ assertNotNull(t.executor(), t.name() + " has no executor");
+ }
+ }
+
+ @Test
+ void findToolReturnsRegisteredTool() {
+ ToolDescriptor tool = ToolRegistry.findTool("catalog_components");
+ assertNotNull(tool);
+ assertEquals("catalog_components", tool.name());
+ }
+
+ @Test
+ void findToolReturnsNullForUnknown() {
+ assertNull(ToolRegistry.findTool("nonexistent_tool"));
+ }
+
+ @Test
+ void catalogToolsWorkWithoutProcess() {
+ // Catalog tools should not require a running process
+ ToolContext ctx = new ToolContext();
+ Object result = ToolRegistry.execute("catalog_components",
+ ctx, Map.of("filter", "timer"));
+ assertNotNull(result);
+ String json = result.toString();
+ assertTrue(json.contains("timer"), "Should find timer component");
+ }
+
+ @Test
+ void catalogEipToolsWorkWithoutProcess() {
+ ToolContext ctx = new ToolContext();
+ Object result = ToolRegistry.execute("catalog_eips",
+ ctx, Map.of("filter", "split"));
+ assertNotNull(result);
+ String json = result.toString();
+ assertTrue(json.contains("split"), "Should find split EIP");
+ }
+
+ @Test
+ void runtimeToolThrowsWithoutProcess() {
+ ToolContext ctx = new ToolContext();
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("get_context", ctx, Map.of()));
+ }
+
+ @Test
+ void registryContainsExpectedToolCount() {
+ // Should have at least 50 tools (process + status + actions +
DevConsole + analysis + catalog + examples)
+ assertTrue(ToolRegistry.allTools().size() >= 50,
+ "Expected at least 50 tools, got " +
ToolRegistry.allTools().size());
+ }
+
+ @Test
+ void newCatalogToolsPresent() {
+ // Verify the new catalog tools that were MCP-only are now registered
+ assertNotNull(ToolRegistry.findTool("catalog_dataformats"),
"catalog_dataformats should be registered");
+ assertNotNull(ToolRegistry.findTool("catalog_dataformat_doc"),
"catalog_dataformat_doc should be registered");
+ assertNotNull(ToolRegistry.findTool("catalog_languages"),
"catalog_languages should be registered");
+ assertNotNull(ToolRegistry.findTool("catalog_language_doc"),
"catalog_language_doc should be registered");
+ assertNotNull(ToolRegistry.findTool("catalog_eip_doc"),
"catalog_eip_doc should be registered");
+ }
+
+ @Test
+ void devConsoleToolsPresent() {
+ // Verify DevConsole tools are registered
+ assertNotNull(ToolRegistry.findTool("get_circuit_breakers"),
"get_circuit_breakers should be registered");
+ assertNotNull(ToolRegistry.findTool("get_startup_steps"),
"get_startup_steps should be registered");
+ assertNotNull(ToolRegistry.findTool("get_datasources"),
"get_datasources should be registered");
+ assertNotNull(ToolRegistry.findTool("execute_sql"), "execute_sql
should be registered");
+ assertNotNull(ToolRegistry.findTool("get_spans"), "get_spans should be
registered");
+ assertNotNull(ToolRegistry.findTool("get_metrics"), "get_metrics
should be registered");
+ }
+
+ @Test
+ void executeSqlToolRequiresProcess() {
+ ToolContext ctx = new ToolContext();
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("execute_sql", ctx, Map.of("query",
"SELECT 1")));
+ }
+
+ @Test
+ void executeSqlToolRequiresQueryParam() {
+ ToolContext ctx = new ToolContext();
+ ctx.selectProcess(99999);
+ // execute_sql should throw for empty query parameter
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("execute_sql", ctx, Map.of()));
+ }
+
+ @Test
+ void circuitBreakerToolRequiresProcess() {
+ ToolContext ctx = new ToolContext();
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("get_circuit_breakers", ctx,
Map.of()));
+ }
+
+ @Test
+ void analysisToolsPresent() {
+ assertNotNull(ToolRegistry.findTool("get_route_analysis"),
"get_route_analysis should be registered");
+ assertNotNull(ToolRegistry.findTool("get_eip_stats"), "get_eip_stats
should be registered");
+ assertNotNull(ToolRegistry.findTool("detect_config_drift"),
"detect_config_drift should be registered");
+ }
+
+ @Test
+ void analysisToolsRequireProcess() {
+ ToolContext ctx = new ToolContext();
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("get_route_analysis", ctx,
Map.of()));
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("get_eip_stats", ctx, Map.of()));
+ assertThrows(ToolExecutionException.class,
+ () -> ToolRegistry.execute("detect_config_drift", ctx,
Map.of()));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
index 05e7e26a0512..a4bfa93a682d 100644
---
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
+++
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PromptDefinitions.java
@@ -204,4 +204,177 @@ public class PromptDefinitions {
return List.of(PromptMessage.withUserRole(instructions));
}
+
+ /**
+ * Guided workflow for diagnosing issues with a running Camel route.
+ */
+ @Prompt(name = "camel_diagnose_route",
+ description = "Guided workflow to diagnose issues with a running
Camel route: "
+ + "gather runtime state, errors, health, message
history, "
+ + "and produce a root cause analysis with actionable
fixes.")
+ public List<PromptMessage> diagnoseRoute(
+ @PromptArg(name = "routeId",
+ description = "Route ID to diagnose (optional — if
omitted, diagnoses all routes with issues)",
+ required = false) String routeId,
+ @PromptArg(name = "symptom",
+ description = "Description of the observed problem
(e.g., 'messages are not being processed', "
+ + "'high error rate', 'route is
suspended')",
+ required = false) String symptom) {
+
+ String routeFilter = routeId != null && !routeId.isBlank() ? routeId :
"*";
+ String symptomNote = symptom != null && !symptom.isBlank()
+ ? "Reported symptom: " + symptom
+ : "No specific symptom reported — perform a general health
diagnosis.";
+
+ String instructions = """
+ You are diagnosing a running Camel application.
+
+ %s
+ Target route(s): %s
+
+ ## Workflow
+
+ Follow these steps in order:
+
+ ### Step 1: Gather context
+ Call `get_context` to understand the application state
(version, uptime, total exchanges).
+ Call `get_routes` to see all routes, their states, and error
counts.
+
+ ### Step 2: Check health
+ Call `get_health` to see if any health checks are failing.
+ Note any DOWN or UNKNOWN health statuses.
+
+ ### Step 3: Collect errors
+ Call `get_errors` to get captured routing errors with stack
traces.
+ Call `get_history` to see the message history trace of the
last completed exchange.
+
+ ### Step 4: Inspect route details
+ Call `get_route_source` with filter="%s" to see the route
definition.
+ Call `get_top_processors` to identify slow or failing
processors.
+ Call `get_inflight` to check for stuck exchanges.
+ Call `get_blocked` to check for blocked exchanges.
+
+ ### Step 5: Check infrastructure
+ Call `get_memory` to check for memory pressure or excessive GC.
+ Call `get_endpoints` to verify all endpoints are reachable.
+
+ ### Step 6: Advanced diagnostics (if needed)
+ If circuit breakers are involved, call `get_circuit_breakers`.
+ If datasources are involved, call `get_datasources`.
+ If the issue involves timing, call `get_spans` for trace data.
+ If the issue involves metrics, call `get_metrics`.
+
+ ### Step 7: Root cause analysis
+ Synthesize the gathered data into a structured diagnosis:
+
+ **Summary**: One-sentence description of the root cause.
+
+ **Evidence**: List the specific data points that led to this
conclusion:
+ - Which tools provided the key evidence
+ - Specific values that indicate the problem (error counts,
timing, states)
+
+ **Root Cause**: Detailed explanation of what is going wrong
and why.
+
+ **Immediate Actions**: Steps to take right now to mitigate:
+ - Route actions (stop, restart, suspend)
+ - Configuration changes
+ - Resource adjustments
+
+ **Permanent Fix**: What needs to change in the route
definition or configuration \
+ to prevent recurrence. Include specific code examples if
applicable.
+
+ **Monitoring**: What to watch going forward to detect this
issue early.
+ """.formatted(symptomNote, routeFilter, routeFilter);
+
+ return List.of(PromptMessage.withUserRole(instructions));
+ }
+
+ /**
+ * Guided workflow for optimizing a running Camel application's
performance.
+ */
+ @Prompt(name = "camel_optimize_route",
+ description = "Guided workflow to optimize a Camel application's
performance: "
+ + "analyze throughput, identify bottlenecks, review
resource usage, "
+ + "and produce prioritized optimization
recommendations.")
+ public List<PromptMessage> optimizeRoute(
+ @PromptArg(name = "routeId",
+ description = "Route ID to optimize (optional — if
omitted, analyzes all routes)",
+ required = false) String routeId,
+ @PromptArg(name = "goal",
+ description = "Optimization goal: throughput, latency,
memory, or general (default: general)",
+ required = false) String goal) {
+
+ String routeFilter = routeId != null && !routeId.isBlank() ? routeId :
"*";
+ String resolvedGoal = goal != null && !goal.isBlank() ? goal :
"general";
+
+ String instructions = """
+ You are optimizing a running Camel application for: **%s**.
+
+ Target route(s): %s
+
+ ## Workflow
+
+ Follow these steps in order:
+
+ ### Step 1: Baseline performance
+ Call `get_context` to get overall exchange statistics (total,
completed, failed, mean time).
+ Call `get_routes` to get per-route throughput and timing data.
+ Call `get_top_processors` to identify the slowest processors.
+
+ ### Step 2: Resource analysis
+ Call `get_memory` to assess JVM memory usage, GC frequency,
and thread counts.
+ Call `get_consumers` to check consumer configuration and
polling strategies.
+ Call `get_endpoints` to review endpoint usage patterns.
+
+ ### Step 3: Route structure analysis
+ Call `get_route_analysis` to get route structure and
anti-pattern hints.
+ Call `get_eip_stats` to understand EIP usage and processor
performance distribution.
+
+ ### Step 4: Infrastructure checks
+ Call `get_metrics` for detailed Micrometer metrics (if
available).
+ Call `get_datasources` if database access is involved.
+ Call `get_circuit_breakers` if resilience patterns are in use.
+
+ ### Step 5: Route inspection
+ Call `get_route_source` with filter="%s" to review the route
code.
+ Call `get_route_dump` with routeId="%s" and format="yaml" to
see the full definition.
+
+ ### Step 6: Optimization report
+ Produce a structured optimization report:
+
+ **Current Performance Baseline**:
+ - Overall throughput (exchanges/sec)
+ - Mean and max processing time
+ - Error rate
+ - Resource utilization (memory, threads)
+
+ **Bottlenecks Identified** (ordered by impact):
+ For each bottleneck:
+ - What: the specific processor, endpoint, or pattern
+ - Impact: how much time or throughput it costs
+ - Evidence: the specific metrics that revealed it
+
+ **Optimization Recommendations** (ordered by priority):
+
+ For each recommendation:
+ 1. **Change**: What to modify (with specific code examples)
+ 2. **Expected Impact**: Estimated improvement
+ 3. **Risk**: What could go wrong
+ 4. **Effort**: Low/Medium/High
+
+ Common optimizations to evaluate:
+ - **Parallelism**: Can `split` with `parallelProcessing` or
`threads` EIP help?
+ - **Batching**: Should messages be batched with `aggregate`?
+ - **Caching**: Can frequently-accessed data be cached?
+ - **Connection pooling**: Are connection pools properly sized?
+ - **Async processing**: Can synchronous calls be made
asynchronous?
+ - **Backpressure**: Is `throttle` or `circuitBreaker` needed?
+ - **Serialization**: Is a more efficient data format available?
+
+ **Trade-offs**: Note any trade-offs between the optimization
goal \
+ and other qualities (e.g., throughput vs. latency, performance
vs. reliability).
+ """.formatted(resolvedGoal, routeFilter, routeFilter,
routeFilter);
+
+ return List.of(PromptMessage.withUserRole(instructions));
+ }
}
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 3f86c4ecd9a6..91123420f214 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
@@ -16,7 +16,9 @@
*/
package org.apache.camel.dsl.jbang.core.commands.mcp;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
@@ -24,13 +26,21 @@ import jakarta.inject.Inject;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import io.quarkiverse.mcp.server.ToolCallException;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolContext;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolExecutionException;
+import org.apache.camel.dsl.jbang.core.commands.ai.ToolRegistry;
import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
/**
* MCP tools for inspecting and interacting with running Camel applications.
* <p>
- * These tools communicate with running Camel processes via the file-based IPC
protocol in ~/.camel/. Status data is
- * read from periodic snapshots; interactive commands use the multi-client
action file protocol.
+ * These tools are thin MCP wrappers that delegate to the shared {@link
ToolRegistry}. Each method resolves the
+ * {@code nameOrPid} parameter (an MCP-specific concern for multi-process
discovery) and then delegates to the
+ * corresponding {@link ToolRegistry} tool for the actual logic.
+ * <p>
+ * The {@code @Tool} and {@code @ToolArg} annotations are required by the
Quarkus MCP server for protocol discovery but
+ * all tool logic lives in {@link ToolRegistry}, ensuring a single source of
truth shared with the Agent REPL.
*/
@ApplicationScoped
public class RuntimeTools {
@@ -41,7 +51,55 @@ public class RuntimeTools {
@Inject
RuntimeService runtimeService;
- // ---- Process discovery ----
+ // ---- Delegation helpers ----
+
+ /**
+ * Resolve nameOrPid via {@link RuntimeService}, create a {@link
ToolContext} with the resolved PID, and delegate to
+ * the shared {@link ToolRegistry}.
+ *
+ * @param registryToolName the tool name in ToolRegistry (e.g.,
"get_context")
+ * @param nameOrPid MCP process selector (name, PID, or empty for
auto-detect)
+ * @param args tool arguments as a String map
+ * @return the tool result as a JsonObject suitable for
MCP responses
+ */
+ private JsonObject delegateToRegistry(String registryToolName, String
nameOrPid, Map<String, String> args) {
+ RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
+ ToolContext ctx = new ToolContext();
+ ctx.selectProcess(p.pid());
+ try {
+ Object result = ToolRegistry.execute(registryToolName, ctx, args);
+ return toJsonObject(result);
+ } catch (ToolExecutionException e) {
+ throw new ToolCallException(e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Convert a {@link ToolRegistry} result (typically a JSON String or
JsonObject) into a {@link JsonObject} for MCP
+ * serialization.
+ */
+ static JsonObject toJsonObject(Object result) {
+ if (result instanceof JsonObject jo) {
+ return jo;
+ }
+ if (result == null) {
+ return new JsonObject();
+ }
+ String str = result.toString();
+ try {
+ Object parsed = Jsoner.deserialize(str);
+ if (parsed instanceof JsonObject jo) {
+ return jo;
+ }
+ } catch (Exception e) {
+ // not a valid JSON object — wrap as plain text
+ }
+ JsonObject wrapper = new JsonObject();
+ wrapper.put("result", str);
+ return wrapper;
+ }
+
+ // ---- Process discovery (MCP-specific, not in ToolRegistry) ----
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = """
@@ -51,121 +109,94 @@ public class RuntimeTools {
return runtimeService.discoverProcesses();
}
- // ---- Read-only tools (from status file) ----
+ // ---- Read-only status tools ----
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Get Camel context information: name, version, state,
uptime, route count, exchange statistics.")
public JsonObject camel_runtime_context(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "context");
+ return delegateToRegistry("get_context", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "List Camel routes with their state, uptime, messages
processed, last error, and throughput statistics.")
public JsonObject camel_runtime_routes(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "routes");
+ return delegateToRegistry("get_routes", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Get health check status for the Camel application.")
public JsonObject camel_runtime_health(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "healthChecks");
+ return delegateToRegistry("get_health", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "List all endpoints registered in the Camel context
with their URIs and usage statistics.")
public JsonObject camel_runtime_endpoints(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "endpoints");
+ return delegateToRegistry("get_endpoints", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show currently in-flight exchanges (messages being
processed).")
public JsonObject camel_runtime_inflight(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "inflight");
+ return delegateToRegistry("get_inflight", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show blocked exchanges that are stuck or waiting.")
public JsonObject camel_runtime_blocked(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "blocked");
+ return delegateToRegistry("get_blocked", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show exchange variables in the Camel context.")
public JsonObject camel_runtime_variables(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "variables");
+ return delegateToRegistry("get_variables", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show consumer statistics (polling consumers,
event-driven consumers).")
public JsonObject camel_runtime_consumers(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "consumers");
+ return delegateToRegistry("get_consumers", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show configuration properties of the running Camel
application.")
public JsonObject camel_runtime_properties(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "properties");
+ return delegateToRegistry("get_properties", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show services registered in the Camel service
registry.")
public JsonObject camel_runtime_services(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readStatusSection(p.pid(), "services");
+ return delegateToRegistry("get_services", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show JVM memory usage (heap/non-heap), garbage
collection stats, and thread counts.")
public JsonObject camel_runtime_memory(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- JsonObject status = runtimeService.readStatus(p.pid());
- if (status == null) {
- return new JsonObject();
- }
- JsonObject result = new JsonObject();
- if (status.containsKey("memory")) {
- result.put("memory", status.get("memory"));
- }
- if (status.containsKey("gc")) {
- result.put("gc", status.get("gc"));
- }
- if (status.containsKey("threads")) {
- result.put("threads", status.get("threads"));
- }
- return result;
+ return delegateToRegistry("get_memory", nameOrPid, Map.of());
}
- // ---- Interactive tools (via action file IPC) ----
+ // ---- Interactive action tools ----
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Get the source code of routes in the running Camel
application.")
public JsonObject camel_runtime_route_source(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
@ToolArg(description = "Filter source files by name (supports
wildcards)") String filter) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "source", root -> {
- root.put("filter", filter != null ? filter : "*");
- });
+ return delegateToRegistry("get_route_source", nameOrPid,
+ Map.of("filter", filter != null ? filter : "*"));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -174,11 +205,10 @@ public class RuntimeTools {
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
@ToolArg(description = "Route ID to dump (use * for all routes)")
String routeId,
@ToolArg(description = "Output format: xml, yaml, or java
(default: yaml)") String format) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "route-dump", root -> {
- root.put("id", routeId != null ? routeId : "*");
- root.put("format", format != null ? format : "yaml");
- });
+ Map<String, String> args = new HashMap<>();
+ args.put("routeId", routeId != null ? routeId : "*");
+ args.put("format", format != null ? format : "yaml");
+ return delegateToRegistry("get_route_dump", nameOrPid, args);
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -186,10 +216,8 @@ public class RuntimeTools {
public JsonObject camel_runtime_route_structure(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
@ToolArg(description = "Route ID to inspect (use * for all
routes)") String routeId) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "route-structure", root
-> {
- root.put("id", routeId != null ? routeId : "*");
- });
+ return delegateToRegistry("get_route_structure", nameOrPid,
+ Map.of("routeId", routeId != null ? routeId : "*"));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = true, openWorldHint = false),
@@ -204,11 +232,16 @@ public class RuntimeTools {
if (command == null || command.isBlank()) {
throw new ToolCallException("command is required (start, stop,
suspend, resume)", null);
}
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "route", root -> {
- root.put("id", routeId);
- root.put("command", command);
- });
+ // Map the MCP command to the corresponding ToolRegistry tool
+ String toolName = switch (command.toLowerCase()) {
+ case "start" -> "start_route";
+ case "stop" -> "stop_route";
+ case "suspend" -> "suspend_route";
+ case "resume" -> "resume_route";
+ default -> throw new ToolCallException(
+ "Unknown command: " + command + ". Use start, stop,
suspend, or resume.", null);
+ };
+ return delegateToRegistry(toolName, nameOrPid, Map.of("routeId",
routeId));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = true, openWorldHint = false),
@@ -221,16 +254,15 @@ public class RuntimeTools {
if (endpoint == null || endpoint.isBlank()) {
throw new ToolCallException("endpoint is required", null);
}
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "send", root -> {
- root.put("endpoint", endpoint);
- if (body != null) {
- root.put("body", body);
- }
- if (headers != null) {
- root.put("headers", headers);
- }
- });
+ Map<String, String> args = new HashMap<>();
+ args.put("endpoint", endpoint);
+ if (body != null) {
+ args.put("body", body);
+ }
+ if (headers != null) {
+ args.put("headers", headers);
+ }
+ return delegateToRegistry("send_message", nameOrPid, args);
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = false, openWorldHint = false),
@@ -241,26 +273,14 @@ public class RuntimeTools {
if (action == null || action.isBlank()) {
throw new ToolCallException("action is required (enable, disable,
dump)", null);
}
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "trace", root -> {
- switch (action.toLowerCase()) {
- case "enable" -> root.put("enabled", "true");
- case "disable" -> root.put("enabled", "false");
- case "dump" -> root.put("dump", "true");
- default -> throw new ToolCallException(
- "Unknown trace action: " + action
- + ". Use 'enable',
'disable', or 'dump'.",
- null);
- }
- });
+ return delegateToRegistry("trace_control", nameOrPid, Map.of("action",
action));
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Show top processor statistics: which processors are
slowest and most active.")
public JsonObject camel_runtime_top(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "top-processors", null);
+ return delegateToRegistry("get_top_processors", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -277,11 +297,10 @@ public class RuntimeTools {
if (expression == null || expression.isBlank()) {
throw new ToolCallException("expression is required", null);
}
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "eval", root -> {
- root.put("language", language);
- root.put("expression", expression);
- });
+ Map<String, String> args = new HashMap<>();
+ args.put("language", language);
+ args.put("expression", expression);
+ return delegateToRegistry("eval_expression", nameOrPid, args);
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -292,11 +311,10 @@ public class RuntimeTools {
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid,
@ToolArg(description = "Include live metrics (message counts,
throughput) on nodes and edges") Boolean metric,
@ToolArg(description = "Include external systems (databases,
messaging brokers, etc.) as nodes") Boolean external) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "route-topology", root ->
{
- root.put("metric", metric != null && metric ? "true" : "false");
- root.put("external", external != null && external ? "true" :
"false");
- });
+ Map<String, String> args = new HashMap<>();
+ args.put("metric", metric == null || metric ? "true" : "false");
+ args.put("external", external == null || external ? "true" : "false");
+ return delegateToRegistry("get_route_topology", nameOrPid, args);
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -305,16 +323,14 @@ public class RuntimeTools {
Returns error details including exception, exchange context,
and route information.""")
public JsonObject camel_runtime_errors(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readErrorFile(p.pid());
+ return delegateToRegistry("get_errors", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
description = "Get a JVM thread dump showing thread names, states,
and stack traces.")
public JsonObject camel_runtime_thread_dump(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "thread-dump", null);
+ return delegateToRegistry("get_thread_dump", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -323,8 +339,7 @@ public class RuntimeTools {
Useful for diagnosing memory leaks and understanding which
classes dominate heap usage.""")
public JsonObject camel_runtime_heap_histogram(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "heap-histogram", null);
+ return delegateToRegistry("get_heap_histogram", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint
= false, openWorldHint = false),
@@ -334,8 +349,7 @@ public class RuntimeTools {
with its route path, processors visited, headers, body, and
timing.""")
public JsonObject camel_runtime_history(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.readHistoryFile(p.pid());
+ return delegateToRegistry("get_history", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = true, openWorldHint = false),
@@ -344,11 +358,7 @@ public class RuntimeTools {
The application will finish processing in-flight exchanges
before stopping.""")
public JsonObject camel_runtime_stop(
@ToolArg(description = NAME_OR_PID_DESC) String nameOrPid) {
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- String result = runtimeService.stopApplication(p.pid());
- JsonObject response = new JsonObject();
- response.put("result", result);
- return response;
+ return delegateToRegistry("stop_application", nameOrPid, Map.of());
}
@Tool(annotations = @Tool.Annotations(readOnlyHint = false,
destructiveHint = false, openWorldHint = false),
@@ -361,6 +371,7 @@ public class RuntimeTools {
if (endpoint == null || endpoint.isBlank()) {
throw new ToolCallException("endpoint is required", null);
}
+ // receive is MCP-only (no ToolRegistry equivalent) — call
RuntimeService directly
RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
return runtimeService.executeAction(p.pid(), "receive", root -> {
root.put("enabled", "true");
@@ -377,10 +388,11 @@ public class RuntimeTools {
if (endpoint == null || endpoint.isBlank()) {
throw new ToolCallException("endpoint is required", null);
}
- RuntimeService.ProcessInfo p =
runtimeService.findSingleProcess(nameOrPid);
- return runtimeService.executeAction(p.pid(), "browse", root -> {
- root.put("endpoint", endpoint);
- root.put("limit", limit != null ? limit : 50);
- });
+ Map<String, String> args = new HashMap<>();
+ args.put("endpoint", endpoint);
+ if (limit != null) {
+ args.put("limit", limit.toString());
+ }
+ return delegateToRegistry("browse_endpoint", nameOrPid, args);
}
}