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

davsclaus pushed a commit to branch tui-mcp-CAMEL-23606
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d3bcfdab6af53a4df8a1d6dd545211c458c5d209
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun May 24 22:52:59 2026 +0200

    CAMEL-23606: camel-tui - MCP log master/detail view with request/response 
capture
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 182 ++++++++++++++++-----
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  |  49 ++++--
 2 files changed, 182 insertions(+), 49 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index 44d40c48c2b8..e04744fcdb82 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -49,6 +49,7 @@ import org.apache.camel.dsl.jbang.core.common.ExampleHelper;
 import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
 import org.apache.camel.dsl.jbang.core.common.PathUtils;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
 import static 
org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hintLast;
@@ -97,6 +98,11 @@ class ActionsPopup {
     private String docTitle;
     private int docScroll;
 
+    private boolean showMcpLog;
+    private List<TuiMcpServer.LogEntry> mcpLogEntries;
+    private int mcpLogSelected;
+    private int mcpLogDetailScroll;
+
     private final DoctorPopup doctorPopup = new DoctorPopup();
     private final ClasspathPopup classpathPopup = new ClasspathPopup();
     private final StopAllPopup stopAllPopup;
@@ -137,7 +143,7 @@ class ActionsPopup {
 
     boolean isVisible() {
         return showActionsMenu || showExampleBrowser || 
runOptionsForm.isVisible() || showDocPicker || showDocViewer
-                || doctorPopup.isVisible() || classpathPopup.isVisible()
+                || showMcpLog || doctorPopup.isVisible() || 
classpathPopup.isVisible()
                 || stopAllPopup.isVisible() || captionOverlay.isInputVisible();
     }
 
@@ -152,6 +158,7 @@ class ActionsPopup {
         runOptionsForm.close();
         showDocPicker = false;
         showDocViewer = false;
+        showMcpLog = false;
         doctorPopup.close();
         classpathPopup.close();
         stopAllPopup.close();
@@ -167,6 +174,26 @@ class ActionsPopup {
     }
 
     boolean handleKeyEvent(KeyEvent ke) {
+        if (showMcpLog) {
+            if (ke.isCancel()) {
+                showMcpLog = false;
+            } else if (ke.isUp() || ke.isChar('k')) {
+                if (mcpLogEntries != null && !mcpLogEntries.isEmpty()) {
+                    mcpLogSelected = Math.max(0, mcpLogSelected - 1);
+                    mcpLogDetailScroll = 0;
+                }
+            } else if (ke.isDown() || ke.isChar('j')) {
+                if (mcpLogEntries != null && !mcpLogEntries.isEmpty()) {
+                    mcpLogSelected = Math.min(mcpLogEntries.size() - 1, 
mcpLogSelected + 1);
+                    mcpLogDetailScroll = 0;
+                }
+            } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+                mcpLogDetailScroll = Math.max(0, mcpLogDetailScroll - 5);
+            } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+                mcpLogDetailScroll += 5;
+            }
+            return true;
+        }
         if (showDocViewer) {
             if (ke.isCancel()) {
                 showDocViewer = false;
@@ -307,6 +334,9 @@ class ActionsPopup {
         if (showDocViewer) {
             renderDocViewer(frame, area);
         }
+        if (showMcpLog) {
+            renderMcpLog(frame, area);
+        }
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
         }
@@ -338,6 +368,12 @@ class ActionsPopup {
             doctorPopup.renderFooter(spans);
             return;
         }
+        if (showMcpLog) {
+            hint(spans, "↑↓", "select");
+            hint(spans, "PgUp/Dn", "scroll detail");
+            hintLast(spans, "Esc", "back");
+            return;
+        }
         if (showDocViewer) {
             hint(spans, "↑↓", "scroll");
             hintLast(spans, "Esc", "back");
@@ -732,45 +768,113 @@ class ActionsPopup {
     }
 
     private void openMcpLog() {
-        List<TuiMcpServer.LogEntry> entries = mcpActivityLog != null ? 
mcpActivityLog.get() : List.of();
-        if (entries.isEmpty()) {
-            docContent = "No MCP activity yet.";
-            docLines = null;
-        } else {
-            docContent = null;
-            List<Line> lines = new ArrayList<>();
-            for (TuiMcpServer.LogEntry entry : entries) {
-                String levelTag;
-                Style levelStyle;
-                switch (entry.level()) {
-                    case CONNECT:
-                        levelTag = " CONNECT ";
-                        levelStyle = Style.EMPTY.fg(Color.GREEN);
-                        break;
-                    case TOOL:
-                        levelTag = " TOOL    ";
-                        levelStyle = Style.EMPTY.fg(Color.CYAN);
-                        break;
-                    case ERROR:
-                        levelTag = " ERROR   ";
-                        levelStyle = Style.EMPTY.fg(Color.LIGHT_RED);
-                        break;
-                    default:
-                        levelTag = " INFO    ";
-                        levelStyle = Style.EMPTY.fg(Color.GREEN);
-                        break;
-                }
-                lines.add(Line.from(
-                        Span.styled(entry.timestamp(), Style.EMPTY.dim()),
-                        Span.styled(levelTag, levelStyle),
-                        Span.raw(entry.message())));
-            }
-            docLines = lines;
+        mcpLogEntries = mcpActivityLog != null ? mcpActivityLog.get() : 
List.of();
+        mcpLogSelected = mcpLogEntries.isEmpty() ? 0 : mcpLogEntries.size() - 
1;
+        mcpLogDetailScroll = 0;
+        showMcpLog = true;
+    }
+
+    private void renderMcpLog(Frame frame, Rect area) {
+        Rect popup = new Rect(area.left() + 2, area.top() + 1, area.width() - 
4, area.height() - 2);
+        frame.renderWidget(Clear.INSTANCE, popup);
+
+        if (mcpLogEntries == null || mcpLogEntries.isEmpty()) {
+            Block block = Block.builder()
+                    .borderType(BorderType.ROUNDED)
+                    .title(" MCP Log ")
+                    .titleBottom(Title.from(Line.from(
+                            Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
+                    .build();
+            frame.renderWidget(block, popup);
+            Rect inner = block.inner(popup);
+            frame.renderWidget(Paragraph.from(Line.from(
+                    Span.styled("No MCP activity yet.", Style.EMPTY.dim()))), 
inner);
+            return;
+        }
+
+        int splitY = popup.top() + Math.max(3, (popup.height() * 2) / 5);
+        Rect masterArea = new Rect(popup.left(), popup.top(), popup.width(), 
splitY - popup.top());
+        Rect detailArea = new Rect(popup.left(), splitY, popup.width(), 
popup.bottom() - splitY);
+
+        // Master: log entry list
+        List<ListItem> items = new ArrayList<>();
+        for (TuiMcpServer.LogEntry entry : mcpLogEntries) {
+            Style levelStyle = switch (entry.level()) {
+                case CONNECT -> Style.EMPTY.fg(Color.GREEN);
+                case TOOL -> Style.EMPTY.fg(Color.CYAN);
+                case ERROR -> Style.EMPTY.fg(Color.LIGHT_RED);
+                default -> Style.EMPTY.fg(Color.GREEN);
+            };
+            String levelTag = switch (entry.level()) {
+                case CONNECT -> " CONNECT ";
+                case TOOL -> " TOOL    ";
+                case ERROR -> " ERROR   ";
+                default -> " INFO    ";
+            };
+            items.add(ListItem.from(Line.from(
+                    Span.styled(entry.timestamp(), Style.EMPTY.dim()),
+                    Span.styled(levelTag, levelStyle),
+                    Span.raw(entry.message()))));
+        }
+
+        ListState masterState = new ListState();
+        masterState.select(mcpLogSelected);
+        ListWidget list = ListWidget.builder()
+                .items(items.toArray(ListItem[]::new))
+                .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
+                .highlightSymbol("▸ ")
+                .scrollMode(ScrollMode.AUTO_SCROLL)
+                .block(Block.builder()
+                        .borderType(BorderType.ROUNDED)
+                        .title(" MCP Log ")
+                        .build())
+                .build();
+        frame.renderStatefulWidget(list, masterArea, masterState);
+
+        // Detail: request + response JSON
+        TuiMcpServer.LogEntry selected = mcpLogEntries.get(mcpLogSelected);
+        List<Line> detailLines = new ArrayList<>();
+        if (selected.requestBody() != null) {
+            detailLines.add(Line.from(Span.styled("▶ Request", 
Style.EMPTY.fg(Color.YELLOW).bold())));
+            addJsonLines(detailLines, selected.requestBody());
+            detailLines.add(Line.from(Span.raw("")));
+        }
+        if (selected.responseBody() != null) {
+            detailLines.add(Line.from(Span.styled("◀ Response", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            addJsonLines(detailLines, selected.responseBody());
+        }
+        if (selected.requestBody() == null && selected.responseBody() == null) 
{
+            detailLines.add(Line.from(Span.styled("(no request/response 
data)", Style.EMPTY.dim())));
+        }
+
+        Block detailBlock = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" Detail ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" select │"),
+                        Span.styled(" PgUp/Dn", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" scroll │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
+                .build();
+        frame.renderWidget(detailBlock, detailArea);
+        Rect detailInner = detailBlock.inner(detailArea);
+
+        int visibleLines = detailInner.height();
+        int totalLines = detailLines.size();
+        int clampedScroll = Math.min(mcpLogDetailScroll, Math.max(0, 
totalLines - visibleLines));
+        int end = Math.min(clampedScroll + visibleLines, totalLines);
+        if (clampedScroll < end) {
+            List<Line> visible = detailLines.subList(clampedScroll, end);
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+                    detailInner);
+        }
+    }
+
+    private static void addJsonLines(List<Line> lines, String json) {
+        String pretty = Jsoner.prettyPrint(json, 2);
+        for (String line : pretty.split("\n", -1)) {
+            lines.add(Line.from(Span.styled("  " + line, Style.EMPTY.dim())));
         }
-        docTitle = "MCP Log";
-        docScroll = 0;
-        showDocViewer = true;
-        docViewerFromExampleBrowser = false;
     }
 
     private void openClasspath() {
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 914b22311f4e..d094c7a21757 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
@@ -62,7 +62,7 @@ class TuiMcpServer {
         ERROR
     }
 
-    record LogEntry(String timestamp, LogLevel level, String message) {
+    record LogEntry(String timestamp, LogLevel level, String message, String 
requestBody, String responseBody) {
     }
 
     private final int port;
@@ -104,7 +104,11 @@ class TuiMcpServer {
     }
 
     private synchronized void log(LogLevel level, String message) {
-        activityLog.add(new LogEntry(TIME_FMT.format(Instant.now()), level, 
message));
+        log(level, message, null, null);
+    }
+
+    private synchronized void log(LogLevel level, String message, String 
requestBody, String responseBody) {
+        activityLog.add(new LogEntry(TIME_FMT.format(Instant.now()), level, 
message, requestBody, responseBody));
         if (activityLog.size() > MAX_LOG_ENTRIES) {
             activityLog.remove(0);
         }
@@ -160,7 +164,8 @@ class TuiMcpServer {
             if (result == null) {
                 sendError(exchange, request, -32601, "Method not found: " + 
jsonrpcMethod);
             } else {
-                sendResult(exchange, request, result);
+                String responseJson = sendResult(exchange, request, result);
+                logMethodCall(jsonrpcMethod, request, body, responseJson);
             }
         } catch (Exception e) {
             exchange.sendResponseHeaders(500, -1);
@@ -169,6 +174,30 @@ class TuiMcpServer {
         }
     }
 
+    private void logMethodCall(String jsonrpcMethod, JsonObject request, 
String requestBody, String responseBody) {
+        switch (jsonrpcMethod) {
+            case "initialize" -> {
+                JsonObject params = (JsonObject) request.get("params");
+                String name = "unknown";
+                if (params != null) {
+                    JsonObject clientInfo = (JsonObject) 
params.get("clientInfo");
+                    if (clientInfo != null && clientInfo.get("name") != null) {
+                        name = (String) clientInfo.get("name");
+                    }
+                }
+                log(LogLevel.CONNECT, "Client connected: " + name, 
requestBody, responseBody);
+            }
+            case "tools/call" -> {
+                JsonObject params = (JsonObject) request.get("params");
+                String toolName = params != null ? (String) params.get("name") 
: "unknown";
+                log(LogLevel.TOOL, "Tool call: " + toolName, requestBody, 
responseBody);
+            }
+            case "tools/list" -> log(LogLevel.INFO, "Tools listed", 
requestBody, responseBody);
+            case "ping" -> log(LogLevel.INFO, "Ping", requestBody, 
responseBody);
+            default -> log(LogLevel.INFO, jsonrpcMethod, requestBody, 
responseBody);
+        }
+    }
+
     private JsonObject handleInitialize(JsonObject request) {
         JsonObject params = (JsonObject) request.get("params");
         if (params != null) {
@@ -177,7 +206,6 @@ class TuiMcpServer {
                 clientName = (String) clientInfo.get("name");
             }
         }
-        log(LogLevel.CONNECT, "Client connected: " + (clientName != null ? 
clientName : "unknown"));
 
         JsonObject result = new JsonObject();
         result.put("protocolVersion", PROTOCOL_VERSION);
@@ -222,7 +250,7 @@ class TuiMcpServer {
                 Map.of("text", propDef("string", "The caption text to 
display"),
                         "duration", propDef("integer",
                                 "Auto-dismiss after this many seconds. Caption 
won't block key events. "
-                                                        + "If omitted, caption 
stays until dismissed by a key press.")),
+                                                       + "If omitted, caption 
stays until dismissed by a key press.")),
                 List.of("text")));
         toolList.add(toolDef(
                 "tui_navigate",
@@ -258,7 +286,6 @@ class TuiMcpServer {
         }
 
         lastToolCallTime = System.currentTimeMillis();
-        log(LogLevel.TOOL, "Tool call: " + toolName);
 
         String text;
         boolean isError = false;
@@ -436,12 +463,12 @@ class TuiMcpServer {
 
     // --- JSON-RPC helpers ---
 
-    private void sendResult(HttpExchange exchange, JsonObject request, 
JsonObject result) throws IOException {
+    private String sendResult(HttpExchange exchange, JsonObject request, 
JsonObject result) throws IOException {
         JsonObject response = new JsonObject();
         response.put("jsonrpc", "2.0");
         response.put("id", request.get("id"));
         response.put("result", result);
-        sendJson(exchange, 200, response);
+        return sendJson(exchange, 200, response);
     }
 
     private void sendError(HttpExchange exchange, JsonObject request, int 
code, String message) throws IOException {
@@ -456,13 +483,15 @@ class TuiMcpServer {
         sendJson(exchange, 200, response);
     }
 
-    private void sendJson(HttpExchange exchange, int status, JsonObject json) 
throws IOException {
-        byte[] bytes = Jsoner.serialize(json).getBytes(StandardCharsets.UTF_8);
+    private String sendJson(HttpExchange exchange, int status, JsonObject 
json) throws IOException {
+        String serialized = Jsoner.serialize(json);
+        byte[] bytes = serialized.getBytes(StandardCharsets.UTF_8);
         exchange.getResponseHeaders().set("Content-Type", "application/json");
         exchange.sendResponseHeaders(status, bytes.length);
         try (OutputStream os = exchange.getResponseBody()) {
             os.write(bytes);
         }
+        return serialized;
     }
 
     // --- Tool definition helpers ---

Reply via email to