This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 79dee237aa11a8249b63be62c3f8a19fb210e187
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 8 17:17:37 2026 +0200

    CAMEL-24656: camel-jbang TUI - expose the status document as MCP resources 
and a tui_get_status tool
    
    camel-cli-connector writes a full status document per integration to
    ~/.camel/<pid>-status.json; the tabs only show a digest of it. Add
    StatusFileReader and expose the document through the TUI MCP server as
    resources (camel://status/<pid> for the whole document, 
camel://status/<pid>/<section>
    for one top-level section, with resources/list, resources/read and a URI
    template) so an external agent can fetch exactly the section it needs.
    
    The F8 agent loop only speaks tools, so add tui_get_status(section, pid) to
    the core set; section='sections' lists what the document contains. Also add
    a guideline that the panel's own tool calls are recorded in the AI log, not
    the MCP log, since the model claimed there was no way to see them.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-tui.adoc        |  18 +++
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |   3 +
 .../dsl/jbang/core/commands/tui/McpFacade.java     |  20 +++
 .../jbang/core/commands/tui/StatusFileReader.java  |  86 +++++++++++++
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  | 137 ++++++++++++++++++++-
 .../jbang/core/commands/tui/TuiToolRegistry.java   |  49 +++++++-
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   |   3 +-
 .../core/commands/tui/StatusFileReaderTest.java    |  69 +++++++++++
 .../commands/tui/TuiMcpServerResourcesTest.java    |  78 ++++++++++++
 9 files changed, 460 insertions(+), 3 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
index b5716f7eceb6..8039f5133e04 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui.adoc
@@ -872,6 +872,24 @@ The server is bound to `127.0.0.1` only -- it never 
listens on external interfac
 When MCP is active, the TUI footer shows the connection status.
 Use *F2* -> _MCP Info_ to see server details and _MCP Log_ to view the tool 
call history.
 
+=== Status document resources
+
+Every running integration keeps a full status document in 
`~/.camel/<pid>-status.json`, written
+once a second by `camel-cli-connector`. The tabs show a digest of it; the MCP 
server exposes the
+whole document as resources so an agent can fetch exactly the section it needs 
instead of
+paging through tabs:
+
+* `camel://status/<pid>` -- the whole document
+* `camel://status/<pid>/<section>` -- one top-level section, for example 
`context` (name, version,
+  state, uptime, start timestamp, statistics), `runtime`, `routes`, 
`endpoints`, `healthChecks`,
+  `properties`, `main-configuration`, `routeController`, `memory`, `threads`, 
`gc` or `events`
+
+`resources/list` enumerates the document and its sections for every monitored 
integration, and a
+`resources/templates/list` template describes the URI shape. The same data is 
available to the
+F8 AI panel (and as an MCP tool) through `tui_get_status`, which takes a 
`section` argument and
+returns that section only; pass `sections` to list what the document contains. 
Sections such as
+`events` can be large, so ask for the one you need.
+
 === AI panel slash commands
 
 When the AI panel is open, input that starts with `/` runs a local panel 
command instead of sending a question to the configured AI provider. 
`/provider` and `/model` are unavailable while a response or command is already 
in progress; the panel shows a message asking you to wait.
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 0ec870f14ef1..a640ec44cc02 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -1787,6 +1787,9 @@ class AiPanel {
         sb.append("- Call tui_get_options only when unsure which tab holds the 
data you need\n");
         sb.append("- Use tui_get_state to learn which integration and tab is 
selected, if the question depends on it\n");
         sb.append("- Use tui_get_processor_detail to explain what a route does 
or how its steps are configured\n");
+        sb.append("- Use tui_get_status for data no tab shows (context, 
runtime, health checks, properties, ...)\n");
+        sb.append("- Your own tool calls are recorded in the AI log 
(tui_get_ai_log, F2 -> AI Log); ");
+        sb.append("the MCP log only records external clients\n");
         sb.append("- Be concise and actionable in your answers\n");
         sb.append("- When something looks wrong, explain what it means and 
suggest fixes\n");
         sb.append("- For stopping routes or applications, use tui_control for 
graceful shutdown\n");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 3fc3a2c2f016..a39bcf1666bd 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -107,6 +107,7 @@ class McpFacade {
     private final MonitorBridge bridge;
 
     private volatile Supplier<List<AiPanel.LogEntry>> aiActivityLog;
+    private volatile StatusFileReader statusFiles = 
StatusFileReader.defaultReader();
     private volatile Supplier<List<TuiMcpServer.LogEntry>> mcpActivityLog;
     private volatile Supplier<Integer> mcpToolCallCount;
 
@@ -340,6 +341,25 @@ class McpFacade {
         return tab != null ? tab.isDetailFocused() : null;
     }
 
+    /**
+     * Reader for the per-process status documents; replaceable so tests can 
point it at a temporary directory.
+     */
+    StatusFileReader statusFiles() {
+        return statusFiles;
+    }
+
+    void setStatusFiles(StatusFileReader statusFiles) {
+        this.statusFiles = statusFiles;
+    }
+
+    /**
+     * Integrations currently monitored (vanished processes excluded), for 
callers that need pid and name.
+     */
+    List<IntegrationInfo> liveIntegrations() {
+        List<IntegrationInfo> all = data.get();
+        return all == null ? List.of() : all.stream().filter(i -> 
!i.vanishing).toList();
+    }
+
     List<String> getIntegrationNames() {
         return data.get().stream()
                 .filter(i -> !i.vanishing)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReader.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReader.java
new file mode 100644
index 000000000000..906ebaafc919
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReader.java
@@ -0,0 +1,86 @@
+/*
+ * 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.tui;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Reads the full status document that camel-cli-connector writes for each 
running integration to
+ * {@code ~/.camel/<pid>-status.json}. The tabs show a digest of it; the AI 
tools and MCP resources hand out whole
+ * top-level sections so a model can fetch exactly the piece it needs.
+ */
+final class StatusFileReader {
+
+    static final String SECTION_LIST = "sections";
+
+    private final Path camelDir;
+
+    StatusFileReader(Path camelDir) {
+        this.camelDir = camelDir;
+    }
+
+    static StatusFileReader defaultReader() {
+        return new StatusFileReader(CommandLineHelper.getCamelDir());
+    }
+
+    Path statusFile(String pid) {
+        return camelDir.resolve(pid + "-status.json");
+    }
+
+    /**
+     * The parsed document, or {@code null} when there is no readable status 
file for the pid.
+     */
+    JsonObject read(String pid) {
+        if (pid == null || pid.isBlank()) {
+            return null;
+        }
+        Path file = statusFile(pid.trim());
+        if (!Files.isRegularFile(file)) {
+            return null;
+        }
+        try {
+            String text = Files.readString(file, StandardCharsets.UTF_8);
+            return (JsonObject) Jsoner.deserialize(text);
+        } catch (Exception e) {
+            // the connector rewrites the file every second, a torn read is 
expected now and then
+            return null;
+        }
+    }
+
+    /**
+     * Top-level section names of the document, in file order, or an empty 
list when it cannot be read.
+     */
+    List<String> sections(String pid) {
+        JsonObject doc = read(pid);
+        return doc == null ? List.of() : List.copyOf(doc.keySet());
+    }
+
+    /**
+     * One top-level section, or {@code null} when the document or the section 
does not exist.
+     */
+    Object section(String pid, String section) {
+        JsonObject doc = read(pid);
+        return doc == null || section == null ? null : doc.get(section);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index 82fc6eed794c..25edf336c4c5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -177,11 +177,16 @@ class TuiMcpServer {
                 case "initialize" -> handleInitialize(request);
                 case "tools/list" -> handleToolsList();
                 case "tools/call" -> handleToolsCall(request);
+                case "resources/list" -> handleResourcesList();
+                case "resources/templates/list" -> 
handleResourceTemplatesList();
+                case "resources/read" -> handleResourcesRead(request);
                 case "ping" -> new JsonObject();
                 default -> null;
             };
 
-            if (result == null) {
+            if (result == null && "resources/read".equals(jsonrpcMethod)) {
+                sendError(exchange, request, -32002, "Resource not found: " + 
requestedUri(request));
+            } else if (result == null) {
                 sendError(exchange, request, -32601, "Method not found: " + 
jsonrpcMethod);
             } else {
                 String responseJson = sendResult(exchange, request, result);
@@ -224,6 +229,9 @@ class TuiMcpServer {
                 log(LogLevel.TOOL, "Tool call: " + toolName, requestBody, 
responseBody);
             }
             case "tools/list" -> log(LogLevel.INFO, "Tools listed", 
requestBody, responseBody);
+            case "resources/list" -> log(LogLevel.INFO, "Resources listed", 
requestBody, responseBody);
+            case "resources/templates/list" -> log(LogLevel.INFO, "Resource 
templates listed", requestBody, responseBody);
+            case "resources/read" -> log(LogLevel.TOOL, "Resource read: " + 
requestedUri(request), requestBody, responseBody);
             case "ping" -> log(LogLevel.INFO, "Ping", requestBody, 
responseBody);
             default -> log(LogLevel.INFO, jsonrpcMethod, requestBody, 
responseBody);
         }
@@ -250,6 +258,10 @@ class TuiMcpServer {
         JsonObject tools = new JsonObject();
         tools.put("listChanged", false);
         capabilities.put("tools", tools);
+        JsonObject resources = new JsonObject();
+        resources.put("subscribe", false);
+        resources.put("listChanged", false);
+        capabilities.put("resources", resources);
         result.put("capabilities", capabilities);
 
         return result;
@@ -269,6 +281,129 @@ class TuiMcpServer {
         return result;
     }
 
+    // ---- Resources: the per-process status document, whole or one section 
at a time ----
+
+    static final String STATUS_URI_PREFIX = "camel://status/";
+    private static final String JSON_MIME = "application/json";
+
+    /**
+     * Address of a status resource: {@code camel://status/<pid>} for the 
whole document, or
+     * {@code camel://status/<pid>/<section>} for one top-level section.
+     */
+    record StatusUri(String pid, String section) {
+
+        String uri() {
+            return STATUS_URI_PREFIX + pid + (section != null ? "/" + section 
: "");
+        }
+
+        /**
+         * Parses a resource URI, or returns {@code null} when it is not a 
status URI.
+         */
+        static StatusUri parse(String uri) {
+            if (uri == null || !uri.startsWith(STATUS_URI_PREFIX)) {
+                return null;
+            }
+            String rest = uri.substring(STATUS_URI_PREFIX.length());
+            if (rest.isEmpty()) {
+                return null;
+            }
+            int slash = rest.indexOf('/');
+            String pid = slash < 0 ? rest : rest.substring(0, slash);
+            String section = slash < 0 ? null : rest.substring(slash + 1);
+            if (pid.isEmpty() || !pid.chars().allMatch(Character::isDigit)
+                    || (section != null && (section.isEmpty() || 
section.contains("/")))) {
+                return null;
+            }
+            return new StatusUri(pid, section);
+        }
+    }
+
+    private JsonObject handleResourcesList() {
+        JsonObject result = new JsonObject();
+        result.put("resources", buildResourceList(facade.liveIntegrations(), 
facade.statusFiles()));
+        return result;
+    }
+
+    /**
+     * One resource per monitored integration for the whole document, followed 
by one per top-level section.
+     */
+    static JsonArray buildResourceList(List<IntegrationInfo> integrations, 
StatusFileReader reader) {
+        JsonArray resources = new JsonArray();
+        for (IntegrationInfo info : integrations) {
+            if (info.pid == null) {
+                continue;
+            }
+            List<String> sections = reader.sections(info.pid);
+            if (sections.isEmpty()) {
+                continue;
+            }
+            String name = info.name != null ? info.name : info.pid;
+            resources.add(resource(new StatusUri(info.pid, null).uri(), name + 
" status",
+                    "Full status document of " + name + " (PID " + info.pid + 
"): " + String.join(", ", sections)));
+            for (String section : sections) {
+                resources.add(resource(new StatusUri(info.pid, section).uri(), 
name + " " + section,
+                        "The '" + section + "' section of the status document 
of " + name + " (PID " + info.pid + ")"));
+            }
+        }
+        return resources;
+    }
+
+    private static JsonObject resource(String uri, String name, String 
description) {
+        JsonObject resource = new JsonObject();
+        resource.put("uri", uri);
+        resource.put("name", name);
+        resource.put("description", description);
+        resource.put("mimeType", JSON_MIME);
+        return resource;
+    }
+
+    private JsonObject handleResourceTemplatesList() {
+        JsonObject template = new JsonObject();
+        template.put("uriTemplate", STATUS_URI_PREFIX + "{pid}/{section}");
+        template.put("name", "Integration status section");
+        template.put("description", "One top-level section of the status 
document that camel-cli-connector writes "
+                                    + "for the integration with the given PID; 
omit /{section} for the whole document. "
+                                    + "Sections include context, runtime, 
routes, endpoints, healthChecks, properties, "
+                                    + "main-configuration, memory, threads, gc 
and events.");
+        template.put("mimeType", JSON_MIME);
+        JsonArray templates = new JsonArray();
+        templates.add(template);
+        JsonObject result = new JsonObject();
+        result.put("resourceTemplates", templates);
+        return result;
+    }
+
+    /**
+     * Returns the resource contents, or {@code null} when the URI does not 
name a readable status document or section
+     * (the caller turns that into a "resource not found" error).
+     */
+    private JsonObject handleResourcesRead(JsonObject request) {
+        StatusUri uri = StatusUri.parse(requestedUri(request));
+        if (uri == null) {
+            return null;
+        }
+        Object payload = uri.section() == null
+                ? facade.statusFiles().read(uri.pid())
+                : facade.statusFiles().section(uri.pid(), uri.section());
+        if (payload == null) {
+            return null;
+        }
+        JsonObject content = new JsonObject();
+        content.put("uri", uri.uri());
+        content.put("mimeType", JSON_MIME);
+        content.put("text", Jsoner.serialize(payload));
+        JsonArray contents = new JsonArray();
+        contents.add(content);
+        JsonObject result = new JsonObject();
+        result.put("contents", contents);
+        return result;
+    }
+
+    private static String requestedUri(JsonObject request) {
+        JsonObject params = (JsonObject) request.get("params");
+        return params != null && params.get("uri") instanceof String s ? s : 
null;
+    }
+
     @SuppressWarnings("unchecked")
     private JsonObject handleToolsCall(JsonObject request) {
         JsonObject params = (JsonObject) request.get("params");
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
index 03bbb0fc4caf..09bd8c1bcb21 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiToolRegistry.java
@@ -89,7 +89,7 @@ class TuiToolRegistry {
             "tui_get_state", "tui_get_options", "tui_get_table", 
"tui_get_log", "tui_get_errors",
             "tui_get_diagram", "tui_get_topology", "tui_get_processor_detail", 
"tui_catalog_doc",
             "tui_get_history", "tui_get_spans", "tui_control", 
"tui_send_message", "tui_get_files",
-            "tui_get_readme", "tui_navigate", "tui_set_log_level", 
"tui_filter");
+            "tui_get_readme", "tui_navigate", "tui_set_log_level", 
"tui_filter", "tui_get_status");
 
     /**
      * Returns all tool definitions. The result is cached since it is 
immutable.
@@ -119,6 +119,7 @@ class TuiToolRegistry {
             case "tui_get_screen" -> callGetScreen(args);
             case "tui_get_events" -> callGetEvents(args);
             case "tui_get_state" -> callGetState();
+            case "tui_get_status" -> callGetStatus(args);
             case "tui_show_caption" -> callShowCaption(args);
             case "tui_navigate" -> callNavigate(args);
             case "tui_send_keys" -> callSendKeys(args);
@@ -392,6 +393,21 @@ class TuiToolRegistry {
                         "Tab name to get data from (e.g. 'Routes', 
'Endpoints', 'Kafka'). "
                                                 + "Use tui_get_options to 
discover available tab names. "
                                                 + "If omitted, uses the active 
tab.")))));
+        tools.add(toToolDef(toolDef(
+                "tui_get_status",
+                "Returns one top-level section of the integration's full 
status document, the same JSON the Camel "
+                                  + "CLI reads from 
~/.camel/<pid>-status.json. Use it for data the tabs do not show: "
+                                  + "'context' (name, version, state, uptime 
in millis, startTimestamp, statistics), "
+                                  + "'runtime' (pid, directory, java version), 
'healthChecks', 'properties', "
+                                  + "'main-configuration', 'routeController', 
'services', 'transformers', 'rests', "
+                                  + "'consumers', 'producers', 'endpoints', 
'dataSources', 'memory', 'threads', 'gc', "
+                                  + "'classLoading', 'trace', 'events'. Pass 
section='sections' to list what the "
+                                  + "document contains. Sections can be large, 
so request only the one you need.",
+                Map.of("section", propDef("string",
+                        "Top-level section name, or 'sections' to list the 
available names"),
+                        "pid", propDef("string",
+                                "Process id of the integration; defaults to 
the selected integration")),
+                List.of("section"))));
         tools.add(toToolDef(toolDef(
                 "tui_action",
                 "Invokes a TUI action by name, bypassing fragile key 
sequences. "
@@ -1335,6 +1351,37 @@ class TuiToolRegistry {
         return Jsoner.serialize(data);
     }
 
+    private String callGetStatus(Map<String, Object> args) {
+        String section = args.get("section") instanceof String s ? s.trim() : 
"";
+        if (section.isEmpty()) {
+            return "Error: section is required (use 'sections' to list the 
available names)";
+        }
+        String pid = args.get("pid") instanceof String s && !s.isBlank() ? 
s.trim() : facade.getSelectedPid();
+        if (pid == null || pid.isBlank()) {
+            return "No integration selected";
+        }
+        StatusFileReader reader = facade.statusFiles();
+        List<String> sections = reader.sections(pid);
+        if (sections.isEmpty()) {
+            return "No status document available for PID " + pid;
+        }
+        if (StatusFileReader.SECTION_LIST.equals(section)) {
+            JsonObject result = new JsonObject();
+            result.put("pid", pid);
+            result.put("sections", new JsonArray(sections));
+            return Jsoner.serialize(result);
+        }
+        Object value = reader.section(pid, section);
+        if (value == null) {
+            return "Unknown section '" + section + "' for PID " + pid + ". 
Available: " + String.join(", ", sections);
+        }
+        JsonObject result = new JsonObject();
+        result.put("pid", pid);
+        result.put("section", section);
+        result.put("data", value);
+        return Jsoner.serialize(result);
+    }
+
     private String callAction(Map<String, Object> args) {
         String action = (String) args.get("action");
         if (action == null || action.isBlank()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 51b912a2564f..ad71a7d2ba4d 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -808,7 +808,8 @@ class AiPanelTest {
         assertTrue(panel.toolDefinitionsForTesting().stream()
                 .allMatch(def -> 
TuiToolRegistry.CORE_TOOLS.contains(def.name())));
         assertFalse(panel.systemPromptForTesting().contains("tui_draw_shape"));
-        assertTrue(panel.describeToolModeForTesting().startsWith("core (18 of 
"));
+        assertTrue(panel.describeToolModeForTesting()
+                .startsWith("core (" + TuiToolRegistry.CORE_TOOLS.size() + " 
of "));
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReaderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReaderTest.java
new file mode 100644
index 000000000000..c75cd7166739
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusFileReaderTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StatusFileReaderTest {
+
+    private static final String STATUS = """
+            {"runtime":{"pid":4711,"directory":"/tmp/app"},
+             
"context":{"name":"timer-log","uptime":262307,"startTimestamp":1788879661524},
+             "routes":[{"routeId":"route1","state":"Started"}]}
+            """;
+
+    @Test
+    void readsSectionsInFileOrder(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("4711-status.json"), STATUS);
+        StatusFileReader reader = new StatusFileReader(dir);
+
+        assertEquals(List.of("runtime", "context", "routes"), 
reader.sections("4711"));
+    }
+
+    @Test
+    void returnsOneSectionOrNull(@TempDir Path dir) throws Exception {
+        Files.writeString(dir.resolve("4711-status.json"), STATUS);
+        StatusFileReader reader = new StatusFileReader(dir);
+
+        JsonObject context = (JsonObject) reader.section("4711", "context");
+        assertEquals("timer-log", context.get("name"));
+        assertEquals(262307L, ((Number) context.get("uptime")).longValue());
+        assertNull(reader.section("4711", "nope"));
+        assertNull(reader.section("4711", null));
+    }
+
+    @Test
+    void missingOrCorruptFilesReadAsAbsent(@TempDir Path dir) throws Exception 
{
+        Files.writeString(dir.resolve("99-status.json"), "{ not json");
+        StatusFileReader reader = new StatusFileReader(dir);
+
+        assertNull(reader.read("4711"));
+        assertNull(reader.read("99"));
+        assertNull(reader.read(" "));
+        assertTrue(reader.sections("4711").isEmpty());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerResourcesTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerResourcesTest.java
new file mode 100644
index 000000000000..9d6bf34720e8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServerResourcesTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.tui;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class TuiMcpServerResourcesTest {
+
+    @Test
+    void parsesWholeDocumentAndSectionUris() {
+        TuiMcpServer.StatusUri whole = 
TuiMcpServer.StatusUri.parse("camel://status/4711");
+        assertEquals("4711", whole.pid());
+        assertNull(whole.section());
+        assertEquals("camel://status/4711", whole.uri());
+
+        TuiMcpServer.StatusUri section = 
TuiMcpServer.StatusUri.parse("camel://status/4711/main-configuration");
+        assertEquals("4711", section.pid());
+        assertEquals("main-configuration", section.section());
+        assertEquals("camel://status/4711/main-configuration", section.uri());
+    }
+
+    @Test
+    void rejectsUrisThatAreNotStatusResources() {
+        assertNull(TuiMcpServer.StatusUri.parse(null));
+        assertNull(TuiMcpServer.StatusUri.parse("file:///etc/passwd"));
+        assertNull(TuiMcpServer.StatusUri.parse("camel://status/"));
+        assertNull(TuiMcpServer.StatusUri.parse("camel://status/abc"));
+        assertNull(TuiMcpServer.StatusUri.parse("camel://status/4711/"));
+        assertNull(TuiMcpServer.StatusUri.parse("camel://status/4711/a/b"));
+        assertNull(TuiMcpServer.StatusUri.parse("camel://status/../4711"));
+    }
+
+    @Test
+    void listsWholeDocumentThenEachSectionPerIntegration(@TempDir Path dir) 
throws Exception {
+        Files.writeString(dir.resolve("4711-status.json"),
+                
"{\"runtime\":{\"pid\":4711},\"context\":{\"name\":\"timer-log\"}}");
+        IntegrationInfo running = new IntegrationInfo();
+        running.pid = "4711";
+        running.name = "timer-log";
+        IntegrationInfo gone = new IntegrationInfo();
+        gone.pid = "4712";
+        gone.name = "no-file";
+
+        JsonArray resources = TuiMcpServer.buildResourceList(List.of(running, 
gone), new StatusFileReader(dir));
+
+        assertEquals(3, resources.size());
+        JsonObject whole = (JsonObject) resources.get(0);
+        assertEquals("camel://status/4711", whole.get("uri"));
+        assertEquals("timer-log status", whole.get("name"));
+        assertEquals("application/json", whole.get("mimeType"));
+        assertEquals("camel://status/4711/runtime", ((JsonObject) 
resources.get(1)).get("uri"));
+        assertEquals("camel://status/4711/context", ((JsonObject) 
resources.get(2)).get("uri"));
+    }
+}

Reply via email to