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


The following commit(s) were added to refs/heads/main by this push:
     new 0db40eea7da1 CAMEL-23606: camel-tui - embedded MCP server for AI agent 
observability (#23494)
0db40eea7da1 is described below

commit 0db40eea7da1f9fc7af5cffbbed95da1ca206ae5
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon May 25 20:37:24 2026 +0200

    CAMEL-23606: camel-tui - embedded MCP server for AI agent observability 
(#23494)
    
    * CAMEL-23606: camel-tui - embedded MCP server for AI agent observability
    
    Adds an embedded MCP (Model Context Protocol) server to the Camel TUI,
    allowing AI coding agents to observe the live terminal session. Uses JDK's
    built-in HttpServer with the Streamable HTTP transport (spec 2025-03-26),
    requiring zero new dependencies.
    
    Phase 1 provides three observation tools:
    - tui_get_screen: returns current screen content (plain text or ANSI)
    - tui_get_events: returns recent key presses and navigation events
    - tui_get_state: returns active tab, selected integration, etc.
    
    Features:
    - --mcp flag to enable, --mcp-port to configure (default 8123)
    - Auto-generates .mcp.json for zero-config Claude Code discovery
    - Footer indicator with connected client name and activity dot
    - F2 menu: MCP Info (setup guide) and MCP Log (colored activity log)
    - Graceful handling when port is already in use
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP caption tool and inline caption mode
    
    Adds tui_show_caption MCP tool so AI agents can display messages on the
    TUI screen with the typewriter animation. Also adds Ctrl+Shift+T inline
    caption mode where keystrokes render directly on screen without a dialog.
    
    - tui_show_caption: text parameter (required), triggers caption display
    - Ctrl+Shift+T: live typing mode with blinking cursor, finishes on
      Enter/Esc or 3s idle timeout, then hold+fade as usual
    - MCP Info guide updated with tui_show_caption in tools table
    - toolDef helper now supports required fields in JSON Schema
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP navigation tool for tab switching and 
integration selection
    
    Adds tui_navigate MCP tool so AI agents can switch tabs and select
    integrations without key injection. Returns available options on error
    so the agent can self-correct.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP key injection tool for AI-driven demo 
recording
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP footer connection status and daemon threads
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP caption auto-dismiss with non-blocking 
duration
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - caption overlay should not swallow unrelated key 
events
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP log master/detail view with request/response 
capture
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - extract McpLogPopup from ActionsPopup
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - add tui_get_options and tui_wait_for_idle MCP 
tools
    
    tui_get_options returns available tabs and integrations upfront so agents
    can discover what's available without guessing. tui_wait_for_idle waits
    for N new render frames after an action, using a render generation counter
    instead of text comparison to work reliably on live-updating screens.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - faster key injection, wait option, and F2 menu 
discovery
    
    Multi-key-per-tick processing so batched keys execute efficiently. Added
    wait=true option to tui_send_keys that blocks until keys are processed
    and returns the screen, eliminating separate wait_for_idle + get_screen
    round-trips. Extended tui_get_options with F2 actions menu structure so
    agents can plan key sequences upfront. Minimum key delay set to 80ms
    for human-paced interaction.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - MCP selection context metadata for AI navigation
    
    Add structured SelectionContext to MCP tool responses so AI agents
    can reliably navigate tables and popups without parsing screen text.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - auto-select launched integration and clear stale 
selection
    
    Auto-select a newly launched integration in the overview table so the user
    sees it highlighted immediately. Clear stale selectedPid when integrations
    are stopped to avoid showing "selected: ?" in the header.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - fix footer overlap and space keystroke label
    
    Show keystrokes and MCP status together in the footer instead of
    mutually exclusive. Display space key as "Space" in keystroke overlay.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - Ctrl+K toggles keystroke overlay, expose in MCP 
state
    
    Allow toggling keystroke display with Ctrl+K. Expose keystrokesVisible
    in tui_get_state so AI agents can check and toggle the overlay.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - VHS tape recording for humans and AI agents
    
    Extract TapeRecorder to its own class, shared between MCP tools and
    human interaction. Add F2 menu item and Ctrl+R shortcut to toggle tape
    recording. Saves tape to camel-tui-tape-<timestamp>.tape in current
    directory. MCP tools tui_tape_start/tui_tape_stop also supported.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - tape recording guide and MCP save option
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - improve MCP tape recording and caption display
    
    Record tab navigation and captions in tape for replay, suppress spurious
    Sleep entries from AI processing latency, add tui_sleep tool, return
    screen content from tui_navigate, expose captionVisible in tui_get_state,
    and fix caption newline handling to avoid showing backslash characters.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - simplify captions and fix tape recording
    
    - Remove caption dialog mode, keep only inline mode (Ctrl+T)
    - Fix caption input not receiving key events during tape replay
    - Fix Unicode arrow keys recorded as literal characters in tape
    - Use human-friendly timestamp in tape filenames (yyyyMMdd-HHmmss)
    - Add VHS-compatible Set directives as defaults in tape header
    - Update recording guide: replay via camel tui monitor --record
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * CAMEL-23606: camel-tui - workaround macOS JLine close deadlock on exit
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../camel-jbang-plugin-tui/docs/video/readme.md    |  97 ++-
 .../dsl/jbang/core/commands/tui/ActionsPopup.java  | 314 +++++++-
 .../dsl/jbang/core/commands/tui/CamelMonitor.java  | 440 ++++++++++-
 .../jbang/core/commands/tui/CaptionOverlay.java    | 183 +++--
 .../jbang/core/commands/tui/CircuitBreakerTab.java |  13 +
 .../dsl/jbang/core/commands/tui/ConsumersTab.java  |  13 +
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |  18 +
 .../dsl/jbang/core/commands/tui/HealthTab.java     |  16 +
 .../dsl/jbang/core/commands/tui/HistoryTab.java    |  32 +
 .../camel/dsl/jbang/core/commands/tui/HttpTab.java |  14 +
 .../dsl/jbang/core/commands/tui/McpLogPopup.java   | 210 ++++++
 .../dsl/jbang/core/commands/tui/MonitorTab.java    |   4 +
 .../dsl/jbang/core/commands/tui/RoutesTab.java     |  13 +
 .../tui/{MonitorTab.java => SelectionContext.java} |  33 +-
 .../dsl/jbang/core/commands/tui/TapeRecorder.java  | 218 ++++++
 .../dsl/jbang/core/commands/tui/TuiEventLog.java   |  57 ++
 .../dsl/jbang/core/commands/tui/TuiMcpServer.java  | 821 +++++++++++++++++++++
 17 files changed, 2339 insertions(+), 157 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/docs/video/readme.md 
b/dsl/camel-jbang/camel-jbang-plugin-tui/docs/video/readme.md
index e28f111a6871..5300152022be 100644
--- a/dsl/camel-jbang/camel-jbang-plugin-tui/docs/video/readme.md
+++ b/dsl/camel-jbang/camel-jbang-plugin-tui/docs/video/readme.md
@@ -11,17 +11,93 @@ This produces Asciinema `.cast` files with pixel-perfect 
rendering
   (for the circuit-breaker, openapi, and routes demos)
 - Optional: [agg](https://github.com/asciinema/agg) to convert `.cast` to 
`.gif`
   (`brew install agg`)
+- Optional: [vhs](https://github.com/charmbracelet/vhs) to convert `.tape` to 
`.gif`
+  (`brew install vhs`)
 - Optional: [asciinema](https://asciinema.org/) to play `.cast` files in the 
terminal
   (`brew install asciinema`)
 
-## Recording
+## Live Tape Recording (Interactive)
 
-Use the `record.sh` wrapper script:
+You can record your live TUI session as a `.tape` file while using the TUI 
interactively.
+The tape captures your keystrokes with timing, producing a script that can be 
replayed
+or converted to an animated GIF.
+
+### Starting and Stopping
+
+There are two ways to toggle tape recording:
+
+1. **Keyboard shortcut**: Press `Ctrl+R` to start/stop recording at any time
+2. **Actions menu**: Press `F2` to open the actions menu, then select
+   "Start Tape Recording" (or "Stop Tape Recording" if already recording)
+
+When recording starts, a notification confirms "Tape recording started".
+When recording stops, the tape is saved to the current directory as
+`camel-tui-tape-<timestamp>.tape` and a notification shows the filename.
+
+### Tips
+
+- `Ctrl+R` is the quickest way to toggle recording. It is **not** captured
+  in the tape itself, so the resulting script stays clean.
+- The tape records the natural pauses between your keystrokes as `Sleep` 
commands,
+  preserving the real-time feel.
+- Keep recordings focused — record one specific workflow or feature at a time.
+
+### Converting Tape to Animated GIF
+
+The `.tape` file uses the [VHS](https://github.com/charmbracelet/vhs) format.
+Install VHS and run:
+
+```bash
+vhs camel-tui-tape-20260525-143022.tape
+```
+
+VHS will replay the keystrokes in a virtual terminal and produce an animated 
GIF.
+You can customize the output by editing the `.tape` file before converting — 
for
+example, adding a title or adjusting sleep durations.
+
+### Example Tape File
+
+A recorded tape looks like this:
+
+```
+# My TUI Demo
+
+Sleep 2s
+Type "3"
+Sleep 1.5s
+Type "D"
+Sleep 3s
+Type "D"
+Sleep 500ms
+Type "1"
+Sleep 1s
+Type "q"
+```
+
+You can edit the file to add VHS directives at the top:
+
+```
+Output demo.gif
+Set FontSize 14
+Set Width 1200
+Set Height 600
+
+# My TUI Demo
+Sleep 2s
+Type "3"
+...
+```
+
+See the [VHS documentation](https://github.com/charmbracelet/vhs) for all
+available settings (font, dimensions, padding, themes, etc.).
+
+## Scripted Recording (Headless)
+
+Use the `record.sh` wrapper script for automated, repeatable recordings:
 
 ```bash
 # Basic hello world demo (uses built-in example)
 ./record.sh camel-tui-hello --example=timer-log
-
 ```
 
 The script starts a Camel integration in the background (`camel run 
--background`),
@@ -31,16 +107,31 @@ After recording, the background integration is stopped 
with `camel stop`.
 
 ## Playback
 
+Play `.cast` files in the terminal:
+
 ```bash
 asciinema play camel-tui-hello.cast
 ```
 
 ## Convert to GIF
 
+There are two conversion paths depending on your source format:
+
+### From .cast (Asciinema recording)
+
 ```bash
 agg camel-tui-hello.cast camel-tui-hello.gif
 ```
 
+### From .tape (VHS tape file)
+
+```bash
+vhs camel-tui-hello.tape
+```
+
+VHS produces a `.gif` by default. You can also produce `.mp4`, `.webm`,
+or `.png` screenshots by adding `Output` directives to the tape file.
+
 ## Tape files
 
 Tape files use the [VHS](https://github.com/charmbracelet/vhs) format to script
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 2db68d8234be..a5050d6cc395 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
@@ -44,6 +44,7 @@ import dev.tamboui.widgets.list.ListItem;
 import dev.tamboui.widgets.list.ListState;
 import dev.tamboui.widgets.list.ListWidget;
 import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
 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;
@@ -59,17 +60,26 @@ class ActionsPopup {
     private static final int ACTION_CAPTION = 2;
     private static final int ACTION_SCREENSHOT = 3;
     private static final int ACTION_SHOW_KEYSTROKES = 4;
-    private static final int ACTION_DOCTOR = 5;
-    private static final int ACTION_CLASSPATH = 6;
-    private static final int ACTION_STOP_ALL = 7;
-    private static final int ACTION_COUNT = 8;
+    private static final int ACTION_TAPE_RECORDING = 5;
+    private static final int ACTION_TAPE_INSTRUCTIONS = 6;
+    private static final int ACTION_DOCTOR = 7;
+    private static final int ACTION_CLASSPATH = 8;
+    private static final int ACTION_MCP_INFO = 9;
+    private static final int ACTION_MCP_LOG = 10;
+    private static final int ACTION_STOP_ALL = 11;
 
     private final Supplier<Set<String>> runningNames;
     private final Supplier<List<IntegrationInfo>> integrations;
     private final Runnable screenshotAction;
     private final Runnable toggleKeystrokes;
     private final Supplier<Boolean> keystrokesEnabled;
+    private final Runnable toggleTapeRecording;
+    private final Supplier<Boolean> tapeRecordingActive;
     private MonitorContext ctx;
+    private boolean mcpEnabled;
+    private int mcpPort;
+    private Supplier<String> mcpConnectedClient;
+    private Supplier<List<TuiMcpServer.LogEntry>> mcpActivityLog;
 
     private boolean showActionsMenu;
     private final ListState actionsMenuState = new ListState();
@@ -87,9 +97,12 @@ class ActionsPopup {
     private boolean showDocViewer;
     private boolean docViewerFromExampleBrowser;
     private String docContent;
+    private List<Line> docLines;
     private String docTitle;
     private int docScroll;
 
+    private final McpLogPopup mcpLogPopup = new McpLogPopup();
+
     private final DoctorPopup doctorPopup = new DoctorPopup();
     private final ClasspathPopup classpathPopup = new ClasspathPopup();
     private final StopAllPopup stopAllPopup;
@@ -99,16 +112,20 @@ class ActionsPopup {
     private String launchNotification;
     private boolean launchNotificationError;
     private long launchNotificationExpiry;
+    private volatile String pendingAutoSelect;
 
     ActionsPopup(Supplier<Set<String>> runningNames, 
Supplier<List<IntegrationInfo>> integrations,
                  Supplier<List<InfraInfo>> infraServices, CaptionOverlay 
captionOverlay,
-                 Runnable screenshotAction, Runnable toggleKeystrokes, 
Supplier<Boolean> keystrokesEnabled) {
+                 Runnable screenshotAction, Runnable toggleKeystrokes, 
Supplier<Boolean> keystrokesEnabled,
+                 Runnable toggleTapeRecording, Supplier<Boolean> 
tapeRecordingActive) {
         this.runningNames = runningNames;
         this.integrations = integrations;
         this.captionOverlay = captionOverlay;
         this.screenshotAction = screenshotAction;
         this.toggleKeystrokes = toggleKeystrokes;
         this.keystrokesEnabled = keystrokesEnabled;
+        this.toggleTapeRecording = toggleTapeRecording;
+        this.tapeRecordingActive = tapeRecordingActive;
         this.stopAllPopup = new StopAllPopup(integrations, infraServices);
     }
 
@@ -116,10 +133,74 @@ class ActionsPopup {
         this.ctx = ctx;
     }
 
+    void setMcpEnabled(
+            boolean enabled, int port, Supplier<String> connectedClient, 
Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
+        this.mcpEnabled = enabled;
+        this.mcpPort = port;
+        this.mcpConnectedClient = connectedClient;
+        this.mcpActivityLog = activityLog;
+        mcpLogPopup.setActivityLog(activityLog);
+    }
+
+    private int actionCount() {
+        return mcpEnabled ? 12 : 10;
+    }
+
     boolean isVisible() {
         return showActionsMenu || showExampleBrowser || 
runOptionsForm.isVisible() || showDocPicker || showDocViewer
-                || doctorPopup.isVisible() || classpathPopup.isVisible()
-                || stopAllPopup.isVisible() || captionOverlay.isInputVisible();
+                || mcpLogPopup.isVisible() || doctorPopup.isVisible() || 
classpathPopup.isVisible()
+                || stopAllPopup.isVisible() || captionOverlay.isInlineMode();
+    }
+
+    SelectionContext getSelectionContext() {
+        if (showExampleBrowser && exampleCatalog != null) {
+            List<String> items = new ArrayList<>();
+            String currentLevel = null;
+            for (JsonObject ex : exampleCatalog) {
+                String level = ex.getStringOrDefault("level", "beginner");
+                if (!level.equals(currentLevel)) {
+                    currentLevel = level;
+                    items.add("── " + capitalize(level) + " ──");
+                }
+                items.add(ex.getStringOrDefault("name", ""));
+            }
+            int total = countExampleListItems();
+            Integer sel = exampleBrowserState.selected();
+            return new SelectionContext("list", items, sel != null ? sel : -1, 
total, "Examples");
+        }
+        if (showActionsMenu) {
+            List<String> items = getActionLabels();
+            Integer sel = actionsMenuState.selected();
+            return new SelectionContext("popup", items, sel != null ? sel : 
-1, items.size(), "Actions");
+        }
+        return null;
+    }
+
+    String getPendingAutoSelect() {
+        return pendingAutoSelect;
+    }
+
+    void clearPendingAutoSelect() {
+        pendingAutoSelect = null;
+    }
+
+    List<String> getActionLabels() {
+        List<String> labels = new ArrayList<>();
+        labels.add("Run an example...");
+        labels.add("Show Documentation");
+        labels.add("Caption...");
+        labels.add("Take Screenshot");
+        labels.add(keystrokesEnabled.get() ? "Hide Keystrokes" : "Show 
Keystrokes");
+        labels.add(tapeRecordingActive.get() ? "Stop Tape Recording" : "Start 
Tape Recording");
+        labels.add("Tape Recording Guide");
+        labels.add("Run Doctor");
+        labels.add("Show Classpath");
+        if (mcpEnabled) {
+            labels.add("MCP Info");
+            labels.add("MCP Log");
+        }
+        labels.add("Stop All");
+        return labels;
     }
 
     void open() {
@@ -133,6 +214,7 @@ class ActionsPopup {
         runOptionsForm.close();
         showDocPicker = false;
         showDocViewer = false;
+        mcpLogPopup.close();
         doctorPopup.close();
         classpathPopup.close();
         stopAllPopup.close();
@@ -148,6 +230,9 @@ class ActionsPopup {
     }
 
     boolean handleKeyEvent(KeyEvent ke) {
+        if (mcpLogPopup.handleKeyEvent(ke)) {
+            return true;
+        }
         if (showDocViewer) {
             if (ke.isCancel()) {
                 showDocViewer = false;
@@ -211,7 +296,7 @@ class ActionsPopup {
             }
             return true;
         }
-        if (captionOverlay.isInputVisible()) {
+        if (captionOverlay.isInlineMode()) {
             return captionOverlay.handleKeyEvent(ke);
         }
         if (classpathPopup.handleKeyEvent(ke)) {
@@ -230,33 +315,46 @@ class ActionsPopup {
             } else if (ke.isUp()) {
                 actionsMenuState.selectPrevious();
             } else if (ke.isDown()) {
-                actionsMenuState.selectNext(ACTION_COUNT);
+                actionsMenuState.selectNext(actionCount());
             } else if (ke.isConfirm()) {
                 Integer sel = actionsMenuState.selected();
                 if (sel != null) {
-                    if (sel == ACTION_RUN_EXAMPLE) {
+                    int action = resolveAction(sel);
+                    if (action == ACTION_RUN_EXAMPLE) {
                         openExampleBrowser();
-                    } else if (sel == ACTION_SHOW_DOCS) {
+                    } else if (action == ACTION_SHOW_DOCS) {
                         openDocPicker();
-                    } else if (sel == ACTION_SCREENSHOT) {
+                    } else if (action == ACTION_SCREENSHOT) {
                         showActionsMenu = false;
                         screenshotAction.run();
-                    } else if (sel == ACTION_SHOW_KEYSTROKES) {
+                    } else if (action == ACTION_SHOW_KEYSTROKES) {
                         showActionsMenu = false;
                         toggleKeystrokes.run();
-                    } else if (sel == ACTION_DOCTOR) {
+                    } else if (action == ACTION_TAPE_RECORDING) {
+                        showActionsMenu = false;
+                        toggleTapeRecording.run();
+                    } else if (action == ACTION_TAPE_INSTRUCTIONS) {
+                        showActionsMenu = false;
+                        openTapeInstructions();
+                    } else if (action == ACTION_DOCTOR) {
                         showActionsMenu = false;
                         doctorPopup.open();
-                    } else if (sel == ACTION_CLASSPATH) {
+                    } else if (action == ACTION_CLASSPATH) {
                         showActionsMenu = false;
                         openClasspath();
-                    } else if (sel == ACTION_STOP_ALL) {
+                    } else if (action == ACTION_MCP_INFO) {
+                        showActionsMenu = false;
+                        openMcpInfo();
+                    } else if (action == ACTION_MCP_LOG) {
+                        showActionsMenu = false;
+                        openMcpLog();
+                    } else if (action == ACTION_STOP_ALL) {
                         showActionsMenu = false;
                         stopAllPopup.open();
                         checkStopAllNotification();
-                    } else if (sel == ACTION_CAPTION) {
+                    } else if (action == ACTION_CAPTION) {
                         showActionsMenu = false;
-                        captionOverlay.openInput();
+                        captionOverlay.openInline();
                     }
                 }
             }
@@ -281,6 +379,9 @@ class ActionsPopup {
         if (showDocViewer) {
             renderDocViewer(frame, area);
         }
+        if (mcpLogPopup.isVisible()) {
+            mcpLogPopup.render(frame, area);
+        }
         if (doctorPopup.isVisible()) {
             doctorPopup.render(frame, area);
         }
@@ -290,13 +391,13 @@ class ActionsPopup {
         if (classpathPopup.isVisible()) {
             classpathPopup.render(frame, area);
         }
-        if (captionOverlay.isInputVisible()) {
+        if (captionOverlay.isInlineMode()) {
             captionOverlay.render(frame, area);
         }
     }
 
     void renderFooter(List<Span> spans) {
-        if (captionOverlay.isInputVisible()) {
+        if (captionOverlay.isInlineMode()) {
             captionOverlay.renderFooter(spans);
             return;
         }
@@ -312,6 +413,10 @@ class ActionsPopup {
             doctorPopup.renderFooter(spans);
             return;
         }
+        if (mcpLogPopup.isVisible()) {
+            mcpLogPopup.renderFooter(spans);
+            return;
+        }
         if (showDocViewer) {
             hint(spans, "↑↓", "scroll");
             hintLast(spans, "Esc", "back");
@@ -352,8 +457,9 @@ class ActionsPopup {
     // ---- Rendering ----
 
     private void renderActionsMenu(Frame frame, Rect area) {
-        int popupW = 34;
-        int popupH = 2 + ACTION_COUNT;
+        int count = actionCount();
+        int popupW = 40;
+        int popupH = 2 + count;
         int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
         int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
         Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
@@ -366,15 +472,26 @@ class ActionsPopup {
         String stopLabel = stopAllPopup.hasBothGroups()
                 ? "  🛑 Stop All..."
                 : "  🛑 Stop All";
+        List<ListItem> items = new ArrayList<>();
+        items.add(ListItem.from("  🐪 Run an example..."));
+        items.add(ListItem.from("  📖 Show Documentation"));
+        items.add(ListItem.from("  💬 Caption..."));
+        items.add(ListItem.from("  📸 Take Screenshot"));
+        items.add(ListItem.from(keystrokeLabel));
+        String tapeLabel = tapeRecordingActive.get()
+                ? "  ⏹️  Stop Tape Recording (Ctrl+R)"
+                : "  ⏺️  Start Tape Recording (Ctrl+R)";
+        items.add(ListItem.from(tapeLabel));
+        items.add(ListItem.from("  📄 Tape Recording Guide"));
+        items.add(ListItem.from("  🩺 Run Doctor"));
+        items.add(ListItem.from("  📦 Show Classpath"));
+        if (mcpEnabled) {
+            items.add(ListItem.from("  🤖 MCP Info"));
+            items.add(ListItem.from("  📋 MCP Log"));
+        }
+        items.add(ListItem.from(stopLabel));
         ListWidget list = ListWidget.builder()
-                .items(ListItem.from("  🐪 Run an example..."),
-                        ListItem.from("  📖 Show Documentation"),
-                        ListItem.from("  💬 Caption... (Ctrl+T)"),
-                        ListItem.from("  📸 Take Screenshot"),
-                        ListItem.from(keystrokeLabel),
-                        ListItem.from("  🩺 Run Doctor"),
-                        ListItem.from("  📦 Show Classpath"),
-                        ListItem.from(stopLabel))
+                .items(items.toArray(ListItem[]::new))
                 .highlightStyle(Style.EMPTY.fg(Color.WHITE).bold().onBlue())
                 .highlightSymbol("")
                 .scrollMode(ScrollMode.NONE)
@@ -465,18 +582,32 @@ class ActionsPopup {
     private void renderDocViewer(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);
-        MarkdownView view = MarkdownView.builder()
-                .source(docContent)
-                .scroll(docScroll)
-                .block(Block.builder()
-                        .borderType(BorderType.ROUNDED)
-                        .title(" " + docTitle + " ")
-                        .titleBottom(Title.from(Line.from(
-                                Span.styled(" ↑↓", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" scroll │"),
-                                Span.styled(" Esc", 
MonitorContext.HINT_KEY_STYLE), Span.raw(" back "))))
-                        .build())
+        Block block = Block.builder()
+                .borderType(BorderType.ROUNDED)
+                .title(" " + docTitle + " ")
+                .titleBottom(Title.from(Line.from(
+                        Span.styled(" ↑↓", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" scroll │"),
+                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" back "))))
                 .build();
-        frame.renderWidget(view, popup);
+        if (docLines != null) {
+            frame.renderWidget(block, popup);
+            Rect inner = block.inner(popup);
+            int visibleLines = inner.height();
+            int totalLines = docLines.size();
+            int clampedScroll = Math.min(docScroll, Math.max(0, totalLines - 
visibleLines));
+            int end = Math.min(clampedScroll + visibleLines, totalLines);
+            List<Line> visible = docLines.subList(clampedScroll, end);
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+                    inner);
+        } else {
+            MarkdownView view = MarkdownView.builder()
+                    .source(docContent)
+                    .scroll(docScroll)
+                    .block(block)
+                    .build();
+            frame.renderWidget(view, popup);
+        }
     }
 
     private void renderDocPicker(Frame frame, Rect area) {
@@ -544,6 +675,7 @@ class ActionsPopup {
         if (ctx == null) {
             return;
         }
+        docLines = null;
         showDocPicker = false;
         try {
             Path outputFile = ctx.getOutputFile(info.pid);
@@ -601,6 +733,7 @@ class ActionsPopup {
         }
         if (content != null && !content.isEmpty()) {
             docContent = isAdoc ? DocHelper.asciidocToMarkdown(content) : 
content;
+            docLines = null;
             docTitle = name;
             docScroll = 0;
             showExampleBrowser = false;
@@ -624,6 +757,103 @@ class ActionsPopup {
         }
     }
 
+    private int resolveAction(int index) {
+        if (!mcpEnabled && index >= ACTION_MCP_INFO) {
+            return index + 2;
+        }
+        return index;
+    }
+
+    private void openTapeInstructions() {
+        docLines = null;
+        docContent = "# Tape Recording Guide\n\n"
+                     + "Record your live TUI session as a `.tape` file that 
captures keystrokes\n"
+                     + "with timing. The tape can be replayed inside the TUI 
to produce\n"
+                     + "an Asciinema `.cast` recording.\n\n"
+                     + "## Starting and Stopping\n\n"
+                     + "- Press **Ctrl+R** to start/stop recording at any 
time\n"
+                     + "- Or use the **F2** actions menu → Start/Stop Tape 
Recording\n\n"
+                     + "When recording stops, the tape is saved to the current 
directory as\n"
+                     + "`camel-tui-tape-<timestamp>.tape`.\n\n"
+                     + "## Replaying a Tape\n\n"
+                     + "Replay the tape inside the TUI with the `--record` 
option:\n\n"
+                     + "    camel tui monitor 
--record=camel-tui-tape-20260525-153000.tape\n\n"
+                     + "This replays the keystrokes inside the live TUI and 
produces\n"
+                     + "an Asciinema `.cast` file.\n\n"
+                     + "## Converting to Animated GIF\n\n"
+                     + "Use `agg` to convert the `.cast` file to an animated 
GIF:\n\n"
+                     + "    brew install asciinema/tap/agg\n"
+                     + "    agg recording.cast demo.gif\n\n"
+                     + "Or upload to [asciinema.org](https://asciinema.org) 
for a shareable link.\n\n"
+                     + "## Tips\n\n"
+                     + "- **Ctrl+R** is not captured in the tape, keeping the 
script clean\n"
+                     + "- Natural pauses between keystrokes are preserved as 
`Sleep` commands\n"
+                     + "- Keep recordings focused — one workflow at a time 
works best\n";
+        docTitle = "Tape Recording Guide";
+        docScroll = 0;
+        showDocViewer = true;
+        docViewerFromExampleBrowser = false;
+    }
+
+    private void openMcpInfo() {
+        docLines = null;
+        String url = "http://localhost:"; + mcpPort + "/mcp";
+        String client = mcpConnectedClient != null ? mcpConnectedClient.get() 
: null;
+        String status = client != null
+                ? "**Connected:** " + client
+                : "**Status:** Waiting for connection";
+        docContent = "# MCP Server\n\n"
+                     + status + "\n\n"
+                     + "The TUI has an embedded MCP (Model Context Protocol) 
server running at:\n\n"
+                     + "    " + url + "\n\n"
+                     + "This allows AI coding agents to observe your TUI 
session — see the screen,\n"
+                     + "follow your key presses, and understand what you're 
doing.\n\n"
+                     + "## Available Tools\n\n"
+                     + "| Tool | Description |\n"
+                     + "|------|-------------|\n"
+                     + "| `tui_get_screen` | Returns the current screen 
content as text |\n"
+                     + "| `tui_get_events` | Returns recent key presses and 
navigation events |\n"
+                     + "| `tui_get_state` | Returns active tab, selected 
integration, etc. |\n"
+                     + "| `tui_get_options` | Returns available tabs and 
integrations |\n"
+                     + "| `tui_show_caption` | Shows a message on the TUI 
screen |\n"
+                     + "| `tui_navigate` | Switch tabs and select integrations 
|\n"
+                     + "| `tui_send_keys` | Send key presses to control the 
TUI |\n"
+                     + "| `tui_wait_for_idle` | Waits for the screen to settle 
after an action |\n"
+                     + "| `tui_tape_start` | Start recording interactions as a 
VHS .tape file |\n"
+                     + "| `tui_tape_stop` | Stop recording and return the tape 
content |\n\n"
+                     + "## Setup for Claude Code\n\n"
+                     + "Run this command to connect Claude Code to the 
TUI:\n\n"
+                     + "    claude mcp add --transport http camel-tui " + url 
+ "\n\n"
+                     + "Or add to your project's `.mcp.json`:\n\n"
+                     + "    {\n"
+                     + "      \"mcpServers\": {\n"
+                     + "        \"camel-tui\": {\n"
+                     + "          \"type\": \"http\",\n"
+                     + "          \"url\": \"" + url + "\"\n"
+                     + "        }\n"
+                     + "      }\n"
+                     + "    }\n\n"
+                     + "A `.mcp.json` file is auto-generated in the current 
directory while the TUI\n"
+                     + "is running with `--mcp` enabled. It is removed when 
the TUI exits.\n\n"
+                     + "## Usage Examples\n\n"
+                     + "Once connected, ask your AI agent:\n\n"
+                     + "- \"What's on my Camel TUI screen right now?\"\n"
+                     + "- \"What tab am I on and what integration is 
selected?\"\n"
+                     + "- \"What keys did I press in the last minute?\"\n"
+                     + "- \"What color is the throughput chart?\"\n"
+                     + "- \"Show me a message on the TUI screen\"\n"
+                     + "- \"Switch to the Health tab\"\n"
+                     + "- \"Select the myApp integration\"\n";
+        docTitle = "MCP Info";
+        docScroll = 0;
+        showDocViewer = true;
+        docViewerFromExampleBrowser = false;
+    }
+
+    private void openMcpLog() {
+        mcpLogPopup.open();
+    }
+
     private void openClasspath() {
         if (ctx == null) {
             return;
@@ -701,6 +931,7 @@ class ActionsPopup {
             pb.redirectOutput(outputFile.toFile());
             Process process = pb.start();
             pendingLaunches.add(new PendingLaunch(displayName, process, 
outputFile, System.currentTimeMillis()));
+            pendingAutoSelect = displayName;
             launchNotification = "Starting: " + displayName;
             launchNotificationError = false;
             launchNotificationExpiry = System.currentTimeMillis() + 5000;
@@ -865,6 +1096,7 @@ class ActionsPopup {
             pb.redirectOutput(outputFile.toFile());
             Process process = pb.start();
             pendingLaunches.add(new PendingLaunch(exampleName, process, 
outputFile, System.currentTimeMillis()));
+            pendingAutoSelect = exampleName;
             launchNotification = "Starting: " + exampleName;
             launchNotificationError = false;
             launchNotificationExpiry = System.currentTimeMillis() + 5000;
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 72a1265c6d39..91d02161000b 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -35,8 +35,10 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
@@ -58,6 +60,7 @@ import dev.tamboui.tui.TuiRunner;
 import dev.tamboui.tui.event.Event;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.tui.event.KeyModifiers;
 import dev.tamboui.tui.event.TickEvent;
 import dev.tamboui.widgets.Clear;
 import dev.tamboui.widgets.barchart.Bar;
@@ -126,10 +129,19 @@ public class CamelMonitor extends CamelCommand {
     long refreshInterval = DEFAULT_REFRESH_MS;
 
     @CommandLine.Option(names = { "--record" },
-                        description = "Record a demo to an Asciinema .cast 
file using a TamboUI tape script",
+                        description = "Replay a .tape file inside the TUI and 
record to an Asciinema .cast file",
                         arity = "0..1")
     String record;
 
+    @CommandLine.Option(names = { "--mcp" },
+                        description = "Enable embedded MCP server for AI agent 
access to the TUI")
+    boolean mcp;
+
+    @CommandLine.Option(names = { "--mcp-port" },
+                        description = "MCP server port (default: 
${DEFAULT-VALUE})",
+                        defaultValue = "8123")
+    int mcpPort = 8123;
+
     // State
     private final AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(Collections.emptyList());
     private final AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(Collections.emptyList());
@@ -197,10 +209,16 @@ public class CamelMonitor extends CamelCommand {
     private volatile long lastRefresh;
     private boolean showKillConfirm;
     private volatile Buffer lastBuffer;
+    private volatile long renderGeneration;
     private volatile String screenshotMessage;
     private volatile long screenshotMessageTime;
     private volatile boolean pendingScreenshot;
     private boolean recording;
+    private TapeRecorder tapeRecorder;
+    private boolean mcpInjectedKey;
+    private TuiEventLog eventLog;
+    private TuiMcpServer mcpServer;
+    private final Queue<PendingKey> pendingKeys = new 
ConcurrentLinkedQueue<>();
     private final List<KeyRecord> recentKeys = new ArrayList<>();
     private final CaptionOverlay captionOverlay = new CaptionOverlay();
 
@@ -218,7 +236,9 @@ public class CamelMonitor extends CamelCommand {
             captionOverlay,
             () -> pendingScreenshot = true,
             () -> recording = !recording,
-            () -> recording);
+            () -> recording,
+            this::toggleTapeRecording,
+            () -> tapeRecorder != null && tapeRecorder.isActive());
 
     private final AtomicBoolean refreshInProgress = new AtomicBoolean(false);
     private TuiRunner runner;
@@ -281,7 +301,24 @@ public class CamelMonitor extends CamelCommand {
         // Initial data load (synchronous before TUI starts)
         refreshDataSync();
 
-        try (var tui = TuiRunner.create()) {
+        eventLog = new TuiEventLog(500);
+        Path mcpJsonFile = null;
+        if (mcp) {
+            mcpServer = new TuiMcpServer(mcpPort, this);
+            try {
+                mcpServer.start();
+                actionsPopup.setMcpEnabled(true, mcpPort, 
mcpServer::getConnectedClient, mcpServer::getActivityLog);
+                mcpJsonFile = writeMcpJson(mcpPort);
+            } catch (java.net.BindException e) {
+                System.err.println("MCP server failed to start: port " + 
mcpPort + " is already in use.");
+                System.err.println("Use --mcp-port to specify a different 
port, e.g.: camel tui --mcp --mcp-port 8124");
+                mcpServer = null;
+                mcp = false;
+            }
+        }
+
+        var tui = TuiRunner.create();
+        try {
             this.runner = tui;
             ctx.runner = tui;
             // Intercept Ctrl+C: quit the TUI cleanly instead of letting
@@ -291,7 +328,29 @@ public class CamelMonitor extends CamelCommand {
                     this::handleEvent,
                     this::render);
         } finally {
+            if (mcpServer != null) {
+                mcpServer.stop();
+            }
+            deleteMcpJson(mcpJsonFile);
             this.runner = null;
+            // Workaround: on macOS, JLine's terminal close deadlocks in
+            // FileDescriptor.close0() while the reader thread is in native 
read0().
+            // Run close in a daemon thread so the JVM can exit even if it 
hangs.
+            // Remove when fixed upstream: 
https://github.com/tamboui/tamboui/pull/355
+            Thread closeThread = new Thread(() -> {
+                try {
+                    tui.close();
+                } catch (Exception e) {
+                    // best effort
+                }
+            }, "tui-close");
+            closeThread.setDaemon(true);
+            closeThread.start();
+            try {
+                closeThread.join(3000);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
         }
         return 0;
     }
@@ -300,18 +359,39 @@ public class CamelMonitor extends CamelCommand {
 
     private boolean handleEvent(Event event, TuiRunner runner) {
         if (event instanceof KeyEvent ke) {
+            if (eventLog != null) {
+                String elabel = keyLabel(ke);
+                if (elabel != null) {
+                    eventLog.record(elabel, elabel);
+                }
+            }
             if (recording) {
                 String label = keyLabel(ke);
                 if (label != null) {
                     recentKeys.add(new KeyRecord(label, 
System.currentTimeMillis()));
                 }
             }
-            if (captionOverlay.isCaptionVisible()) {
-                captionOverlay.handleKeyEvent(ke);
+            if (ke.hasCtrl() && ke.isChar('r')) {
+                toggleTapeRecording();
+                return true;
+            }
+            if (tapeRecorder != null && tapeRecorder.isActive() && 
!mcpInjectedKey) {
+                String label = keyLabel(ke);
+                if (label != null) {
+                    tapeRecorder.recordKey(label);
+                }
+            }
+            if (captionOverlay.isVisible()) {
+                if (captionOverlay.handleKeyEvent(ke)) {
+                    return true;
+                }
+            }
+            if (ke.hasCtrl() && ke.isChar('k')) {
+                recording = !recording;
                 return true;
             }
             if (ke.hasCtrl() && ke.isChar('t')) {
-                captionOverlay.openInput();
+                captionOverlay.openInline();
                 return true;
             }
             if (actionsPopup.isVisible()) {
@@ -547,6 +627,18 @@ public class CamelMonitor extends CamelCommand {
         }
         if (event instanceof TickEvent) {
             long now = System.currentTimeMillis();
+            boolean keyProcessed = false;
+            PendingKey pk;
+            while ((pk = pendingKeys.peek()) != null && now >= pk.fireAt()) {
+                pendingKeys.poll();
+                mcpInjectedKey = true;
+                handleEvent(pk.event(), runner);
+                mcpInjectedKey = false;
+                keyProcessed = true;
+            }
+            if (keyProcessed) {
+                return true;
+            }
             actionsPopup.tick(now);
             captionOverlay.tick(now);
             if (recording && !recentKeys.isEmpty()) {
@@ -613,6 +705,9 @@ public class CamelMonitor extends CamelCommand {
         }
         if (ke.code() == KeyCode.CHAR) {
             String s = ke.string();
+            if (" ".equals(s)) {
+                return "Space";
+            }
             if (!s.isEmpty()) {
                 return s;
             }
@@ -649,7 +744,10 @@ public class CamelMonitor extends CamelCommand {
 
     private void selectCurrentIntegration() {
         if (ctx.selectedPid != null) {
-            return;
+            if (findSelectedIntegration() != null || findSelectedInfra() != 
null) {
+                return;
+            }
+            ctx.selectedPid = null;
         }
         if (ctx.infraTableFocused) {
             List<InfraInfo> infras = infraData.get();
@@ -764,6 +862,7 @@ public class CamelMonitor extends CamelCommand {
         renderFooter(frame, mainChunks.get(5));
 
         lastBuffer = frame.buffer();
+        renderGeneration++;
 
         if (pendingScreenshot) {
             pendingScreenshot = false;
@@ -1604,9 +1703,10 @@ public class CamelMonitor extends CamelCommand {
             renderOverviewFooter(spans);
         }
 
+        List<Span> rightSpans = new ArrayList<>();
+
         if (recording && !recentKeys.isEmpty()) {
             long now = System.currentTimeMillis();
-            List<Span> keySpans = new ArrayList<>();
             int maxKeys = Math.min(recentKeys.size(), 8);
             List<KeyRecord> visible = recentKeys.subList(recentKeys.size() - 
maxKeys, recentKeys.size());
             for (KeyRecord kr : visible) {
@@ -1614,13 +1714,40 @@ public class CamelMonitor extends CamelCommand {
                 Style style = age < 1000
                         ? Style.EMPTY.fg(Color.WHITE).bold().onBlue()
                         : Style.EMPTY.dim();
-                keySpans.add(Span.styled(" " + kr.label() + " ", style));
+                rightSpans.add(Span.styled(" " + kr.label() + " ", style));
+            }
+        }
+
+        if (mcp) {
+            if (!rightSpans.isEmpty()) {
+                rightSpans.add(Span.raw("  "));
             }
+            String client = mcpServer != null ? mcpServer.getConnectedClient() 
: null;
+            boolean active = mcpServer != null && mcpServer.isRecentActivity();
+            String mcpLabel = "MCP :" + mcpPort;
+            String suffix;
+            Style labelStyle;
+            Style suffixStyle;
+            if (client != null) {
+                suffix = active ? " ●" : " ○";
+                mcpLabel += " (" + client + ")";
+                labelStyle = Style.EMPTY.fg(Color.GREEN);
+                suffixStyle = Style.EMPTY.fg(active ? Color.GREEN : 
Color.DARK_GRAY);
+            } else {
+                suffix = " ✗";
+                labelStyle = Style.EMPTY.dim();
+                suffixStyle = Style.EMPTY.fg(Color.RED);
+            }
+            rightSpans.add(Span.styled(mcpLabel, labelStyle));
+            rightSpans.add(Span.styled(suffix, suffixStyle));
+        }
+
+        if (!rightSpans.isEmpty()) {
             int hintsWidth = spans.stream().mapToInt(s -> s.width()).sum();
-            int keystrokeWidth = keySpans.stream().mapToInt(s -> 
s.width()).sum();
-            int gap = Math.max(1, area.width() - hintsWidth - keystrokeWidth);
+            int rightWidth = rightSpans.stream().mapToInt(s -> 
s.width()).sum();
+            int gap = Math.max(1, area.width() - hintsWidth - rightWidth);
             spans.add(Span.raw(" ".repeat(gap)));
-            spans.addAll(keySpans);
+            spans.addAll(rightSpans);
         }
 
         frame.renderWidget(Paragraph.from(Line.from(spans)), area);
@@ -1752,6 +1879,28 @@ public class CamelMonitor extends CamelCommand {
 
             data.set(infos);
 
+            // Clear stale selection when the selected integration is gone
+            if (ctx.selectedPid != null && !ctx.infraTableFocused) {
+                boolean stillAlive = infos.stream()
+                        .anyMatch(i -> ctx.selectedPid.equals(i.pid) && 
!i.vanishing);
+                if (!stillAlive) {
+                    ctx.selectedPid = null;
+                }
+            }
+
+            // Auto-select a newly launched integration
+            String autoSelect = actionsPopup.getPendingAutoSelect();
+            if (autoSelect != null) {
+                for (IntegrationInfo info : infos) {
+                    if (!info.vanishing && 
autoSelect.equalsIgnoreCase(info.name)) {
+                        ctx.selectedPid = info.pid;
+                        ctx.infraTableFocused = false;
+                        actionsPopup.clearPendingAutoSelect();
+                        break;
+                    }
+                }
+            }
+
             // Discover running infra services
             refreshInfraData();
 
@@ -2929,4 +3078,271 @@ public class CamelMonitor extends CamelCommand {
     record VanishingInfraInfo(InfraInfo info, long startTime) {
     }
 
+    // ---- MCP .mcp.json lifecycle ----
+
+    private static Path writeMcpJson(int port) {
+        Path path = Path.of(".mcp.json");
+        try {
+            String json = "{\n"
+                          + "  \"mcpServers\": {\n"
+                          + "    \"camel-tui\": {\n"
+                          + "      \"type\": \"http\",\n"
+                          + "      \"url\": \"http://localhost:"; + port + 
"/mcp\"\n"
+                          + "    }\n"
+                          + "  }\n"
+                          + "}\n";
+            Files.writeString(path, json);
+            return path;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private static void deleteMcpJson(Path path) {
+        if (path != null) {
+            try {
+                Files.deleteIfExists(path);
+            } catch (IOException e) {
+                // best effort
+            }
+        }
+    }
+
+    // ---- MCP accessor methods ----
+
+    private static final String[] TAB_NAMES = {
+            "Overview", "Log", "Routes", "Consumers", "Endpoints",
+            "HTTP", "Health", "Inspect", "Circuit Breaker"
+    };
+
+    Buffer getLastBuffer() {
+        return lastBuffer;
+    }
+
+    long getRenderGeneration() {
+        return renderGeneration;
+    }
+
+    boolean isKeystrokesVisible() {
+        return recording;
+    }
+
+    TapeRecorder getTapeRecorder() {
+        return tapeRecorder;
+    }
+
+    boolean isTapeRecording() {
+        return tapeRecorder != null && tapeRecorder.isActive();
+    }
+
+    void startTapeRecording(String title) {
+        tapeRecorder = new TapeRecorder();
+        tapeRecorder.start(title);
+    }
+
+    void clearTapeRecorder() {
+        tapeRecorder = null;
+    }
+
+    private void toggleTapeRecording() {
+        if (tapeRecorder != null && tapeRecorder.isActive()) {
+            String tape = tapeRecorder.stop();
+            tapeRecorder = null;
+            String timestamp = java.time.LocalDateTime.now()
+                    
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
+            String filename = "camel-tui-tape-" + timestamp + ".tape";
+            try {
+                
java.nio.file.Files.writeString(java.nio.file.Path.of(filename), tape);
+                captionOverlay.showCaption("Tape saved: " + filename, 5);
+            } catch (java.io.IOException e) {
+                captionOverlay.showCaption("Failed to save tape: " + 
e.getMessage(), 5);
+            }
+        } else {
+            tapeRecorder = new TapeRecorder();
+            tapeRecorder.start(null);
+            captionOverlay.showCaption("Tape recording started", 3);
+        }
+    }
+
+    TuiEventLog getEventLog() {
+        return eventLog;
+    }
+
+    int getActiveTabIndex() {
+        return tabsState.selected();
+    }
+
+    String getActiveTabName() {
+        int idx = tabsState.selected();
+        return idx >= 0 && idx < TAB_NAMES.length ? TAB_NAMES[idx] : "Unknown";
+    }
+
+    String getSelectedPid() {
+        return ctx != null ? ctx.selectedPid : null;
+    }
+
+    String getSelectedIntegrationName() {
+        if (ctx == null) {
+            return null;
+        }
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        return info != null ? info.name : null;
+    }
+
+    int getIntegrationCount() {
+        List<IntegrationInfo> list = data.get();
+        return (int) list.stream().filter(i -> !i.vanishing).count();
+    }
+
+    boolean isCaptionVisible() {
+        return captionOverlay.isCaptionVisible();
+    }
+
+    void showCaption(String text) {
+        captionOverlay.showCaption(text);
+    }
+
+    void showCaption(String text, int durationSeconds) {
+        captionOverlay.showCaption(text, durationSeconds);
+    }
+
+    String navigateToTab(String tabName) {
+        for (int i = 0; i < TAB_NAMES.length; i++) {
+            if (TAB_NAMES[i].equalsIgnoreCase(tabName)) {
+                handleTabKey(i);
+                return TAB_NAMES[i];
+            }
+        }
+        return null;
+    }
+
+    String selectIntegration(String nameOrPid) {
+        List<IntegrationInfo> infos = data.get();
+        for (IntegrationInfo info : infos) {
+            if (info.vanishing) {
+                continue;
+            }
+            if (nameOrPid.equals(info.pid)
+                    || (info.name != null && 
info.name.equalsIgnoreCase(nameOrPid))) {
+                ctx.selectedPid = info.pid;
+                ctx.infraTableFocused = false;
+                return info.name != null ? info.name : info.pid;
+            }
+        }
+        return null;
+    }
+
+    List<String> getTabNames() {
+        return List.of(TAB_NAMES);
+    }
+
+    List<String> getActionLabels() {
+        return actionsPopup.getActionLabels();
+    }
+
+    SelectionContext getSelectionContext() {
+        SelectionContext popup = actionsPopup.getSelectionContext();
+        if (popup != null) {
+            return popup;
+        }
+        if (tabsState.selected() == TAB_OVERVIEW) {
+            List<IntegrationInfo> infos = sortedOverviewInfos();
+            if (infos.isEmpty()) {
+                return null;
+            }
+            List<String> items = infos.stream().map(i -> i.name != null ? 
i.name : i.pid).toList();
+            Integer sel = overviewTableState.selected();
+            return new SelectionContext("table", items, sel != null ? sel : 
-1, items.size(), "Integrations");
+        }
+        MonitorTab tab = activeTab();
+        return tab != null ? tab.getSelectionContext() : null;
+    }
+
+    List<String> getIntegrationNames() {
+        return data.get().stream()
+                .filter(i -> !i.vanishing)
+                .map(i -> i.name != null ? i.name : i.pid)
+                .toList();
+    }
+
+    int injectKeys(List<String> keys, int delayMs) {
+        long fireAt = System.currentTimeMillis();
+        int count = 0;
+        for (String key : keys) {
+            KeyEvent ke = parseKey(key);
+            if (ke != null) {
+                pendingKeys.add(new PendingKey(ke, fireAt));
+                fireAt += delayMs;
+                count++;
+            }
+        }
+        return count;
+    }
+
+    static KeyEvent parseKey(String key) {
+        if (key == null || key.isEmpty()) {
+            return null;
+        }
+
+        boolean ctrl = false;
+        boolean shift = false;
+        String remainder = key;
+        while (remainder.contains("+")) {
+            int plus = remainder.indexOf('+');
+            String mod = remainder.substring(0, plus).trim();
+            remainder = remainder.substring(plus + 1).trim();
+            if (mod.equalsIgnoreCase("Ctrl")) {
+                ctrl = true;
+            } else if (mod.equalsIgnoreCase("Shift")) {
+                shift = true;
+            }
+        }
+
+        KeyModifiers mods = KeyModifiers.of(ctrl, false, shift);
+
+        KeyCode code = switch (remainder.toLowerCase(Locale.ROOT)) {
+            case "enter", "return" -> KeyCode.ENTER;
+            case "esc", "escape" -> KeyCode.ESCAPE;
+            case "tab" -> KeyCode.TAB;
+            case "backspace" -> KeyCode.BACKSPACE;
+            case "delete", "del" -> KeyCode.DELETE;
+            case "up" -> KeyCode.UP;
+            case "down" -> KeyCode.DOWN;
+            case "left" -> KeyCode.LEFT;
+            case "right" -> KeyCode.RIGHT;
+            case "home" -> KeyCode.HOME;
+            case "end" -> KeyCode.END;
+            case "pageup", "pgup" -> KeyCode.PAGE_UP;
+            case "pagedown", "pgdn" -> KeyCode.PAGE_DOWN;
+            case "f1" -> KeyCode.F1;
+            case "f2" -> KeyCode.F2;
+            case "f3" -> KeyCode.F3;
+            case "f4" -> KeyCode.F4;
+            case "f5" -> KeyCode.F5;
+            case "f6" -> KeyCode.F6;
+            case "f7" -> KeyCode.F7;
+            case "f8" -> KeyCode.F8;
+            case "f9" -> KeyCode.F9;
+            case "f10" -> KeyCode.F10;
+            case "f11" -> KeyCode.F11;
+            case "f12" -> KeyCode.F12;
+            case "space" -> null;
+            default -> null;
+        };
+
+        if (code != null) {
+            return KeyEvent.ofKey(code, mods);
+        }
+        if ("space".equalsIgnoreCase(remainder)) {
+            return KeyEvent.ofChar(' ', mods);
+        }
+        if (remainder.length() == 1) {
+            return KeyEvent.ofChar(remainder.charAt(0), mods);
+        }
+        return null;
+    }
+
+    private record PendingKey(KeyEvent event, long fireAt) {
+    }
+
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
index 6574e4eb640d..c728c4b1ecda 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CaptionOverlay.java
@@ -29,11 +29,6 @@ import dev.tamboui.text.Text;
 import dev.tamboui.tui.event.KeyCode;
 import dev.tamboui.tui.event.KeyEvent;
 import dev.tamboui.widgets.Clear;
-import dev.tamboui.widgets.block.Block;
-import dev.tamboui.widgets.block.BorderType;
-import dev.tamboui.widgets.block.Title;
-import dev.tamboui.widgets.input.TextInput;
-import dev.tamboui.widgets.input.TextInputState;
 import dev.tamboui.widgets.paragraph.Paragraph;
 
 import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.hint;
@@ -44,16 +39,19 @@ class CaptionOverlay {
     private static final long CHAR_DELAY_MS = 50;
     private static final long HOLD_DURATION_MS = 3000;
     private static final long FADE_DURATION_MS = 1000;
+    private static final long INLINE_IDLE_TIMEOUT_MS = 3000;
 
-    private boolean showInput;
-    private TextInputState inputState;
+    private boolean inlineMode;
+    private StringBuilder inlineBuffer;
+    private long inlineLastKeystroke;
 
     private String captionText;
     private long captionStartTime;
     private long captionFullyTypedTime;
+    private long captionAutoDismissTime;
 
-    boolean isInputVisible() {
-        return showInput;
+    boolean isInlineMode() {
+        return inlineMode;
     }
 
     boolean isCaptionVisible() {
@@ -61,53 +59,64 @@ class CaptionOverlay {
     }
 
     boolean isVisible() {
-        return showInput || captionText != null;
+        return inlineMode || captionText != null;
     }
 
-    void openInput() {
-        showInput = true;
-        inputState = new TextInputState("");
+    void openInline() {
+        inlineMode = true;
+        inlineBuffer = new StringBuilder();
+        inlineLastKeystroke = System.currentTimeMillis();
+        captionText = "";
+        captionStartTime = System.currentTimeMillis();
+        captionFullyTypedTime = 0;
+    }
+
+    void showCaption(String text) {
+        captionText = text.replace("\\n", "\n");
+        captionStartTime = System.currentTimeMillis();
+        captionFullyTypedTime = 0;
+        captionAutoDismissTime = 0;
+    }
+
+    void showCaption(String text, int durationSeconds) {
+        captionText = text.replace("\\n", "\n");
+        captionStartTime = System.currentTimeMillis();
+        captionFullyTypedTime = 0;
+        if (durationSeconds > 0) {
+            captionAutoDismissTime = System.currentTimeMillis() + 
(durationSeconds * 1000L);
+        } else {
+            captionAutoDismissTime = 0;
+        }
     }
 
     void close() {
-        showInput = false;
-        inputState = null;
+        inlineMode = false;
+        inlineBuffer = null;
         captionText = null;
         captionFullyTypedTime = 0;
     }
 
     boolean handleKeyEvent(KeyEvent ke) {
-        if (showInput) {
-            if (ke.isCancel()) {
-                showInput = false;
-                inputState = null;
-            } else if (ke.isConfirm()) {
-                String text = inputState.text().trim();
-                showInput = false;
-                inputState = null;
-                if (!text.isEmpty()) {
-                    captionText = text;
-                    captionStartTime = System.currentTimeMillis();
-                    captionFullyTypedTime = 0;
-                }
+        if (inlineMode) {
+            if (ke.isCancel() || ke.isConfirm()) {
+                finishInline();
             } else if (ke.isDeleteBackward()) {
-                inputState.deleteBackward();
-            } else if (ke.isDeleteForward()) {
-                inputState.deleteForward();
-            } else if (ke.isLeft()) {
-                inputState.moveCursorLeft();
-            } else if (ke.isRight()) {
-                inputState.moveCursorRight();
-            } else if (ke.isHome()) {
-                inputState.moveCursorToStart();
-            } else if (ke.isEnd()) {
-                inputState.moveCursorToEnd();
+                if (!inlineBuffer.isEmpty()) {
+                    inlineBuffer.deleteCharAt(inlineBuffer.length() - 1);
+                    captionText = inlineBuffer.toString();
+                    inlineLastKeystroke = System.currentTimeMillis();
+                }
             } else if (ke.code() == KeyCode.CHAR) {
-                inputState.insert(ke.character());
+                inlineBuffer.append(ke.character());
+                captionText = inlineBuffer.toString();
+                inlineLastKeystroke = System.currentTimeMillis();
             }
             return true;
         }
         if (captionText != null) {
+            if (captionAutoDismissTime > 0) {
+                return false;
+            }
             captionText = null;
             captionFullyTypedTime = 0;
             return true;
@@ -116,9 +125,22 @@ class CaptionOverlay {
     }
 
     void tick(long now) {
-        if (captionText == null) {
+        if (inlineMode && !inlineBuffer.isEmpty()
+                && now - inlineLastKeystroke > INLINE_IDLE_TIMEOUT_MS) {
+            finishInline();
+            return;
+        }
+        if (captionText == null || inlineMode) {
             return;
         }
+
+        if (captionAutoDismissTime > 0 && now > captionAutoDismissTime) {
+            captionText = null;
+            captionFullyTypedTime = 0;
+            captionAutoDismissTime = 0;
+            return;
+        }
+
         int totalChars = captionText.length();
         long elapsed = now - captionStartTime;
         int charsToShow = (int) (elapsed / CHAR_DELAY_MS);
@@ -126,15 +148,29 @@ class CaptionOverlay {
         if (charsToShow >= totalChars && captionFullyTypedTime == 0) {
             captionFullyTypedTime = now;
         }
-        if (captionFullyTypedTime > 0 && now - captionFullyTypedTime > 
HOLD_DURATION_MS + FADE_DURATION_MS) {
+        if (captionAutoDismissTime == 0
+                && captionFullyTypedTime > 0
+                && now - captionFullyTypedTime > HOLD_DURATION_MS + 
FADE_DURATION_MS) {
             captionText = null;
             captionFullyTypedTime = 0;
         }
     }
 
+    private void finishInline() {
+        inlineMode = false;
+        if (inlineBuffer.isEmpty()) {
+            captionText = null;
+            inlineBuffer = null;
+            return;
+        }
+        captionText = inlineBuffer.toString().replace("\\n", "\n");
+        inlineBuffer = null;
+        captionFullyTypedTime = System.currentTimeMillis();
+    }
+
     void render(Frame frame, Rect area) {
-        if (showInput) {
-            renderInput(frame, area);
+        if (inlineMode) {
+            renderInline(frame, area);
             return;
         }
         if (captionText != null) {
@@ -143,41 +179,40 @@ class CaptionOverlay {
     }
 
     void renderFooter(List<Span> spans) {
-        if (showInput) {
-            hint(spans, "Enter", "show");
+        if (inlineMode) {
+            hint(spans, "Enter", "finish");
             hintLast(spans, "Esc", "cancel");
         } else if (captionText != null) {
             hintLast(spans, "any key", "dismiss");
         }
     }
 
-    private void renderInput(Frame frame, Rect area) {
-        int popupW = Math.min(50, area.width() - 4);
-        int popupH = 4;
-        int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
-        int y = area.top() + Math.max(0, (area.height() - popupH) / 2);
-        Rect popup = new Rect(x, y, Math.min(popupW, area.width()), 
Math.min(popupH, area.height()));
-
-        frame.renderWidget(Clear.INSTANCE, popup);
-
-        Block block = Block.builder()
-                .borderType(BorderType.ROUNDED)
-                .title(" 💬 Caption ")
-                .titleBottom(Title.from(Line.from(
-                        Span.styled(" Enter", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" show │"),
-                        Span.styled(" Esc", MonitorContext.HINT_KEY_STYLE), 
Span.raw(" cancel "))))
-                .build();
-        frame.renderWidget(block, popup);
-
-        Rect inner = new Rect(popup.left() + 2, popup.top() + 1, popup.width() 
- 4, 1);
-        frame.renderWidget(Paragraph.from(Line.from(
-                Span.styled("Caption text (\\n for newline):", 
Style.EMPTY.dim()))), inner);
-
-        Rect inputArea = new Rect(popup.left() + 2, popup.top() + 2, 
popup.width() - 4, 1);
-        TextInput textInput = TextInput.builder()
-                .cursorStyle(Style.EMPTY.reversed())
-                .build();
-        frame.renderStatefulWidget(textInput, inputArea, inputState);
+    private void renderInline(Frame frame, Rect area) {
+        Style style = Style.EMPTY.fg(Color.WHITE).bold();
+        String text = inlineBuffer != null ? inlineBuffer.toString() : "";
+        boolean cursorVisible = (System.currentTimeMillis() / 500) % 2 == 0;
+        String display = text + (cursorVisible ? "▌" : " ");
+
+        String[] parts = display.split("\\\\n", -1);
+        List<Line> lines = new ArrayList<>();
+        int maxWidth = 0;
+        for (String part : parts) {
+            lines.add(Line.from(Span.styled("  " + part + "  ", style)));
+            maxWidth = Math.max(maxWidth, part.length() + 4);
+        }
+
+        int captionW = Math.min(Math.max(maxWidth, 6), area.width() - 2);
+        int captionH = lines.size();
+        int captionX = area.left() + Math.max(0, (area.width() - captionW) / 
2);
+        int captionY = area.top() + Math.max(0, (area.height() - captionH) / 
2);
+        Rect captionArea = new Rect(
+                captionX, captionY, Math.min(captionW, area.width()),
+                Math.min(captionH, area.height()));
+
+        frame.renderWidget(Clear.INSTANCE, captionArea);
+        frame.renderWidget(Paragraph.builder()
+                .text(Text.from(lines.toArray(Line[]::new)))
+                .build(), captionArea);
     }
 
     private void renderCaption(Frame frame, Rect area) {
@@ -193,7 +228,7 @@ class CaptionOverlay {
             style = Style.EMPTY.dim();
         }
 
-        String[] parts = visible.split("\\\\n", -1);
+        String[] parts = visible.split("\n", -1);
         List<Line> lines = new ArrayList<>();
         int maxWidth = 0;
         for (String part : parts) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
index bdbffcf13286..43ef5244838f 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CircuitBreakerTab.java
@@ -410,4 +410,17 @@ class CircuitBreakerTab implements MonitorTab {
                 .block(Block.builder().borderType(BorderType.ROUNDED).build())
                 .build(), vSplit.get(2));
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.circuitBreakers.isEmpty()) {
+            return null;
+        }
+        List<CircuitBreakerInfo> sorted = new 
ArrayList<>(info.circuitBreakers);
+        sorted.sort(this::sortCb);
+        List<String> items = sorted.stream().map(cb -> cb.id != null ? cb.id : 
"").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Circuit Breakers");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
index 05fc3bdbc102..193010821a16 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumersTab.java
@@ -245,4 +245,17 @@ class ConsumersTab implements MonitorTab {
         String s3 = ci.sinceLastFailed != null ? ci.sinceLastFailed : "-";
         return s1 + "/" + s2 + "/" + s3;
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.consumers.isEmpty()) {
+            return null;
+        }
+        List<ConsumerInfo> sorted = new ArrayList<>(info.consumers);
+        sorted.sort(this::sortConsumer);
+        List<String> items = sorted.stream().map(c -> c.id != null ? c.id : 
"").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Consumers");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index e3388cbf4510..acbb2d2ea975 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -372,4 +372,22 @@ class EndpointsTab implements MonitorTab {
                         .title(Title.from(chartTitle)).build())
                 .build(), rightArea);
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.endpoints.isEmpty()) {
+            return null;
+        }
+        List<EndpointInfo> sorted = new ArrayList<>(info.endpoints);
+        if (filter == 1) {
+            sorted.removeIf(ep -> !ep.remote);
+        } else if (filter == 2) {
+            sorted.removeIf(ep -> !ep.remote && !ep.stub);
+        }
+        sorted.sort(this::sortEndpoint);
+        List<String> items = sorted.stream().map(ep -> ep.uri != null ? ep.uri 
: "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Endpoints");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
index 2da1b9fbf79f..f4db0025ab3c 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HealthTab.java
@@ -185,4 +185,20 @@ class HealthTab implements MonitorTab {
         }
         return info.healthChecks;
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null) {
+            return null;
+        }
+        List<HealthCheckInfo> checks = new 
ArrayList<>(getFilteredHealthChecks(info));
+        if (checks.isEmpty()) {
+            return null;
+        }
+        checks.sort(this::sortHealth);
+        List<String> items = checks.stream().map(hc -> hc.name != null ? 
hc.name : "").toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Health");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
index 8fc9b05f6b78..668bfce4bbbf 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HistoryTab.java
@@ -916,4 +916,36 @@ class HistoryTab implements MonitorTab {
         }
         return Line.from(result);
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        boolean tracerActive = !traces.get().isEmpty();
+        if (tracerActive) {
+            if (traceDetailView) {
+                List<TraceEntry> steps = 
getTraceSteps(traceSelectedExchangeId);
+                if (steps.isEmpty()) {
+                    return null;
+                }
+                List<String> items = steps.stream()
+                        .map(s -> s.nodeId != null ? s.nodeId : "")
+                        .toList();
+                Integer sel = traceStepTableState.selected();
+                return new SelectionContext("table", items, sel != null ? sel 
: -1, items.size(), "Trace Steps");
+            }
+            List<String> exchangeIds = getTraceExchangeIds();
+            if (exchangeIds.isEmpty()) {
+                return null;
+            }
+            Integer sel = traceTableState.selected();
+            return new SelectionContext("table", exchangeIds, sel != null ? 
sel : -1, exchangeIds.size(), "Traces");
+        }
+        if (historyEntries.isEmpty()) {
+            return null;
+        }
+        List<String> items = historyEntries.stream()
+                .map(h -> h.exchangeId != null ? h.exchangeId : "")
+                .toList();
+        Integer sel = historyTableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "History");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
index 15c2a0bf74a4..06389239b243 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HttpTab.java
@@ -593,4 +593,18 @@ class HttpTab implements MonitorTab {
     private Style sortStyle(String column) {
         return MonitorContext.sortStyle(column, sort);
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        List<HttpEndpointInfo> visible = sortedVisibleEndpoints(info);
+        if (visible.isEmpty()) {
+            return null;
+        }
+        List<String> items = visible.stream()
+                .map(ep -> (ep.method != null ? ep.method : "") + " " + 
(ep.path != null ? ep.path : ""))
+                .toList();
+        Integer sel = tableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "HTTP");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
new file mode 100644
index 000000000000..def5d1c5f3d4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpLogPopup.java
@@ -0,0 +1,210 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyCode;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.Clear;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.block.Title;
+import dev.tamboui.widgets.list.ListItem;
+import dev.tamboui.widgets.list.ListState;
+import dev.tamboui.widgets.list.ListWidget;
+import dev.tamboui.widgets.list.ScrollMode;
+import dev.tamboui.widgets.paragraph.Paragraph;
+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;
+
+class McpLogPopup {
+
+    private boolean visible;
+    private Supplier<List<TuiMcpServer.LogEntry>> activityLog;
+    private List<TuiMcpServer.LogEntry> entries;
+    private int selected;
+    private int detailScroll;
+
+    void setActivityLog(Supplier<List<TuiMcpServer.LogEntry>> activityLog) {
+        this.activityLog = activityLog;
+    }
+
+    boolean isVisible() {
+        return visible;
+    }
+
+    void open() {
+        entries = activityLog != null ? activityLog.get() : List.of();
+        selected = entries.isEmpty() ? 0 : entries.size() - 1;
+        detailScroll = 0;
+        visible = true;
+    }
+
+    void close() {
+        visible = false;
+    }
+
+    boolean handleKeyEvent(KeyEvent ke) {
+        if (!visible) {
+            return false;
+        }
+        if (ke.isCancel()) {
+            visible = false;
+        } else if (ke.isUp() || ke.isChar('k')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.max(0, selected - 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isDown() || ke.isChar('j')) {
+            if (entries != null && !entries.isEmpty()) {
+                selected = Math.min(entries.size() - 1, selected + 1);
+                detailScroll = 0;
+            }
+        } else if (ke.isPageUp() || ke.isKey(KeyCode.PAGE_UP)) {
+            detailScroll = Math.max(0, detailScroll - 5);
+        } else if (ke.isPageDown() || ke.isKey(KeyCode.PAGE_DOWN)) {
+            detailScroll += 5;
+        }
+        return true;
+    }
+
+    void render(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 (entries == null || entries.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);
+
+        renderMaster(frame, masterArea);
+        renderDetail(frame, detailArea);
+    }
+
+    void renderFooter(List<Span> spans) {
+        hint(spans, "↑↓", "select");
+        hint(spans, "PgUp/Dn", "scroll detail");
+        hintLast(spans, "Esc", "back");
+    }
+
+    private void renderMaster(Frame frame, Rect area) {
+        List<ListItem> items = new ArrayList<>();
+        for (TuiMcpServer.LogEntry entry : entries) {
+            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(selected);
+        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, area, masterState);
+    }
+
+    private void renderDetail(Frame frame, Rect area) {
+        TuiMcpServer.LogEntry entry = entries.get(selected);
+        List<Line> lines = new ArrayList<>();
+        if (entry.requestBody() != null) {
+            lines.add(Line.from(Span.styled("▶ Request", 
Style.EMPTY.fg(Color.YELLOW).bold())));
+            addJsonLines(lines, entry.requestBody());
+            lines.add(Line.from(Span.raw("")));
+        }
+        if (entry.responseBody() != null) {
+            lines.add(Line.from(Span.styled("◀ Response", 
Style.EMPTY.fg(Color.GREEN).bold())));
+            addJsonLines(lines, entry.responseBody());
+        }
+        if (entry.requestBody() == null && entry.responseBody() == null) {
+            lines.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, area);
+        Rect inner = detailBlock.inner(area);
+
+        int visibleLines = inner.height();
+        int totalLines = lines.size();
+        int clampedScroll = Math.min(detailScroll, Math.max(0, totalLines - 
visibleLines));
+        int end = Math.min(clampedScroll + visibleLines, totalLines);
+        if (clampedScroll < end) {
+            List<Line> visible = lines.subList(clampedScroll, end);
+            frame.renderWidget(
+                    
Paragraph.builder().text(Text.from(visible.toArray(Line[]::new))).build(),
+                    inner);
+        }
+    }
+
+    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())));
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
index e3ac417bf492..c9870e245780 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
@@ -45,4 +45,8 @@ interface MonitorTab {
 
     default void onIntegrationChanged() {
     }
+
+    default SelectionContext getSelectionContext() {
+        return null;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
index 727236db862b..cb976ee98ff5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RoutesTab.java
@@ -1606,4 +1606,17 @@ class RoutesTab implements MonitorTab {
     private static String objToString(Object o) {
         return o != null ? o.toString() : "";
     }
+
+    @Override
+    public SelectionContext getSelectionContext() {
+        IntegrationInfo info = ctx.findSelectedIntegration();
+        if (info == null || info.routes.isEmpty()) {
+            return null;
+        }
+        List<RouteInfo> sorted = new ArrayList<>(info.routes);
+        sorted.sort(this::sortRoute);
+        List<String> items = sorted.stream().map(r -> r.routeId != null ? 
r.routeId : "").toList();
+        Integer sel = routeTableState.selected();
+        return new SelectionContext("table", items, sel != null ? sel : -1, 
items.size(), "Routes");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SelectionContext.java
similarity index 61%
copy from 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
copy to 
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SelectionContext.java
index e3ac417bf492..015d08e4ae65 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SelectionContext.java
@@ -18,31 +18,10 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.util.List;
 
-import dev.tamboui.layout.Rect;
-import dev.tamboui.terminal.Frame;
-import dev.tamboui.text.Span;
-import dev.tamboui.tui.event.KeyEvent;
-
-/**
- * Interface for TUI monitor tabs. Each tab handles its own events, rendering, 
and footer hints.
- */
-interface MonitorTab {
-
-    boolean handleKeyEvent(KeyEvent ke);
-
-    boolean handleEscape();
-
-    void navigateUp();
-
-    void navigateDown();
-
-    void render(Frame frame, Rect area);
-
-    void renderFooter(List<Span> spans);
-
-    default void onTabSelected() {
-    }
-
-    default void onIntegrationChanged() {
-    }
+record SelectionContext(
+        String type,
+        List<String> items,
+        int selectedIndex,
+        int totalItems,
+        String label) {
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
new file mode 100644
index 000000000000..9fe3b138075a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TapeRecorder.java
@@ -0,0 +1,218 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+class TapeRecorder {
+
+    private static final Set<String> SPECIAL_KEYS = Set.of(
+            "enter", "tab", "space", "backspace", "delete", "escape", "esc",
+            "up", "down", "left", "right", "home", "end", "pageup", "pagedown",
+            "pgup", "pgdn");
+
+    private final List<String> lines = new ArrayList<>();
+    private long startTime;
+    private long lastEventTime;
+    private int keyCount;
+    private boolean active;
+
+    void start(String title) {
+        lines.clear();
+        if (title != null && !title.isBlank()) {
+            lines.add("# " + title);
+            lines.add("");
+        }
+        lines.add("Set WindowBar Colorful");
+        lines.add("Set Width 1200");
+        lines.add("Set Height 1200");
+        lines.add("Set CursorBlink false");
+        lines.add("Set Theme \"Aardvark Blue\"");
+        lines.add("");
+        startTime = System.currentTimeMillis();
+        lastEventTime = startTime;
+        keyCount = 0;
+        active = true;
+    }
+
+    void recordKey(String key) {
+        if (!active || key == null) {
+            return;
+        }
+        long now = System.currentTimeMillis();
+        long elapsed = now - lastEventTime;
+        if (elapsed > 200) {
+            lines.add("Sleep " + formatSleep(elapsed));
+        }
+        String tapeCmd = toTapeCommand(key);
+        if (tapeCmd != null) {
+            lines.add(tapeCmd);
+        } else if (key.length() == 1) {
+            lines.add("Type \"" + escapeTapeString(key) + "\"");
+        }
+        keyCount++;
+        lastEventTime = now;
+    }
+
+    void recordKeys(List<String> keys, int delay) {
+        if (!active) {
+            return;
+        }
+        long now = System.currentTimeMillis();
+        long elapsed = now - lastEventTime;
+        if (elapsed > 200) {
+            lines.add("Sleep " + formatSleep(elapsed));
+        }
+
+        StringBuilder charBatch = new StringBuilder();
+        for (int i = 0; i < keys.size(); i++) {
+            String key = keys.get(i);
+            if (i > 0 && delay > 0) {
+                flushCharBatch(charBatch);
+                lines.add("Sleep " + delay + "ms");
+            }
+            String tapeCmd = toTapeCommand(key);
+            if (tapeCmd != null) {
+                flushCharBatch(charBatch);
+                lines.add(tapeCmd);
+            } else if (key.length() == 1) {
+                charBatch.append(key);
+            }
+            keyCount++;
+        }
+        flushCharBatch(charBatch);
+
+        lastEventTime = now + (long) (keys.size() - 1) * delay;
+    }
+
+    void recordSleep(long ms) {
+        if (!active) {
+            return;
+        }
+        lines.add("Sleep " + formatSleep(ms));
+        lastEventTime = System.currentTimeMillis();
+    }
+
+    void recordCaption(String text, int durationSeconds) {
+        if (!active) {
+            return;
+        }
+        // Normalize: convert real newlines and \n markers to \\n for tape
+        // (parseQuotedString interprets \\ as literal \, so \\n → typed \n)
+        String normalized = text.replace("\n", "\\n");
+        String escaped = normalized.replace("\\", "\\\\").replace("\"", 
"\\\"");
+        lines.add("Ctrl+t");
+        lines.add("Sleep 500ms");
+        lines.add("Type \"" + escaped + "\"");
+        lines.add("Enter");
+        if (durationSeconds > 0) {
+            lines.add("Sleep " + durationSeconds + "s");
+        }
+        lastEventTime = System.currentTimeMillis();
+    }
+
+    void resetClock() {
+        if (!active) {
+            return;
+        }
+        lastEventTime = System.currentTimeMillis();
+    }
+
+    String stop() {
+        active = false;
+        return String.join("\n", lines) + "\n";
+    }
+
+    boolean isActive() {
+        return active;
+    }
+
+    int getKeyCount() {
+        return keyCount;
+    }
+
+    long getDurationMs() {
+        return lastEventTime - startTime;
+    }
+
+    private void flushCharBatch(StringBuilder batch) {
+        if (!batch.isEmpty()) {
+            lines.add("Type \"" + escapeTapeString(batch.toString()) + "\"");
+            batch.setLength(0);
+        }
+    }
+
+    static String toTapeCommand(String key) {
+        if (key.length() == 1) {
+            return switch (key) {
+                case "↑" -> "Up";
+                case "↓" -> "Down";
+                case "←" -> "Left";
+                case "→" -> "Right";
+                case "⌫" -> "Backspace";
+                default -> null;
+            };
+        }
+        String lower = key.toLowerCase(Locale.ROOT);
+
+        if (lower.startsWith("ctrl+")) {
+            return "Ctrl+" + key.substring(5);
+        }
+        if (lower.startsWith("shift+")) {
+            return "Shift+" + key.substring(6);
+        }
+
+        if (SPECIAL_KEYS.contains(lower)) {
+            return switch (lower) {
+                case "esc" -> "Escape";
+                case "pgup", "pageup" -> "PageUp";
+                case "pgdn", "pagedown" -> "PageDown";
+                default -> capitalize(lower);
+            };
+        }
+
+        if (lower.matches("f\\d{1,2}")) {
+            return key.toUpperCase(Locale.ROOT);
+        }
+
+        return null;
+    }
+
+    private static String capitalize(String s) {
+        return s.substring(0, 1).toUpperCase(Locale.ROOT) + s.substring(1);
+    }
+
+    static String formatSleep(long ms) {
+        if (ms >= 1000 && ms % 1000 == 0) {
+            return (ms / 1000) + "s";
+        }
+        if (ms >= 1000) {
+            double seconds = ms / 1000.0;
+            String formatted = String.format(Locale.ROOT, "%.1f", seconds);
+            return formatted + "s";
+        }
+        long rounded = (ms / 100) * 100;
+        return Math.max(100, rounded) + "ms";
+    }
+
+    private static String escapeTapeString(String s) {
+        return s.replace("\\", "\\\\").replace("\"", "\\\"");
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiEventLog.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiEventLog.java
new file mode 100644
index 000000000000..52df4fe7e0f2
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiEventLog.java
@@ -0,0 +1,57 @@
+/*
+ * 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.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Thread-safe bounded ring buffer for TUI key events. Used by the embedded 
MCP server to expose recent user
+ * interactions to AI agents.
+ */
+class TuiEventLog {
+
+    record Event(String key, String label, Instant timestamp) {
+    }
+
+    private final Event[] buffer;
+    private int head;
+    private int size;
+
+    TuiEventLog(int capacity) {
+        this.buffer = new Event[capacity];
+    }
+
+    synchronized void record(String key, String label) {
+        buffer[head] = new Event(key, label, Instant.now());
+        head = (head + 1) % buffer.length;
+        if (size < buffer.length) {
+            size++;
+        }
+    }
+
+    synchronized List<Event> getRecent(int limit) {
+        int count = Math.min(limit, size);
+        List<Event> result = new ArrayList<>(count);
+        int start = (head - count + buffer.length) % buffer.length;
+        for (int i = 0; i < count; i++) {
+            result.add(buffer[(start + i) % buffer.length]);
+        }
+        return result;
+    }
+}
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
new file mode 100644
index 000000000000..87f38387ccea
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -0,0 +1,821 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.export.ExportRequest;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+
+/**
+ * Embedded MCP (Model Context Protocol) server for the Camel TUI.
+ * <p>
+ * Implements the Streamable HTTP transport (spec 2025-03-26) using JDK's 
built-in HttpServer. Exposes tools that let AI
+ * agents observe the live TUI session: screen content, key events, and 
navigation state.
+ * <p>
+ * Binds to 127.0.0.1 only for security.
+ */
+class TuiMcpServer {
+
+    private static final String SERVER_NAME = "camel-tui-mcp";
+    private static final String SERVER_VERSION = "1.0.0";
+    private static final String PROTOCOL_VERSION = "2025-03-26";
+    private static final long CLIENT_TIMEOUT_MS = 60_000;
+
+    private static final int MAX_LOG_ENTRIES = 200;
+    private static final DateTimeFormatter TIME_FMT = 
DateTimeFormatter.ofPattern("HH:mm:ss")
+            .withZone(ZoneId.systemDefault());
+
+    enum LogLevel {
+        INFO,
+        CONNECT,
+        TOOL,
+        ERROR
+    }
+
+    record LogEntry(String timestamp, LogLevel level, String message, String 
requestBody, String responseBody) {
+    }
+
+    private final int port;
+    private final CamelMonitor monitor;
+    private HttpServer server;
+    private volatile String clientName;
+    private volatile long lastActivity;
+    private volatile long lastToolCallTime;
+    private final List<LogEntry> activityLog = new ArrayList<>();
+
+    TuiMcpServer(int port, CamelMonitor monitor) {
+        this.port = port;
+        this.monitor = monitor;
+    }
+
+    void start() throws IOException {
+        server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 
0);
+        server.createContext("/mcp", this::handleMcp);
+        
server.setExecutor(java.util.concurrent.Executors.newCachedThreadPool(r -> {
+            Thread t = new Thread(r, "mcp-handler");
+            t.setDaemon(true);
+            return t;
+        }));
+        server.start();
+        log(LogLevel.INFO, "Server started on port " + port);
+    }
+
+    void stop() {
+        if (server != null) {
+            server.stop(1);
+            if (server.getExecutor() instanceof 
java.util.concurrent.ExecutorService es) {
+                es.shutdownNow();
+            }
+        }
+    }
+
+    synchronized List<LogEntry> getActivityLog() {
+        return new ArrayList<>(activityLog);
+    }
+
+    private synchronized void log(LogLevel level, String 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);
+        }
+    }
+
+    boolean isRecentActivity() {
+        return System.currentTimeMillis() - lastToolCallTime < 2000;
+    }
+
+    String getConnectedClient() {
+        if (System.currentTimeMillis() - lastActivity < CLIENT_TIMEOUT_MS) {
+            return clientName != null ? clientName : "unknown";
+        }
+        return null;
+    }
+
+    private void handleMcp(HttpExchange exchange) throws IOException {
+        try {
+            lastActivity = System.currentTimeMillis();
+            String method = exchange.getRequestMethod();
+            if (!"POST".equals(method)) {
+                exchange.sendResponseHeaders(405, -1);
+                return;
+            }
+
+            String body;
+            try (InputStream is = exchange.getRequestBody()) {
+                body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+            }
+
+            JsonObject request = Jsoner.deserialize(body, new JsonObject());
+            String jsonrpcMethod = (String) request.get("method");
+
+            if (jsonrpcMethod == null) {
+                sendError(exchange, request, -32600, "Invalid Request");
+                return;
+            }
+
+            // Notifications have no "id" — respond with 202
+            if (!request.containsKey("id")) {
+                exchange.sendResponseHeaders(202, -1);
+                return;
+            }
+
+            JsonObject result = switch (jsonrpcMethod) {
+                case "initialize" -> handleInitialize(request);
+                case "tools/list" -> handleToolsList();
+                case "tools/call" -> handleToolsCall(request);
+                case "ping" -> new JsonObject();
+                default -> null;
+            };
+
+            if (result == null) {
+                sendError(exchange, request, -32601, "Method not found: " + 
jsonrpcMethod);
+            } else {
+                String responseJson = sendResult(exchange, request, result);
+                logMethodCall(jsonrpcMethod, request, body, responseJson);
+            }
+        } catch (Exception e) {
+            exchange.sendResponseHeaders(500, -1);
+        } finally {
+            exchange.close();
+        }
+    }
+
+    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) {
+            JsonObject clientInfo = (JsonObject) params.get("clientInfo");
+            if (clientInfo != null) {
+                clientName = (String) clientInfo.get("name");
+            }
+        }
+
+        JsonObject result = new JsonObject();
+        result.put("protocolVersion", PROTOCOL_VERSION);
+
+        JsonObject serverInfo = new JsonObject();
+        serverInfo.put("name", SERVER_NAME);
+        serverInfo.put("version", SERVER_VERSION);
+        result.put("serverInfo", serverInfo);
+
+        JsonObject capabilities = new JsonObject();
+        JsonObject tools = new JsonObject();
+        tools.put("listChanged", false);
+        capabilities.put("tools", tools);
+        result.put("capabilities", capabilities);
+
+        return result;
+    }
+
+    private JsonObject handleToolsList() {
+        JsonArray toolList = new JsonArray();
+        toolList.add(toolDef(
+                "tui_get_screen",
+                "Returns the current TUI screen content as text. "
+                                  + "Shows exactly what the user sees in their 
terminal. "
+                                  + "Use ansi=true to include ANSI color codes 
for color-related questions. "
+                                  + "Also returns a 'selection' field with 
structured metadata about the active list/table "
+                                  + "(type, items, selectedIndex, totalItems, 
label) when available.",
+                Map.of("ansi", propDef("boolean", "Include ANSI color codes in 
the output (default false)"))));
+        toolList.add(toolDef(
+                "tui_get_events",
+                "Returns recent user input events (key presses, navigation). "
+                                  + "Each event has a key, human-readable 
label, and timestamp.",
+                Map.of("limit", propDef("integer", "Maximum number of events 
to return (default 50)"))));
+        toolList.add(toolDef(
+                "tui_get_state",
+                "Returns the current TUI navigation state: active tab, 
selected integration, "
+                                 + "and integration count. "
+                                 + "Includes a 'selection' field with 
structured metadata about the active list/table. "
+                                 + "captionVisible indicates if a caption 
overlay is on screen. "
+                                 + "keystrokesVisible indicates if the 
keystroke overlay is on; toggle with Ctrl+K.",
+                Map.of()));
+        toolList.add(toolDef(
+                "tui_show_caption",
+                "Shows a caption message on the TUI screen with a typewriter 
animation. "
+                                    + "Use this to display messages to the 
user. "
+                                    + "Supports \\n for newlines.",
+                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.")),
+                List.of("text")));
+        toolList.add(toolDef(
+                "tui_navigate",
+                "Navigates the TUI: switch tabs and/or select an integration. "
+                                + "Both parameters are optional — set 
whichever you want to change. "
+                                + "Tab names: Overview, Log, Routes, 
Consumers, Endpoints, HTTP, Health, Inspect, Circuit Breaker. "
+                                + "Returns screen content and selection 
metadata after navigating.",
+                Map.of("tab", propDef("string", "Tab to switch to (e.g. 
'Routes', 'Health')"),
+                        "integration", propDef("string", "Integration name or 
PID to select"))));
+
+        toolList.add(toolDef(
+                "tui_send_keys",
+                "Sends key presses to the TUI. A human is watching the screen, 
"
+                                 + "so keys should be paced naturally like a 
skilled user would type. "
+                                 + "Key names: Enter, Esc, Tab, Backspace, 
Delete, Up, Down, Left, Right, "
+                                 + "Home, End, PgUp, PgDn, Space, F1-F12, or 
any single character. "
+                                 + "Modifiers: Ctrl+x, Shift+x, Ctrl+Shift+x.",
+                Map.of("keys", propDef("array", "Array of key name strings to 
send"),
+                        "delay", propDef("integer",
+                                "Delay in milliseconds between keys (default 
150, minimum 80)"),
+                        "wait", propDef("boolean",
+                                "Wait for all keys to be processed and return 
the resulting screen "
+                                                   + "with selection metadata 
(default false)")),
+                List.of("keys")));
+        toolList.add(toolDef(
+                "tui_get_options",
+                "Returns all available tabs and integrations. "
+                                   + "Use this to discover what tabs exist and 
which integrations are running "
+                                   + "before navigating. Also returns the 
current selection.",
+                Map.of()));
+        toolList.add(toolDef(
+                "tui_wait_for_idle",
+                "Waits for the TUI to render new frames after an action. "
+                                     + "Blocks until the specified number of 
new frames have been rendered, "
+                                     + "ensuring the action has been 
processed. "
+                                     + "Returns the screen content with 
selection metadata after settling. "
+                                     + "Use after tui_navigate or 
tui_send_keys.",
+                Map.of("timeout", propDef("integer",
+                        "Maximum wait time in milliseconds (default 5000, max 
30000)"),
+                        "frames", propDef("integer",
+                                "Number of new frames to wait for (default 
2)"))));
+        toolList.add(toolDef(
+                "tui_tape_start",
+                "Start recording TUI interactions as a .tape file for demo 
playback. "
+                                  + "All subsequent tui_send_keys calls will 
be captured as tape commands. "
+                                  + "Stop recording with tui_tape_stop to get 
the tape content. "
+                                  + "Replay with: camel tui monitor 
--record=<file>.tape",
+                Map.of("title", propDef("string", "Description comment for the 
tape header"))));
+        toolList.add(toolDef(
+                "tui_tape_stop",
+                "Stop tape recording and return the generated .tape content. "
+                                 + "The tape can be replayed with: camel tui 
monitor --record=<file>.tape",
+                Map.of("save", propDef("boolean",
+                        "If true, also save the tape to a local file 
(camel-tui-tape-<timestamp>.tape). Default false."))));
+        toolList.add(toolDef(
+                "tui_sleep",
+                "Pauses for the specified duration. "
+                             + "When tape recording is active, inserts a Sleep 
command into the tape. "
+                             + "Use this to pace demos and wait for captions 
to dismiss.",
+                Map.of("seconds", propDef("integer",
+                        "Number of seconds to sleep (1-30)")),
+                List.of("seconds")));
+
+        JsonObject result = new JsonObject();
+        result.put("tools", toolList);
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    private JsonObject handleToolsCall(JsonObject request) {
+        JsonObject params = (JsonObject) request.get("params");
+        String toolName = params != null ? (String) params.get("name") : null;
+        Map<String, Object> args = params != null ? (Map<String, Object>) 
params.get("arguments") : Map.of();
+        if (args == null) {
+            args = Map.of();
+        }
+
+        lastToolCallTime = System.currentTimeMillis();
+
+        String text;
+        boolean isError = false;
+        try {
+            text = switch (toolName) {
+                case "tui_get_screen" -> callGetScreen(args);
+                case "tui_get_events" -> callGetEvents(args);
+                case "tui_get_state" -> callGetState();
+                case "tui_show_caption" -> callShowCaption(args);
+                case "tui_navigate" -> callNavigate(args);
+                case "tui_send_keys" -> callSendKeys(args);
+                case "tui_get_options" -> callGetOptions();
+                case "tui_wait_for_idle" -> callWaitForIdle(args);
+                case "tui_tape_start" -> callTapeStart(args);
+                case "tui_tape_stop" -> callTapeStop(args);
+                case "tui_sleep" -> callSleep(args);
+                default -> {
+                    isError = true;
+                    yield "Unknown tool: " + toolName;
+                }
+            };
+        } catch (Exception e) {
+            text = "Error: " + e.getMessage();
+            isError = true;
+            log(LogLevel.ERROR, "Tool error: " + toolName + " - " + 
e.getMessage());
+        }
+
+        JsonObject content = new JsonObject();
+        content.put("type", "text");
+        content.put("text", text);
+
+        JsonArray contentArray = new JsonArray();
+        contentArray.add(content);
+
+        JsonObject result = new JsonObject();
+        result.put("content", contentArray);
+        if (isError) {
+            result.put("isError", true);
+        }
+        return result;
+    }
+
+    private void addSelectionContext(JsonObject result) {
+        SelectionContext ctx = monitor.getSelectionContext();
+        if (ctx != null) {
+            JsonObject sel = new JsonObject();
+            sel.put("type", ctx.type());
+            sel.put("label", ctx.label());
+            sel.put("selectedIndex", ctx.selectedIndex());
+            sel.put("totalItems", ctx.totalItems());
+            JsonArray items = new JsonArray();
+            items.addAll(ctx.items());
+            sel.put("items", items);
+            result.put("selection", sel);
+        }
+    }
+
+    private String callGetScreen(Map<String, Object> args) {
+        Buffer buf = monitor.getLastBuffer();
+        if (buf == null) {
+            return "Screen not yet available";
+        }
+        boolean ansi = Boolean.TRUE.equals(args.get("ansi"));
+        String screen = ansi
+                ? ExportRequest.export(buf).text().options(o -> 
o.styles(true)).toString()
+                : ExportRequest.export(buf).text().toString();
+
+        JsonObject result = new JsonObject();
+        result.put("screen", screen);
+        result.put("width", buf.area().width());
+        result.put("height", buf.area().height());
+        addSelectionContext(result);
+        return Jsoner.serialize(result);
+    }
+
+    private String callGetEvents(Map<String, Object> args) {
+        int limit = 50;
+        Object limitArg = args.get("limit");
+        if (limitArg instanceof Number n) {
+            limit = n.intValue();
+        }
+
+        TuiEventLog eventLog = monitor.getEventLog();
+        List<TuiEventLog.Event> events = eventLog.getRecent(limit);
+
+        JsonArray eventsArray = new JsonArray();
+        for (TuiEventLog.Event event : events) {
+            JsonObject obj = new JsonObject();
+            obj.put("key", event.key());
+            obj.put("label", event.label());
+            obj.put("timestamp", event.timestamp().toString());
+            eventsArray.add(obj);
+        }
+
+        JsonObject result = new JsonObject();
+        result.put("events", eventsArray);
+        result.put("count", events.size());
+        return Jsoner.serialize(result);
+    }
+
+    private String callGetState() {
+        JsonObject result = new JsonObject();
+        result.put("activeTab", monitor.getActiveTabName());
+        result.put("tabIndex", monitor.getActiveTabIndex());
+
+        String pid = monitor.getSelectedPid();
+        if (pid != null) {
+            result.put("selectedPid", pid);
+        }
+        String name = monitor.getSelectedIntegrationName();
+        if (name != null) {
+            result.put("selectedIntegration", name);
+        }
+        result.put("integrationCount", monitor.getIntegrationCount());
+        result.put("keystrokesVisible", monitor.isKeystrokesVisible());
+        result.put("captionVisible", monitor.isCaptionVisible());
+        addSelectionContext(result);
+        return Jsoner.serialize(result);
+    }
+
+    private String callShowCaption(Map<String, Object> args) {
+        String text = (String) args.get("text");
+        if (text == null || text.isBlank()) {
+            return "Error: text is required";
+        }
+        Object durationArg = args.get("duration");
+        int duration = 0;
+        if (durationArg instanceof Number n) {
+            duration = n.intValue();
+        }
+
+        TapeRecorder recorder = monitor.getTapeRecorder();
+        if (recorder != null && recorder.isActive()) {
+            recorder.resetClock();
+            recorder.recordCaption(text, Math.max(duration, 0));
+        }
+
+        if (duration > 0) {
+            monitor.showCaption(text, duration);
+            return "Caption displayed (auto-dismiss in " + duration + "s): " + 
text;
+        }
+        monitor.showCaption(text);
+        return "Caption displayed: " + text;
+    }
+
+    private String callNavigate(Map<String, Object> args) {
+        JsonObject result = new JsonObject();
+        String tab = (String) args.get("tab");
+        String integration = (String) args.get("integration");
+
+        if (tab == null && integration == null) {
+            result.put("error", "Provide at least one of: tab, integration");
+            result.put("availableTabs", toJsonArray(monitor.getTabNames()));
+            result.put("availableIntegrations", 
toJsonArray(monitor.getIntegrationNames()));
+            return Jsoner.serialize(result);
+        }
+
+        if (integration != null) {
+            String selected = monitor.selectIntegration(integration);
+            if (selected != null) {
+                result.put("selectedIntegration", selected);
+            } else {
+                result.put("integrationError", "Not found: " + integration);
+                result.put("availableIntegrations", 
toJsonArray(monitor.getIntegrationNames()));
+            }
+        }
+
+        if (tab != null) {
+            String switched = monitor.navigateToTab(tab);
+            if (switched != null) {
+                result.put("activeTab", switched);
+                TapeRecorder recorder = monitor.getTapeRecorder();
+                if (recorder != null && recorder.isActive()) {
+                    recorder.resetClock();
+                    int tabIndex = monitor.getTabNames().indexOf(switched);
+                    if (tabIndex >= 0 && tabIndex < 9) {
+                        recorder.recordKey(String.valueOf(tabIndex + 1));
+                    }
+                }
+            } else {
+                result.put("tabError", "Unknown tab: " + tab);
+                result.put("availableTabs", 
toJsonArray(monitor.getTabNames()));
+            }
+        }
+
+        long beforeGen = monitor.getRenderGeneration();
+        long deadline = System.currentTimeMillis() + 2000;
+        while (System.currentTimeMillis() < deadline) {
+            if (monitor.getRenderGeneration() >= beforeGen + 2) {
+                break;
+            }
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+        Buffer buf = monitor.getLastBuffer();
+        if (buf != null) {
+            result.put("screen", ExportRequest.export(buf).text().toString());
+        }
+        addSelectionContext(result);
+        return Jsoner.serialize(result);
+    }
+
+    @SuppressWarnings("unchecked")
+    private String callSendKeys(Map<String, Object> args) {
+        Object keysArg = args.get("keys");
+        if (!(keysArg instanceof List)) {
+            return "Error: keys must be an array of strings";
+        }
+        List<String> keys = ((List<Object>) keysArg).stream()
+                .map(String::valueOf)
+                .toList();
+        if (keys.isEmpty()) {
+            return "Error: keys array is empty";
+        }
+        int delay = 150;
+        Object delayArg = args.get("delay");
+        if (delayArg instanceof Number n) {
+            delay = Math.max(80, n.intValue());
+        }
+        TapeRecorder recorder = monitor.getTapeRecorder();
+        if (recorder != null && recorder.isActive()) {
+            recorder.resetClock();
+            recorder.recordKeys(keys, delay);
+        }
+
+        boolean wait = Boolean.TRUE.equals(args.get("wait"));
+        long beforeGen = wait ? monitor.getRenderGeneration() : 0;
+        int sent = monitor.injectKeys(keys, delay);
+
+        if (!wait) {
+            return "Queued " + sent + " key(s) with " + delay + "ms delay";
+        }
+
+        long lastKeyFireAt = System.currentTimeMillis() + (long) (sent - 1) * 
delay;
+        long deadline = lastKeyFireAt + 5000;
+        long start = System.currentTimeMillis();
+
+        while (System.currentTimeMillis() < deadline) {
+            long now = System.currentTimeMillis();
+            if (now >= lastKeyFireAt) {
+                long gen = monitor.getRenderGeneration();
+                if (gen >= beforeGen + sent + 2) {
+                    break;
+                }
+            }
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+
+        Buffer buf = monitor.getLastBuffer();
+        JsonObject result = new JsonObject();
+        result.put("sent", sent);
+        result.put("delay", delay);
+        result.put("waitedMs", System.currentTimeMillis() - start);
+        if (buf != null) {
+            result.put("screen", ExportRequest.export(buf).text().toString());
+        }
+        addSelectionContext(result);
+        return Jsoner.serialize(result);
+    }
+
+    private String callGetOptions() {
+        JsonObject result = new JsonObject();
+        result.put("tabs", toJsonArray(monitor.getTabNames()));
+        result.put("activeTab", monitor.getActiveTabName());
+        result.put("activeTabIndex", monitor.getActiveTabIndex());
+        result.put("integrations", toJsonArray(monitor.getIntegrationNames()));
+        String selected = monitor.getSelectedIntegrationName();
+        if (selected != null) {
+            result.put("selectedIntegration", selected);
+        }
+        result.put("integrationCount", monitor.getIntegrationCount());
+
+        List<String> actions = monitor.getActionLabels();
+        JsonArray actionsArray = new JsonArray();
+        for (int i = 0; i < actions.size(); i++) {
+            JsonObject action = new JsonObject();
+            action.put("index", i);
+            action.put("label", actions.get(i));
+            action.put("keys", actionKeys(i, actions.size()));
+            actionsArray.add(action);
+        }
+        result.put("actions", actionsArray);
+        result.put("actionsHint",
+                "Press F2 to open the Actions menu, then use Down arrow to 
reach the item by index, then Enter to select.");
+
+        return Jsoner.serialize(result);
+    }
+
+    private String actionKeys(int index, int totalActions) {
+        StringBuilder sb = new StringBuilder("F2");
+        for (int i = 0; i < index; i++) {
+            sb.append(",Down");
+        }
+        sb.append(",Enter");
+        return sb.toString();
+    }
+
+    private String callWaitForIdle(Map<String, Object> args) {
+        int timeout = 5000;
+        Object timeoutArg = args.get("timeout");
+        if (timeoutArg instanceof Number n) {
+            timeout = Math.min(30_000, Math.max(500, n.intValue()));
+        }
+        int requiredFrames = 2;
+        Object framesArg = args.get("frames");
+        if (framesArg instanceof Number n) {
+            requiredFrames = Math.max(1, Math.min(10, n.intValue()));
+        }
+
+        long startGeneration = monitor.getRenderGeneration();
+        long start = System.currentTimeMillis();
+        long deadline = start + timeout;
+
+        while (System.currentTimeMillis() < deadline) {
+            long current = monitor.getRenderGeneration();
+            if (current >= startGeneration + requiredFrames) {
+                Buffer buf = monitor.getLastBuffer();
+                JsonObject result = new JsonObject();
+                result.put("settled", true);
+                result.put("waitedMs", System.currentTimeMillis() - start);
+                result.put("frames", current - startGeneration);
+                if (buf != null) {
+                    result.put("screen", 
ExportRequest.export(buf).text().toString());
+                }
+                addSelectionContext(result);
+                return Jsoner.serialize(result);
+            }
+            try {
+                Thread.sleep(50);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                break;
+            }
+        }
+
+        JsonObject result = new JsonObject();
+        result.put("settled", false);
+        result.put("waitedMs", System.currentTimeMillis() - start);
+        result.put("reason", "timeout");
+        return Jsoner.serialize(result);
+    }
+
+    private String callTapeStart(Map<String, Object> args) {
+        if (monitor.isTapeRecording()) {
+            return "Tape recording is already active. Stop it first with 
tui_tape_stop.";
+        }
+        String title = args.get("title") instanceof String s ? s : null;
+        monitor.startTapeRecording(title);
+        return "Tape recording started" + (title != null ? ": " + title : "");
+    }
+
+    private String callTapeStop(Map<String, Object> args) {
+        if (!monitor.isTapeRecording()) {
+            return "No tape recording is active. Start one with 
tui_tape_start.";
+        }
+        TapeRecorder recorder = monitor.getTapeRecorder();
+        String tape = recorder.stop();
+        int keyCount = recorder.getKeyCount();
+        long durationMs = recorder.getDurationMs();
+        monitor.clearTapeRecorder();
+
+        JsonObject result = new JsonObject();
+        result.put("tape", tape);
+        result.put("keyCount", keyCount);
+        result.put("duration", TapeRecorder.formatSleep(durationMs));
+
+        boolean save = Boolean.TRUE.equals(args.get("save"));
+        if (save) {
+            String timestamp = java.time.LocalDateTime.now()
+                    
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"));
+            String filename = "camel-tui-tape-" + timestamp + ".tape";
+            try {
+                
java.nio.file.Files.writeString(java.nio.file.Path.of(filename), tape);
+                result.put("file", filename);
+            } catch (java.io.IOException e) {
+                result.put("saveError", e.getMessage());
+            }
+        }
+
+        return Jsoner.serialize(result);
+    }
+
+    private String callSleep(Map<String, Object> args) {
+        Object secArg = args.get("seconds");
+        int seconds = secArg instanceof Number n ? n.intValue() : 3;
+        seconds = Math.max(1, Math.min(30, seconds));
+
+        TapeRecorder recorder = monitor.getTapeRecorder();
+        if (recorder != null && recorder.isActive()) {
+            recorder.resetClock();
+            recorder.recordSleep(seconds * 1000L);
+        }
+
+        try {
+            Thread.sleep(seconds * 1000L);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+
+        return "Slept for " + seconds + "s";
+    }
+
+    private static JsonArray toJsonArray(List<String> list) {
+        JsonArray arr = new JsonArray();
+        arr.addAll(list);
+        return arr;
+    }
+
+    // --- JSON-RPC helpers ---
+
+    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);
+        return sendJson(exchange, 200, response);
+    }
+
+    private void sendError(HttpExchange exchange, JsonObject request, int 
code, String message) throws IOException {
+        JsonObject error = new JsonObject();
+        error.put("code", code);
+        error.put("message", message);
+
+        JsonObject response = new JsonObject();
+        response.put("jsonrpc", "2.0");
+        response.put("id", request.get("id"));
+        response.put("error", error);
+        sendJson(exchange, 200, response);
+    }
+
+    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 ---
+
+    private JsonObject toolDef(String name, String description, Map<String, 
JsonObject> properties) {
+        return toolDef(name, description, properties, List.of());
+    }
+
+    private JsonObject toolDef(String name, String description, Map<String, 
JsonObject> properties, List<String> required) {
+        JsonObject schema = new JsonObject();
+        schema.put("type", "object");
+        if (!properties.isEmpty()) {
+            JsonObject props = new JsonObject();
+            props.putAll(properties);
+            schema.put("properties", props);
+        }
+        if (!required.isEmpty()) {
+            JsonArray req = new JsonArray();
+            req.addAll(required);
+            schema.put("required", req);
+        }
+
+        JsonObject tool = new JsonObject();
+        tool.put("name", name);
+        tool.put("description", description);
+        tool.put("inputSchema", schema);
+        return tool;
+    }
+
+    private JsonObject propDef(String type, String description) {
+        JsonObject prop = new JsonObject();
+        prop.put("type", type);
+        prop.put("description", description);
+        return prop;
+    }
+
+}

Reply via email to