Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fc80969
mcp(fix[search]): Block tmux format jobs in search_panes
tony Jun 13, 2026
b0fe5e0
mcp(fix[display]): Reject tmux format jobs
tony Jun 13, 2026
fe4148f
mcp(fix[middleware]): Raise structured response backstop
tony Jun 13, 2026
fab87c2
mcp(fix[safety]): Fail closed on invalid safety tier
tony Jun 13, 2026
2cbec6e
mcp(fix[cli]): Pin stdio transport at startup
tony Jun 13, 2026
bf2c7df
mcp(feat[pane]): Add run_command tool
tony Jun 13, 2026
15af979
mcp(fix[pane]): Use libtmux reset for clear_pane
tony Jun 13, 2026
c3b08eb
mcp(fix[metadata]): Expose pane metadata and typed marker
tony Jun 13, 2026
1c43cb9
mcp(fix[pane]): Keep run_command output in tail
tony Jun 13, 2026
97fccdf
mcp(test[middleware]): Xfail run_command command redaction
tony Jun 13, 2026
78f0cb8
mcp(fix[middleware]): Redact run_command command in audit log
tony Jun 13, 2026
14ca45f
mcp(test[pane]): Xfail run_command output over-filtering
tony Jun 13, 2026
76e57a5
mcp(fix[pane]): Filter run_command sync line by UUID only
tony Jun 13, 2026
64366ec
mcp(docs[utils]): Count run_command among ANNOTATIONS_SHELL tools
tony Jun 13, 2026
978413c
mcp(docs[search]): Note #( in format-injection comment
tony Jun 13, 2026
1e512b8
mcp(fix[pane]): Hide wrapped run_command sync fragments
tony Jun 13, 2026
9595ba8
mcp(test[pane]): Xfail run_command status isolation
tony Jun 13, 2026
9ca009c
mcp(fix[pane]): Isolate run_command status trailer
tony Jun 13, 2026
6ea53dd
mcp(test[pane]): Xfail run_command history suppression
tony Jun 13, 2026
f630c3f
mcp(fix[pane]): Add run_command history suppression
tony Jun 13, 2026
4080f6f
mcp(test[pane]): Xfail run_command sync filtering
tony Jun 13, 2026
a6718b2
mcp(fix[pane]): Tighten run_command sync filtering
tony Jun 13, 2026
061ca48
mcp(docs[safety]): Align clear_pane annotation table
tony Jun 13, 2026
bddfd35
mcp(docs[pane]): Restore join_wrapped rationale in run_command
tony Jun 13, 2026
7830593
mcp(docs[pane]): Drop bash-only HISTCONTROL from suppress_history
tony Jun 13, 2026
6dcc3cf
mcp(docs[pane]): Note run_command subshell isolation
tony Jun 13, 2026
040ea0c
mcp(test[pane]): Xfail run_command status targeting
tony Jun 13, 2026
1c462aa
mcp(fix[pane]): Target run_command status handoff
tony Jun 13, 2026
839b13e
mcp(style[pane]): Use target_pane_id in RunCommandResult
tony Jun 13, 2026
78f4f11
mcp(docs[CHANGES]): Tool-surface hardening and run_command
tony Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

**One-call command completion with {tooliconl}`run-command`**

{tooliconl}`run-command` runs a shell command in a pane, waits for it to finish, and returns the exit status, timeout state, and tail-preserved output in a single call — no manual {tooliconl}`send-keys` + {tooliconl}`wait-for-channel` + {tooliconl}`capture-pane` sequence. The command runs in a subshell so its state changes don't leak into later calls, and `suppress_history` keeps secret-bearing commands out of shell history where the shell ignores space-prefixed input. (#73)

**Richer, typed pane and window metadata**

{tooliconl}`snapshot-pane` now reports `pane_pid`, `pane_dead`, and `alternate_on` for liveness and alternate-screen decisions, and window results carry `active_pane_id` for reliable follow-up targeting. The package also ships a `py.typed` marker so downstream type checkers see its inline annotations. (#75)

### Fixes

**Read-only tools no longer evaluate tmux `#()` format jobs**

{tooliconl}`search-panes` and {tooliconl}`display-message` are advertised as read-only, but tmux `#(...)` formats schedule shell jobs during expansion. Both now reject or route around `#()` so a read-only call can never spawn a shell. (#68, #69)

**Invalid `LIBTMUX_SAFETY` fails closed**

An unrecognized `LIBTMUX_SAFETY` value now falls back to `readonly` instead of `mutating`, so a typo in the safety tier can no longer expose write tools the operator meant to hide. (#71)

**Large structured results keep their structured payload**

The global response backstop was truncating big successful results into text-only responses before tool-level caps ran, dropping the structured metadata schema-bearing tools depend on. It now matches FastMCP's 1 MB default, leaving per-tool line caps to handle terminal truncation. (#70)

**{tooliconl}`clear-pane` clears scrollback reliably**

{tooliconl}`clear-pane` now uses libtmux's single-call reset path; the previous two-call sequence could leave scrollback intact. Its annotations also disclose that it is destructive and non-idempotent. (#74)

**Stdio transport pinned at startup**

The server runs with an explicit stdio transport so an inherited FastMCP transport environment can't change its startup surface and break stdio clients, and `--help` / `--version` resolve locally without starting the server. (#72)

## libtmux-mcp 0.1.0a11 (2026-06-06)

libtmux-mcp 0.1.0a11 redesigns how tool failures reach agents. Error messages now arrive exactly as raised — no more `Internal error:` mangling — with structured detail and recovery hints that tell agents what to do next, from stale pane ids to stray arguments leaked by client schedulers. Expected, agent-correctable failures log at WARNING so ERROR records always mean an operator should look. The fastmcp floor rises to 3.4.0 to build on its error-result and log-level support.
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ Give your AI agent hands inside the terminal — create sessions, run commands,

| Module | Tools |
|--------|-------|
| **Server** | `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Server** | `list_servers`, `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Session** | `list_windows`, `get_session_info`, `create_window`, `rename_session`, `select_window`, `kill_session` |
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `capture_since`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `respawn_pane`, `kill_pane` |
| **Pane** | `run_command`, `send_keys`, `paste_text`, `capture_pane`, `capture_since`, `snapshot_pane`, `search_panes`, `find_pane_by_position`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `wait_for_channel`, `signal_channel`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `respawn_pane`, `kill_pane` |
| **Options** | `show_option`, `set_option` |
| **Environment** | `show_environment`, `set_environment` |
| **Buffers** | `load_buffer`, `paste_buffer`, `show_buffer`, `delete_buffer` |
| **Hooks** | `show_hooks`, `show_hook` |

## Quickstart

Expand Down Expand Up @@ -108,7 +110,7 @@ re-sending the same scrollback to the model on every check.
and declines self-destructive operations — [`kill_session`](https://libtmux-mcp.git-pull.com/tools/session/kill-session/)
on itself fails loudly instead of silently terminating the host
environment the agent is running in. [`LIBTMUX_SAFETY`](https://libtmux-mcp.git-pull.com/configuration/#envvar-LIBTMUX_SAFETY)
(`read`, `read+send`, `read+send+kill`) hides whole tiers from the
(`readonly`, `mutating`, `destructive`) hides whole tiers from the
client's tool list before any prompt is built.

## Documentation
Expand Down
16 changes: 7 additions & 9 deletions docs/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ counterpart to the longer narrative recipes in {doc}`/recipes`.
:::{grid-item-card} `run_and_wait`
:link: fastmcp-prompt-run-and-wait
:link-type: ref
Execute a shell command and block until it finishes, preserving exit status.
Execute a shell command and block until it finishes. Use
{tooliconl}`run-command` when exit status matters.
:::

:::{grid-item-card} `diagnose_failing_pane`
Expand Down Expand Up @@ -53,7 +54,7 @@ tools instead.
```

**Use when** the agent needs to execute a single shell command and
must know whether it succeeded before deciding the next step.
wait for completion through an explicit tmux signal.

**Why use this instead of `send_keys` + `capture_pane` polling?**
Each rendered call embeds a UUID-scoped ``tmux wait-for`` channel,
Expand Down Expand Up @@ -84,18 +85,15 @@ After the channel signals, read the last ~100 lines to verify the
command's behaviour. Do NOT use a `capture_pane` retry loop —
`wait_for_channel` is strictly cheaper in agent turns.

The payload does not preserve the command's exit status: doing so
in an interactive shell would require exiting the shell (which kills
the pane) or routing through an out-of-band file or tmux variable.
If you need the status, inspect the captured output for
command-specific success markers.
The payload does not preserve the command's exit status. Use
{tooliconl}`run-command` instead when exit status must be returned as
structured data.
````

Shell ``;`` semantics fire the ``wait-for -S`` whether ``pytest``
succeeded or failed, so the edge-triggered signal never deadlocks the
agent on a crashed command. Status preservation is intentionally
omitted: chaining ``exit $status`` after the signal would exit the
interactive shell itself, destroying single-pane sessions.
omitted from this prompt recipe.

---

Expand Down
7 changes: 7 additions & 0 deletions docs/tools/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ All tools accept an optional `socket_name` parameter for multi-server support. I
- Already know the `pane_id` → use it directly

**Running a command?**
- {tool}`run-command` — one call to run a shell command, wait for completion, capture output, and return exit status
- {tool}`send-keys` (with `tmux wait-for -S <channel>` composed into the keys) → {tool}`wait-for-channel` → {tool}`capture-pane` — the deterministic path for commands the agent authors
- For output the agent does not author (third-party logs, daemon prompts), use {tool}`wait-for-text` or {tool}`wait-for-content-change` between `send-keys` and `capture-pane`
- Pasting multi-line text? → {tool}`paste-text`
Expand Down Expand Up @@ -223,6 +224,12 @@ Split a window into panes.
Send commands or keystrokes to a pane.
:::

:::{grid-item-card} run_command
:link: run-command
:link-type: ref
Run a shell command and report exit status.
:::

:::{grid-item-card} rename_session
:link: rename-session
:link-type: ref
Expand Down
2 changes: 1 addition & 1 deletion docs/tools/pane/clear-pane.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

**Use when** you want a clean terminal before capturing output.

**Side effects:** Clears the pane's visible content.
**Side effects:** Clears the pane's visible content and scrollback.

**Example:**

Expand Down
5 changes: 5 additions & 0 deletions docs/tools/pane/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Evaluate a tmux format string against a target.
Send keystrokes or commands to a pane.
:::

:::{grid-item-card} {tooliconl}`run-command`
Run a shell command, wait, and capture output.
:::

:::{grid-item-card} {tooliconl}`paste-text`
Paste multi-line text via tmux buffer.
:::
Expand Down Expand Up @@ -111,6 +115,7 @@ get-pane-info
find-pane-by-position
display-message
send-keys
run-command
paste-text
pipe-pane
select-pane
Expand Down
47 changes: 47 additions & 0 deletions docs/tools/pane/run-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Run command

```{fastmcp-tool} pane_tools.run_command
```

**Use when** you need to run a shell command in a pane and get a typed
result with exit status, timeout state, and captured pane output.

**Avoid when** you need raw interactive key driving — use
{tooliconl}`send-keys` for TUIs, key names, and partial commands.

**Side effects:** Sends a command to the pane's interactive shell. The
command may read or write files, start processes, or access the network
depending on what the shell command does. Each command runs in a subshell,
so directory or environment changes do not persist across calls.
Set `suppress_history=true` for secret-bearing commands on shells that
honor leading-space history suppression.

**Example:**

```json
{
"tool": "run_command",
"arguments": {
"command": "pytest -q",
"pane_id": "%2",
"timeout": 60
}
}
```

Response:

```json
{
"pane_id": "%2",
"exit_status": 0,
"timed_out": false,
"elapsed_seconds": 4.2,
"output": ["..."],
"output_truncated": false,
"output_truncated_lines": 0
}
```

```{fastmcp-tool-input} pane_tools.run_command
```
3 changes: 3 additions & 0 deletions docs/tools/pane/snapshot-pane.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Response:
"pane_at_top": true,
"pane_at_bottom": true,
"pane_tty": "/dev/pts/5",
"pane_pid": "12345",
"pane_dead": false,
"alternate_on": false,
"pane_in_mode": false,
"pane_mode": null,
"scroll_position": null,
Expand Down
3 changes: 2 additions & 1 deletion docs/tools/session/create-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Response:
"window_layout": "b25f,80x24,0,0,5",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%5"
}
```

Expand Down
6 changes: 4 additions & 2 deletions docs/tools/session/list-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ Response:
"window_layout": "c195,80x24,0,0[80x12,0,0,0,80x11,0,13,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%0"
},
{
"window_id": "@1",
Expand All @@ -47,7 +48,8 @@ Response:
"window_layout": "b25f,80x24,0,0,2",
"window_active": "0",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%2"
}
]
```
Expand Down
3 changes: 2 additions & 1 deletion docs/tools/session/select-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Response:
"window_layout": "b25f,80x24,0,0,2",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%2"
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/window/get-window-info.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ Response:
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%1"
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/window/move-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Response:
"window_layout": "b25f,80x24,0,0,2",
"window_active": "0",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%2"
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/window/rename-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Response:
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%0"
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/window/resize-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Response:
"window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]",
"window_active": "1",
"window_width": "120",
"window_height": "40"
"window_height": "40",
"active_pane_id": "%0"
}
```

Expand Down
3 changes: 2 additions & 1 deletion docs/tools/window/select-layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ Response:
"window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
"window_height": "24",
"active_pane_id": "%0"
}
```

Expand Down
6 changes: 4 additions & 2 deletions docs/topics/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ src/libtmux_mcp/
models.py # Pydantic output models
middleware.py # Safety, audit, retry, and error-result middleware
tools/
server_tools.py # list_sessions, create_session, kill_server, get_server_info
server_tools.py # list_servers, list_sessions, create_session, kill_server, get_server_info
session_tools.py # list_windows, create_window, rename_session, kill_session
window_tools.py # list_panes, split_window, rename_window, kill_window, select_layout, resize_window
pane_tools.py # send_keys, capture_pane, capture_since, resize_pane, kill_pane, set_pane_title, get_pane_info, clear_pane, search_panes, wait_for_text
pane_tools.py # run_command, send_keys, capture_pane, capture_since, snapshot_pane, search_panes, wait_for_text
buffer_tools.py # load_buffer, paste_buffer, show_buffer, delete_buffer
hook_tools.py # show_hooks, show_hook
option_tools.py # show_option, set_option
env_tools.py # show_environment, set_environment
resources/
Expand Down
23 changes: 8 additions & 15 deletions docs/topics/completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

# Completion

libtmux-mcp inherits FastMCP's built-in
The
[MCP completion](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion)
behaviour. We don't hand-author completion providers — the argument
shapes on our prompts and resource templates are what the client
sees.
protocol lets clients ask a server for argument suggestions. libtmux-mcp
does not currently register custom completion handlers.

## What the spec does

Expand All @@ -19,22 +18,16 @@ session picker popup when filling ``session_name=`` on
## What libtmux-mcp currently exposes

- **Prompt arguments** — the four recipes ({doc}`/prompts`)
advertise their argument names and types. FastMCP derives a default
completion shape from the Python signatures:
``str`` arguments accept free text, ``float`` arguments accept
numeric strings, no enum / list suggestions.
advertise their argument names and types through their schemas.
- **Resource template parameters** —
{doc}`/resources` URIs carry ``{session_name}``,
``{window_index}``, ``{pane_id}``, and ``{?socket_name}``
placeholders. Completion suggestions are again derived from the
function signatures' types, not from live tmux state.
placeholders.

```{warning}
libtmux-mcp does **not** currently wire completion back to live
tmux enumeration — i.e. the completion for ``session_name`` will not
return the names of sessions that exist on the server right now.
Adding that requires a dedicated FastMCP completion handler;
tracked as a potential enhancement.
Clients should not rely on ``completion/complete`` returning live tmux
suggestions, schema-derived examples, or enum-like values today.
Adding live suggestions requires dedicated completion handlers.
```

## Workarounds for clients that need live enumeration
Expand Down
12 changes: 5 additions & 7 deletions docs/topics/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,11 @@ However, they reset when the tmux **server** restarts. Do not cache pane IDs acr

## `suppress_history` requires shell support

The `suppress_history` parameter on `send_keys` prepends a space before the command, which prevents it from being saved in shell history. This only works if the shell's `HISTCONTROL` variable includes `ignorespace` (the default for bash, but not universal across all shells).

## `clear_pane` is not fully atomic

`clear_pane` runs two tmux commands in sequence: `send-keys -R` (reset terminal) then `clear-history` (clear scrollback). There is a brief gap between them where partial content may be visible.

For most use cases this is not a problem. If you need guaranteed clean state, add a small delay before the next `capture_pane`.
The `suppress_history` parameter on {tooliconl}`send-keys` and
{tooliconl}`run-command` prepends a space before the command, which prevents it
from being saved in shell history. This only works if the shell's `HISTCONTROL`
variable includes `ignorespace` (the default for bash, but not universal across
all shells).

## Gemini CLI injects `wait_for_previous` into tool arguments

Expand Down
Loading