# MCP Clients Source: https://libtmux-mcp.git-pull.com/clients/ (clients)= # MCP Clients Pick your client, install method, and config scope below — the snippet updates accordingly. The scope row appears only for clients with more than one scope (Claude Desktop is always user-level so it has no scope row). The full table of file locations is at the bottom of the page. ```{mcp-install} :variant: full ``` If your client isn't listed, any tool supporting MCP stdio transport will work with the JSON config pattern shown for Claude Desktop or Cursor. See {ref}`migration` for the recommended `tmux` registration slug (existing `libtmux` registrations keep working). ## MCP Inspector For testing and debugging: ```console $ npx @modelcontextprotocol/inspector ``` ## Config file locations | Client | Config file | Format | |--------|-------------|--------| | Claude Code | `.mcp.json` (project) or `~/.claude.json` (local/user) | JSON | | Claude Desktop | `claude_desktop_config.json` | JSON | | Codex CLI | `~/.codex/config.toml` (user) or `.codex/config.toml` (project, manual) | TOML | | Gemini CLI | `~/.gemini/settings.json` (user) or `.gemini/settings.json` (project) | JSON | | Cursor | `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) | JSON | ## Local checkout (development) For live development, point your client at a local checkout via `uv --directory`: **Claude Code:** ```console $ claude mcp add \ --scope user \ tmux -- \ uv --directory ~/work/python/libtmux-mcp \ run libtmux-mcp ```
Codex CLI / Gemini CLI / Cursor **Codex CLI:** ```console $ codex mcp add tmux -- \ uv --directory ~/work/python/libtmux-mcp \ run libtmux-mcp ``` **Gemini CLI:** ```console $ gemini mcp add \ --scope user \ tmux uv -- \ --directory ~/work/python/libtmux-mcp \ run libtmux-mcp ``` **Cursor** — add to `~/.cursor/mcp.json`: ```json { "mcpServers": { "tmux": { "command": "uv", "args": [ "--directory", "~/work/python/libtmux-mcp", "run", "libtmux-mcp" ] } } } ```
## Common pitfalls - **Absolute paths**: Some clients require absolute paths in config. Use `$HOME/...` or the full path instead of `~/...`. - **Virtual environments**: If using pip install, ensure the venv is activated or the `libtmux-mcp` binary is on your PATH. - **Socket isolation**: Set `LIBTMUX_SOCKET` in the `env` block to isolate the MCP server from your default tmux. See {ref}`configuration`. --- # Configuration Source: https://libtmux-mcp.git-pull.com/configuration/ (configuration)= # Configuration Runtime configuration for the libtmux-mcp server. For MCP client setup, see {ref}`clients`. ## Environment variables ```{envvar} LIBTMUX_SOCKET ``` tmux socket name (`-L`). Isolates the MCP server to a specific tmux socket. - **Type:** string - **Default:** (none — uses the default tmux socket) ```{envvar} LIBTMUX_SOCKET_PATH ``` tmux socket path (`-S`). Alternative to socket name for custom socket locations. - **Type:** string - **Default:** (none) ```{envvar} LIBTMUX_TMUX_BIN ``` Path to tmux binary. Useful for testing with different tmux versions. - **Type:** string - **Default:** `tmux` ```{envvar} LIBTMUX_SAFETY ``` Safety tier controlling which tools are available. See {ref}`safety`. - **Type:** string - **Default:** `mutating` - **Values:** `readonly`, `mutating`, `destructive` ## Setting environment variables Set environment variables in your MCP client config: ```json { "mcpServers": { "libtmux": { "command": "uvx", "args": ["libtmux-mcp"], "env": { "LIBTMUX_SOCKET": "ai_workspace", "LIBTMUX_SAFETY": "readonly" } } } } ``` ## Socket isolation By default, the MCP server connects to the default tmux socket. Set {envvar}`LIBTMUX_SOCKET` to isolate AI agent activity from your personal tmux sessions: ```json "env": { "LIBTMUX_SOCKET": "ai_workspace" } ``` The agent will only see sessions on the `ai_workspace` socket, not your personal sessions. ## All tools accept `socket_name` Every tool accepts an optional `socket_name` parameter that overrides {envvar}`LIBTMUX_SOCKET` for that call. This allows agents to work across multiple tmux servers in a single session. --- # Badge & Role Demo Source: https://libtmux-mcp.git-pull.com/demo/ --- orphan: true --- # Badge & Role Demo A showcase of the custom Sphinx roles and visual elements available in libtmux-mcp documentation. ## Safety badges Standalone badges via `{badge}`: - {badge}`readonly` — green, read-only operations - {badge}`mutating` — amber, state-changing operations - {badge}`destructive` — red, irreversible operations ## Tool references ### `{tool}` — code-linked with badge {tool}`capture-pane` · {tool}`capture-since` · {tool}`send-keys` · {tool}`search-panes` · {tool}`wait-for-text` · {tool}`kill-pane` · {tool}`create-session` · {tool}`split-window` ### `{toolref}` — code-linked, no badge {toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`send-keys` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`kill-pane` · {toolref}`create-session` · {toolref}`split-window` ### `{tooliconl}` — icon left, outside code {tooliconl}`capture-pane` · {tooliconl}`capture-since` · {tooliconl}`send-keys` · {tooliconl}`search-panes` · {tooliconl}`wait-for-text` · {tooliconl}`kill-pane` · {tooliconl}`create-session` · {tooliconl}`split-window` ### `{tooliconr}` — icon right, outside code {tooliconr}`capture-pane` · {tooliconr}`capture-since` · {tooliconr}`send-keys` · {tooliconr}`search-panes` · {tooliconr}`wait-for-text` · {tooliconr}`kill-pane` · {tooliconr}`create-session` · {tooliconr}`split-window` ### `{tooliconil}` — icon inline-left, inside code {tooliconil}`capture-pane` · {tooliconil}`capture-since` · {tooliconil}`send-keys` · {tooliconil}`search-panes` · {tooliconil}`wait-for-text` · {tooliconil}`kill-pane` · {tooliconil}`create-session` · {tooliconil}`split-window` ### `{tooliconir}` — icon inline-right, inside code {tooliconir}`capture-pane` · {tooliconir}`capture-since` · {tooliconir}`send-keys` · {tooliconir}`search-panes` · {tooliconir}`wait-for-text` · {tooliconir}`kill-pane` · {tooliconir}`create-session` · {tooliconir}`split-window` ### `{ref}` — plain text link {ref}`capture-pane` · {ref}`capture-since` · {ref}`send-keys` · {ref}`search-panes` · {ref}`wait-for-text` · {ref}`kill-pane` · {ref}`create-session` · {ref}`split-window` ## Badges in context ### In a heading These are the actual tool headings as they render on tool pages: > `capture_pane` {badge}`readonly` > `split_window` {badge}`mutating` > `kill_session` {badge}`destructive` ### In a table | Tool | Tier | Description | |------|------|-------------| | {toolref}`list-sessions` | {badge}`readonly` | List all sessions | | {toolref}`send-keys` | {badge}`mutating` | Send commands to a pane | | {toolref}`kill-pane` | {badge}`destructive` | Destroy a pane | ### In prose Use {tooliconl}`search-panes` to find text across all panes. If you know which pane, use {tooliconl}`capture-pane` for one read or {tooliconl}`capture-since` for repeated observation. After running a command with {tooliconl}`send-keys`, compose `tmux wait-for -S` and call {tooliconl}`wait-for-channel` before capturing. ### Dense inline (toolref, no badges) The fundamental pattern: {toolref}`send-keys` → {toolref}`wait-for-channel` → {toolref}`capture-pane`. For discovery: {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`get-pane-info`. ## Environment variable references {envvar}`LIBTMUX_SOCKET` · {envvar}`LIBTMUX_SAFETY` · {envvar}`LIBTMUX_SOCKET_PATH` · {envvar}`LIBTMUX_TMUX_BIN` ## Glossary terms {term}`SIGINT` · {term}`SIGQUIT` · {term}`MCP` · {term}`Safety tier` · {term}`Pane` · {term}`Session` ## Admonitions ```{tip} Use {tooliconl}`search-panes` before {tooliconl}`capture-pane` when you don't know which pane has the output you need. ``` ```{warning} Do not call {toolref}`capture-pane` immediately after {toolref}`send-keys` — there is a race condition. Compose `tmux wait-for -S` into the command and use {toolref}`wait-for-channel` between them. ``` ```{note} All tools accept an optional `socket_name` parameter for multi-server support. ``` ## Badge anatomy Each badge renders as: ```html 🔍 readonly ``` Features: - **Emoji icon** — 🔍 readonly, ✏️ mutating, 💣 destructive (native system emoji, no filters) - **Matte colors** — forest green, smoky amber, matte crimson with 1px border - **Accessible** — `role="note"` + `aria-label` for screen readers - **Non-selectable** — `user-select: none` so copying tool names skips badge text - **Context-aware sizing** — slightly larger in headings, smaller inline - **Sidebar compression** — badges collapse to colored dots in the right-side TOC - **Heading flex** — `h2/h3/h4:has(.sd-badge)` centers badge against cap-height --- # Glossary Source: https://libtmux-mcp.git-pull.com/glossary/ (glossary)= # Glossary ```{glossary} MCP Model Context Protocol. A standard for AI agents to interact with tools and resources. FastMCP A Python framework for building MCP servers. libtmux-mcp uses FastMCP to expose tmux operations as MCP tools. libtmux A typed Python library that provides an ORM wrapper for tmux. libtmux-mcp depends on libtmux for all tmux interactions. tmux A terminal multiplexer. It lets you switch easily between several programs in one terminal, detach them, and reattach them to a different terminal. See https://github.com/tmux/tmux. Server A tmux server instance. Manages sessions and communicates via a socket. Session A tmux session. Contains one or more windows. Has a name and ID (e.g. `$1`). Window A tmux window within a session. Contains one or more panes. Has a name, index, and ID (e.g. `@1`). Pane A tmux pane within a window. A pseudoterminal that runs a single process. Has an ID (e.g. `%1`) that is globally unique within a server. Safety tier A level controlling which MCP tools are available: `readonly`, `mutating`, or `destructive`. Set via the {envvar}`LIBTMUX_SAFETY` env var. Socket The Unix socket used to communicate with a tmux server. Can be specified by name (`-L`) or path (`-S`). SIGINT Interrupt signal (Ctrl-C). Sent via {toolref}`send-keys` with `keys: "C-c"` and `enter: false`. Most processes terminate gracefully on SIGINT. SIGQUIT Quit signal (Ctrl-\\). Sent via {toolref}`send-keys` with `keys: "C-\\"` and `enter: false`. Stronger than {term}`SIGINT` — may produce a core dump on Unix. Use as an escalation when SIGINT is ignored. ``` --- # Changelog Source: https://libtmux-mcp.git-pull.com/history/ (changes)= (changelog)= (history)= ```{currentmodule} libtmux_mcp ``` ```{include} ../CHANGES :parser: markdown ``` --- # libtmux-mcp Source: https://libtmux-mcp.git-pull.com/ (index)= # libtmux-mcp Terminal control for AI agents, built on [libtmux](https://libtmux.git-pull.com) and [FastMCP](https://gofastmcp.com). This server maps tmux's object hierarchy — sessions, windows, panes — into MCP tools. Some tools read state. Some mutate it. Some destroy. The distinction is explicit and enforced. ```{warning} **Pre-alpha.** APIs may change. [Feedback welcome](https://github.com/tmux-python/libtmux-mcp/issues). ``` ```{mcp-install} :variant: compact ``` --- ::::{grid} 1 1 2 2 :gutter: 3 :::{grid-item-card} Quickstart :link: quickstart :link-type: doc Install, connect, get a first result. Under 2 minutes. ::: :::{grid-item-card} Tools :link: tools/index :link-type: doc Every tool, grouped by intent and safety tier. ::: :::{grid-item-card} Prompts :link: prompts :link-type: doc Four workflow recipes the client renders for the model. ::: :::{grid-item-card} Resources :link: resources :link-type: doc Snapshot views of the tmux hierarchy via `tmux://` URIs. ::: :::{grid-item-card} Safety tiers :link: topics/safety :link-type: doc Readonly, mutating, destructive. Know what changes state. ::: :::{grid-item-card} Client setup :link: clients :link-type: doc Config blocks for Claude Desktop, Claude Code, Cursor, and others. ::: :::: --- ## What you can do ### Inspect (readonly) Read tmux state without changing anything. {toolref}`list-sessions` · {toolref}`capture-pane` · {toolref}`capture-since` · {toolref}`snapshot-pane` · {toolref}`get-pane-info` · {toolref}`find-pane-by-position` · {toolref}`search-panes` · {toolref}`wait-for-text` · {toolref}`wait-for-content-change` · {toolref}`display-message` ### Act (mutating) Create or modify tmux objects. {toolref}`create-session` · {toolref}`send-keys` · {toolref}`paste-text` · {toolref}`create-window` · {toolref}`split-window` · {toolref}`select-pane` · {toolref}`select-window` · {toolref}`move-window` · {toolref}`resize-pane` · {toolref}`pipe-pane` · {toolref}`set-option` ### Destroy (destructive) Tear down tmux objects. Not reversible. {toolref}`kill-session` · {toolref}`kill-window` · {toolref}`kill-pane` · {toolref}`kill-server` [Browse all tools →](tools/index) --- ## Mental model - **Object hierarchy** — sessions contain windows, windows contain panes ({doc}`topics/concepts`) - **Read vs. mutate** — some tools observe, some act, some destroy ({doc}`topics/safety`) - **tmux is the source of truth** — the server reads from it and writes to it, never caches or abstracts --- ```{toctree} :hidden: :caption: Get started quickstart installation clients ``` ```{toctree} :hidden: :caption: Use it tools/index prompts resources recipes configuration ``` ```{toctree} :hidden: :caption: Understand it topics/index ``` ```{toctree} :hidden: :caption: Reference reference/api/index reference/compatibility glossary ``` ```{toctree} :hidden: :caption: Project project/index history migration GitHub ``` --- # Installation Source: https://libtmux-mcp.git-pull.com/installation/ (installation)= # Installation ## Requirements - Python 3.10+ - tmux >= 3.2a - [uv](https://github.com/astral-sh/uv) ([install](https://docs.astral.sh/uv/getting-started/installation/)) or [pipx](https://github.com/pypa/pipx) ([install](https://pipx.pypa.io/stable/installation/)) — for running without a persistent install ## Run without installing No persistent install needed — run directly with a package executor: `````{tab} uvx ```console $ uvx libtmux-mcp ``` ````` `````{tab} pipx ```console $ pipx run libtmux-mcp ``` ````` To wire it into your MCP client, see {ref}`clients`. ## Install the package `````{tab} uv ```console $ uv pip install libtmux-mcp ``` ````` `````{tab} pip ```console $ pip install libtmux-mcp ``` ````` ## Development install Install [uv](https://github.com/astral-sh/uv) ([install](https://docs.astral.sh/uv/getting-started/installation/)), then clone and install in editable mode: ```console $ git clone https://github.com/tmux-python/libtmux-mcp.git ``` ```console $ cd libtmux-mcp ``` ```console $ uv pip install -e "." ``` Code changes take effect immediately — no reinstall needed. ## Running the server ```console $ libtmux-mcp ``` Or via Python module: ```console $ python -m libtmux_mcp ``` ## Upgrading `````{tab} uv ```console $ uv pip install --upgrade libtmux-mcp ``` ````` `````{tab} pip ```console $ pip install --upgrade libtmux-mcp ``` ````` With `uvx` or `pipx run`, you always get the latest version automatically. --- # Migration notes Source: https://libtmux-mcp.git-pull.com/migration/ (migration)= ```{include} ../MIGRATION ``` --- # Code style Source: https://libtmux-mcp.git-pull.com/project/code-style/ (code-style)= # Code style ## Linting and formatting libtmux-mcp uses [ruff](https://docs.astral.sh/ruff/) for linting and formatting: ```console $ uv run ruff check . ``` ```console $ uv run ruff format . ``` ## Type checking [mypy](https://mypy-lang.org/) with strict mode: ```console $ uv run mypy ``` ## Docstrings NumPy-style docstrings throughout. ## Imports - `from __future__ import annotations` at the top of every file. - `import typing as t` and access via namespace. - Standard-library modules imported by namespace (`import pathlib`, not `from pathlib import Path`). --- # Development Source: https://libtmux-mcp.git-pull.com/project/contributing/ # Development Install [git] and [uv] ([install](https://docs.astral.sh/uv/getting-started/installation/)) [git]: https://git-scm.com/ [uv]: https://github.com/astral-sh/uv Clone: ```console $ git clone https://github.com/tmux-python/libtmux-mcp.git ``` ```console $ cd libtmux-mcp ``` Install: ```console $ uv pip install -e . -G dev ``` ## Testing ```console $ uv run pytest ``` Run a specific test file: ```console $ uv run pytest tests/test_pane_tools.py ``` Run a specific test: ```console $ uv run pytest tests/test_pane_tools.py::test_send_keys ``` Watch mode: ```console $ uv run ptw . ``` ## Linting ```console $ uv run ruff check . ``` Format: ```console $ uv run ruff format . ``` Auto-fix: ```console $ uv run ruff check . --fix --show-fixes ``` ## Type checking ```console $ uv run mypy ``` ## Documentation Build: ```console $ just build-docs ``` Serve with auto-reload: ```console $ just start-docs ``` ## Workflow 1. Format: `uv run ruff format .` 2. Test: `uv run pytest` 3. Lint: `uv run ruff check . --fix --show-fixes` 4. Types: `uv run mypy` 5. Verify: `uv run pytest` ## Releasing Releases are published to PyPI via GitHub Actions when a tag is pushed: ```console $ git tag v0.1.0 ``` ```console $ git push --tags ``` The CI workflow builds the package, creates attestations, and publishes via OIDC trusted publishing. --- # Project Source: https://libtmux-mcp.git-pull.com/project/ (project)= # Project Information for contributors and maintainers. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Contributing :link: contributing :link-type: doc Development setup, running tests, submitting PRs. ::: :::{grid-item-card} Code Style :link: code-style :link-type: doc Ruff, mypy, NumPy docstrings, import conventions. ::: :::{grid-item-card} Releasing :link: releasing :link-type: doc Release checklist and version policy. ::: :::: ```{toctree} :hidden: contributing code-style releasing ``` --- # Releasing Source: https://libtmux-mcp.git-pull.com/project/releasing/ (releasing)= # Releasing ## Version scheme libtmux-mcp follows [PEP 440](https://peps.python.org/pep-0440/) with alpha suffixes during pre-1.0 development (e.g. `0.1.0a0`). ## Release checklist 1. Update `CHANGES`. 2. Bump version in `src/libtmux_mcp/__about__.py`. 3. Commit and tag: `git tag v0.X.Y`. 4. Push with tags: `git push --follow-tags`. 5. CI publishes to PyPI. --- # Prompts Source: https://libtmux-mcp.git-pull.com/prompts/ (prompts-overview)= # Prompts MCP prompts are reusable, parameterised text templates the server ships to its clients. A client renders a prompt by calling ``prompts/get``; the rendered text is what the model sees. libtmux-mcp's prompts are short *workflow recipes* — the MCP-shaped counterpart to the longer narrative recipes in {doc}`/recipes`. ## Available prompts ::::{grid} 1 2 2 2 :gutter: 2 2 3 3 :::{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. ::: :::{grid-item-card} `diagnose_failing_pane` :link: fastmcp-prompt-diagnose-failing-pane :link-type: ref Gather pane context and produce a root-cause hypothesis without taking action. ::: :::{grid-item-card} `build_dev_workspace` :link: fastmcp-prompt-build-dev-workspace :link-type: ref Set up a 3-pane editor / shell / logs layout shell-agnostically. ::: :::{grid-item-card} `interrupt_gracefully` :link: fastmcp-prompt-interrupt-gracefully :link-type: ref Send SIGINT and verify the shell prompt returns, refusing to auto-escalate. ::: :::: ```{tip} Most MCP clients render prompts via a slash-command UI (``/:``). For tools-only clients that don't expose prompts, set ``LIBTMUX_MCP_PROMPTS_AS_TOOLS=1`` in the server environment to surface them as ``list_prompts`` / ``get_prompt`` tools instead. ``` --- ```{fastmcp-prompt} run_and_wait ``` **Use when** the agent needs to execute a single shell command and must know whether it succeeded before deciding the next step. **Why use this instead of `send_keys` + `capture_pane` polling?** Each rendered call embeds a UUID-scoped ``tmux wait-for`` channel, so concurrent agents (or parallel prompt calls from one agent) can never cross-signal each other. The server side blocks until the channel is signalled — strictly cheaper in agent turns than a ``capture_pane`` retry loop. ```{fastmcp-prompt-input} run_and_wait ``` **Sample render** (``command="pytest"``, ``pane_id="%1"``): ````markdown Run this shell command in tmux pane %1 and block until it finishes: ```python send_keys( pane_id='%1', keys='pytest; tmux wait-for -S libtmux_mcp_wait_', ) wait_for_channel(channel='libtmux_mcp_wait_', timeout=60.0) capture_pane(pane_id='%1', max_lines=100) ``` 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. ```` 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. --- ```{fastmcp-prompt} diagnose_failing_pane ``` **Use when** something visibly went wrong in a pane and the agent needs to investigate before deciding what to fix. Produces a plan, not an action. **Why use this instead of just calling `capture_pane`?** The recipe prefers {tool}`snapshot-pane`, which returns content + cursor position + pane mode + scroll state in one call — saving a follow-up ``get_pane_info`` round-trip. It also explicitly forbids the agent from acting before it has a hypothesis, which prevents "fix the symptom" anti-patterns. For repeated observation, it routes follow-up reads through {tool}`capture-since` cursors instead of full pane captures. ```{fastmcp-prompt-input} diagnose_failing_pane ``` **Sample render** (``pane_id="%1"``): ```markdown Something went wrong in tmux pane %1. Diagnose it: 1. Call `snapshot_pane(pane_id="%1")` to get content, cursor position, pane mode, and scroll state in one call. 2. If the content looks truncated, re-call with `max_lines=None`. 3. If you need to watch the pane across more than one turn, call `capture_since(pane_id="%1")`, keep the returned cursor, and pass it to later `capture_since(cursor=...)` calls. 4. Identify the last command that ran (look at the prompt line and the line above it) and the last non-empty output line. 5. Propose a root cause hypothesis and a minimal command to verify it (do NOT execute anything yet — produce the plan first). ``` --- ```{fastmcp-prompt} build_dev_workspace ``` **Use when** the operator wants a fresh 3-pane workspace with editor on top, terminal bottom-left, and a logs pane bottom-right — the most common shape for active development. **Why use this instead of describing the layout in free form?** The recipe uses real parameter names that match the tools' actual signatures (``session_name=``, ``pane_id=``, ``direction="below"``) so an agent following it verbatim never hits a validation error. It also explicitly avoids waiting for shell prompts after launching ``vim`` / ``watch`` / ``tail -f`` — the kind of guidance that would deadlock an agent following naïve "wait for the prompt between each step" advice. ```{fastmcp-prompt-input} build_dev_workspace ``` Pass e.g. ``"tail -f /var/log/syslog"`` on Linux or ``"log stream --level info"`` on macOS as the ``log_command`` to override the OS-neutral default. **Sample render** (``session_name="dev"``): ````markdown Set up a 3-pane development workspace named 'dev' with editor on top, a shell on the bottom-left, and a logs tail on the bottom-right: 1. `create_session(session_name="dev")` — creates the session with a single pane (pane A, the editor). Capture the returned `active_pane_id` as `%A`. 2. `split_window(pane_id="%A", direction="below")` — splits off the bottom half (pane B, the terminal). Capture the returned `pane_id` as `%B`. 3. `split_window(pane_id="%B", direction="right")` — splits pane B horizontally (pane C, the logs pane). Capture the returned `pane_id` as `%C`. 4. Launch the editor and the log command via `send_keys`: `send_keys(pane_id="%A", keys="vim")` and `send_keys(pane_id="%C", keys='watch -n 1 date')`. Leave pane B at its fresh shell prompt — nothing needs to be sent there. No pre-launch wait is required: tmux buffers keystrokes into the pane's PTY whether or not the shell has finished drawing, so `send_keys` immediately after `split_window` is safe and shell-agnostic. 5. Optionally confirm each program drew its UI via `wait_for_content_change(pane_id="%A", timeout=3.0)` (and similarly for `%C`). This is a "did the screen change?" check — it works whether the pane shows a prompt glyph, a vim splash screen, or a log tail, so no shell-specific regex is needed. Use pane IDs (`%N`) for all subsequent targeting — they are stable across layout changes; window renames are not. ```` --- ```{fastmcp-prompt} interrupt_gracefully ``` **Use when** the agent needs to stop a running command and confirm control returned to the shell — without escalating beyond SIGINT. **Why use this instead of just sending `C-c`?** The recipe pairs the interrupt with a {tool}`wait-for-text` against a common shell prompt glyph and an explicit instruction to *stop and ask* if the wait times out. That prevents the most dangerous failure mode — an agent auto-escalating to ``C-\\`` (SIGQUIT, may core-dump) or ``kill`` without operator consent — by drawing a clear escalation boundary. ```{fastmcp-prompt-input} interrupt_gracefully ``` **Sample render** (``pane_id="%1"``): ````markdown Interrupt whatever is running in pane %1 and verify that control returns to the shell: 1. `send_keys(pane_id="%1", keys="C-c", literal=False, enter=False)` — tmux interprets `C-c` as SIGINT. 2. `wait_for_text(pane_id="%1", pattern="\$ |\# |\% ", regex=True, timeout=5.0)` — waits for a common shell prompt glyph. Adjust the pattern to match the user's shell theme. 3. If the wait times out the process is ignoring SIGINT. Stop and ask the caller how to proceed — do NOT escalate automatically to `C-\` (SIGQUIT) or `kill`. ```` The shell-prompt regex covers default bash / zsh — adjust for fish (``> ``), zsh + oh-my-zsh (``➜ ``), or starship (``❯ ``). When the pattern doesn't match the user's prompt theme the recipe times out and surfaces the situation to the caller, which is the right default for "I tried, can't tell, what should I do?" workflows. --- # Quickstart Source: https://libtmux-mcp.git-pull.com/quickstart/ (quickstart)= # Quickstart One happy path from zero to a working tool invocation. ## 1. Install Pick your MCP client and install method: ```{mcp-install} ``` See {ref}`clients` for dev-checkout setup, full config-file locations, and common pitfalls. ## 2. Verify Ask your LLM: ```{admonition} Prompt :class: prompt List all my tmux sessions and show me what's running in each pane. ``` The agent will call `list_sessions`, then `list_panes` and `capture_pane` to inspect your workspace. You should see your tmux sessions, windows, and pane contents in the response. ## 3. Try it Here are a few things to try: ```{admonition} Prompt :class: prompt Create a new tmux session called "workspace" with a window named "build". ``` ```{admonition} Prompt :class: prompt Send `make test` to the pane in my build window, then wait for it to finish and capture the output. ``` ```{admonition} Prompt :class: prompt Search all my panes for the word "error". ``` ## How it works When you say "run `make test` and show me the output", the agent executes a three-step pattern: 1. {tool}`send-keys` — send the command (composed with `tmux wait-for -S `) to a tmux pane 2. {tool}`wait-for-channel` — block deterministically until the command signals completion 3. {tool}`capture-pane` — read the terminal output This **send → wait → capture** sequence is the fundamental workflow. For commands the agent authors, the channel pattern is deterministic; for output the agent does not author (third-party log lines, daemon prompts, interactive supervisors), substitute {tool}`wait-for-text` for step 2. When you need to keep checking the same pane after that first read, switch to {tool}`capture-since`: the first call returns a cursor, and follow-up calls return only new pane output. ## Next steps - {ref}`concepts` — Understand the tmux hierarchy and how tools target panes - {ref}`configuration` — Environment variables and socket isolation - {ref}`safety` — Control which tools are available - {ref}`Tools ` — Browse all available tools --- # Recipes Source: https://libtmux-mcp.git-pull.com/recipes/ (recipes)= # Recipes Each recipe starts from a real workspace situation and traces the agent's reasoning through discovery, decision, and action. The goal is not to show tool-call sequences -- it is to show *how an agent decides* what to do with existing tmux state so you can write better prompts and system instructions. Every recipe uses the same structure: - **Situation** -- the developer's world before the agent acts - **Prompt** -- the natural-language sentence that triggers the recipe - **Discover** -- what the agent inspects and why - **Decide** -- the judgment call that changes the plan - **Act** -- the minimum safe action sequence - **The non-obvious part** -- the lesson you would miss from reading tool docs alone --- ## Find a running dev server and test against it **Situation.** A developer manages a React project with [tmuxp](https://tmuxp.git-pull.com). One pane is already running `pnpm start` with Vite somewhere in the `react` window. They want to run Playwright e2e tests. The agent does not know which pane has the server, or what port it chose. ```{admonition} Prompt :class: prompt Run the Playwright tests against my dev server in the myapp session. ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought {toolref}`list-panes` will not help here -- it shows metadata like current command and working directory, not terminal content. The dev server printed its URL to the terminal minutes ago, so I need to search terminal content. ``` The agent calls {tooliconl}`search-panes` with `pattern: "Local:"` and `session_name: "myapp"`. The response comes back with pane `%5` in the `react` window, matched line: `Local: http://localhost:5173/`. ### Decide ```{admonition} Agent reasoning :class: agent-thought The server is alive and its URL is known. I do not need to start anything. I just need an idle pane for running tests. ``` The agent calls {tooliconl}`list-panes` on the `myapp` session. Several panes show `pane_current_command: zsh` -- idle shells. It picks `%4` in the same window. ### Act The agent calls {tooliconl}`send-keys` in pane `%4`: `PLAYWRIGHT_BASE_URL=http://localhost:5173 pnpm exec playwright test` Then it calls {tooliconl}`wait-for-text` on pane `%4` with `pattern: "passed|failed|timed out"`, `regex: true`, and `timeout: 120`. Once the wait resolves, it calls {tooliconl}`capture-pane` on `%4` with `start: -80` to read the test results. ```{tip} The agent's first instinct might be to *start* a Vite server. But {tooliconl}`search-panes` reveals one is already running. This avoids a port conflict, a wasted pane, and the most common agent mistake: treating tmux like a blank shell. ``` ### The non-obvious part {toolref}`search-panes` searches terminal *content* -- what you would see on screen. {toolref}`list-panes` searches *metadata* like current command and working directory. If the agent had used {toolref}`list-panes` to find a pane running `node`, it would know a process exists but not whether it is ready or what URL it chose. --- ## Target the bottom-right pane for an ad-hoc command **Situation.** The developer keeps a four-pane tiled layout for the project: editor top-left, watcher top-right, logs bottom-left, scratch bottom-right. They want the agent to spin up a one-off dev server in the scratch pane without naming it explicitly. ```{admonition} Prompt :class: prompt Spin up the dev server in the bottom-right pane. ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought I do not need to list every pane and compute geometry. tmux already tracks each pane's edge predicates -- there is a tool that resolves a corner directly to a `PaneInfo`. ``` The agent calls {tooliconl}`find-pane-by-position` with `corner: "bottom-right"`. The response is a {class}`~libtmux_mcp.models.PaneInfo` carrying the pane's `pane_id` plus the new geometry block: `pane_at_bottom: true`, `pane_at_right: true`, `pane_left`, `pane_top`, etc. ### Decide ```{admonition} Agent reasoning :class: agent-thought I have the `pane_id`. From here on I target by ID, never by corner -- once the user resizes the layout, "bottom-right" might mean a different pane, but the `pane_id` of the pane I just identified stays stable. ``` ### Act The agent calls {tooliconl}`send-keys` in that pane: `pnpm start`. Then {tooliconl}`wait-for-text` with `pattern: "Local:"` and a generous `timeout` so Vite has room to start. ```{tip} "The bottom-right pane" is a *role* -- a layout-relative target the human reasons about. The `pane_id` returned by {toolref}`find-pane-by-position` is the *handle* the agent should use for every subsequent call. Do not call the corner-finder again on each follow-up; reuse the ID. ``` ### The non-obvious part Before {toolref}`find-pane-by-position`, the only way to resolve a corner was {toolref}`display-message` with `#{pane_at_bottom}` and `#{pane_at_right}` per pane, then parsing the string output. The structured `PaneInfo` response now carries `pane_left`, `pane_top`, `pane_right`, `pane_bottom` and the four `pane_at_*` predicates as typed fields, so agents reasoning about layout no longer need a parsing detour through {toolref}`display-message`. For single-pane windows, every corner resolves to the same pane (it touches every edge). For genuinely ambiguous layouts, the visually innermost pane wins via a `pane_left + pane_top` tie-break. --- ## Start a service and wait for it before running dependent work **Situation.** The developer is starting fresh in their `backend` session -- no server running yet. They want to run integration tests, but the test suite needs a live API server. ```{admonition} Prompt :class: prompt Start the API server in my backend session and run the integration tests once it's ready. ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought First I need to know what exists in the `backend` session. If a server is already running, I should reuse it instead of starting a duplicate. ``` The agent calls {tooliconl}`list-panes` for the `backend` session. No pane is running a server process. A {tooliconl}`search-panes` call for `"listening"` returns no matches. ### Decide ```{admonition} Agent reasoning :class: agent-thought Nothing to reuse. I need a dedicated pane for the server so its output stays separate from the test output. ``` ### Act The agent calls {tooliconl}`split-window` with `session_name: "backend"` to create a new pane, then calls {tooliconl}`send-keys` in that pane: `npm run serve`. The agent calls {tooliconl}`wait-for-text` on the server pane with `pattern: "Listening on"` and `timeout: 30`. Once the wait resolves, the agent calls {tooliconl}`send-keys` in the original pane: `npm test -- --integration`, then {tooliconl}`wait-for-text` with `pattern: "passed|failed|error"` and `regex: true`, then {tooliconl}`capture-pane` to read the test results. ```{warning} Calling {toolref}`capture-pane` immediately after {toolref}`send-keys` is a race condition. {toolref}`send-keys` returns the moment tmux accepts the keystrokes, not when the command finishes. For commands the agent authors, compose `tmux wait-for -S ` into the command and call {toolref}`wait-for-channel` — deterministic, race-free. For output the agent does not author (server-startup banners, test-result lines like the ones above), use {toolref}`wait-for-text` instead. ``` ### The non-obvious part {toolref}`wait-for-text` replaces `sleep`. The server might start in 2 seconds or 20 -- the agent adapts. The anti-pattern is polling with repeated {toolref}`capture-pane` calls or hardcoding a sleep duration. When the job is already running and the agent needs to keep observing it across turns, use {toolref}`capture-since` so each read returns only new pane output. --- ## Find the failing pane without opening random terminals **Situation.** The developer kicked off multiple jobs across panes in a `ci` session -- linting, unit tests, integration tests, type checking. One of them failed, but they stepped away and do not remember which pane. ```{admonition} Prompt :class: prompt Check my ci session -- which jobs failed? ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought I should not capture every pane and read them all -- that is expensive and slow. Instead I will search for common failure indicators across all panes at once. ``` The agent calls {tooliconl}`search-panes` with `pattern: "FAIL|ERROR|error:|Traceback"`, `regex: true`, scoped to `session_name: "ci"`. ### Decide ```{admonition} Agent reasoning :class: agent-thought Two panes matched: `%3` has `FAIL: test_upload` and `%6` has `error: Type 'string' is not assignable`. I will capture context from each. ``` ### Act The agent calls {tooliconl}`capture-pane` on `%3` with `start: -60`, then on `%6` with `start: -60`. ```{tip} If the error scrolled off the visible screen, use `content_start: -200` (or deeper) when calling {tooliconl}`search-panes`. The `content_start` parameter makes search reach into scrollback history, not just the visible screen. ``` ### The non-obvious part {toolref}`search-panes` checks all panes in a single call -- searching 20 panes costs roughly the same as searching 2. An agent that instead calls {toolref}`list-panes` then {toolref}`capture-pane` on each one individually makes 20+ round trips for the same information. The `regex: true` parameter is required here because the `|` in the pattern is a regex alternation, not literal text. --- ## Interrupt a stuck process and recover the pane **Situation.** A long-running build is hanging. The developer wants to interrupt it, verify the pane is responsive, and re-run the command. ```{admonition} Prompt :class: prompt The build in pane %2 is stuck. Kill it and restart. ``` Or with less specificity — the agent will discover the target: ```{admonition} Prompt :class: prompt The build in one of my panes is stuck. Kill it and restart. ``` Or if you've built muscle memory in your chats: ```{admonition} Prompt :class: prompt The build is stuck. Kill it and restart. ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought I need to send Ctrl-C. This is a tmux key name, not text -- so I must use `enter: false` or tmux will send Ctrl-C followed by Enter, which could confirm a prompt I did not intend to answer. ``` The agent calls {tooliconl}`send-keys` with `keys: "C-c"` and `enter: false` on the target pane. ### Decide ```{admonition} Agent reasoning :class: agent-thought Did the interrupt work? Some processes ignore {term}`SIGINT`. I will wait briefly for a shell prompt to reappear. Developers use custom prompts, so I cannot just look for `$`. ``` The agent calls {tooliconl}`wait-for-text` with `pattern: "[$#>%] *$"`, `regex: true`, and `timeout: 5`. ```{admonition} Agent reasoning :class: agent-thought If the wait resolves, the shell is back. If it times out, the process ignored Ctrl-C. I will escalate: try {term}`SIGQUIT` (`C-\` with `enter: false`), then destroy and replace the pane only as a last resort. ``` ### Act If the wait times out, the agent sends `C-\` (also with `enter: false`). If that also fails, it calls {tooliconl}`kill-pane` on the stuck pane, then {tooliconl}`split-window` to create a replacement, then {tooliconl}`send-keys` to re-run. ```{warning} The `enter: false` parameter is critical. Without it, {toolref}`send-keys` sends Ctrl-C *then* Enter, which could confirm a "really quit?" prompt, submit a partially typed command, or enter a newline into a REPL. ``` ### The non-obvious part Recovery is a two-step decision. Try {term}`SIGINT` first (Ctrl-C), verify it worked with {toolref}`wait-for-text`, escalate to {term}`SIGQUIT` only if needed. The escalation ladder is: interrupt, verify, escalate signal, destroy. Skipping straight to {toolref}`kill-pane` loses the pane's scrollback history and any partially written output that might explain *why* it hung. --- ## Re-run a command without mixing old and new output **Situation.** The developer wants `pytest` re-run in tmux, but the candidate pane already has old test output in scrollback. They want only fresh results. ```{admonition} Prompt :class: prompt Run `pytest` in the test pane and show me only the fresh output. ``` ### Discover The agent calls {tooliconl}`list-panes` to find the pane by title, cwd, or current command. If more than one pane is plausible, it uses {tooliconl}`capture-pane` with a small range to confirm the target. ### Decide ```{admonition} Agent reasoning :class: agent-thought The pane is a shell. I should clear it before running so the capture afterwards contains only fresh output. If it were running a watcher or long-lived process, I would not hijack it -- I would use a different pane. ``` ### Act The agent calls {tooliconl}`clear-pane`, then {tooliconl}`send-keys` with `keys: "pytest; tmux wait-for -S pytest_done"`, then {tooliconl}`wait-for-channel` with `channel: "pytest_done"`, then {tooliconl}`capture-pane` to read the fresh output. Composing the `tmux wait-for -S` signal directly into the shell command is the deterministic path for authored commands. ### The non-obvious part {toolref}`clear-pane` runs two tmux commands internally (`send-keys -R` then `clear-history`) with a brief gap between them. Calling {toolref}`capture-pane` immediately after {toolref}`clear-pane` may catch partial state. The {toolref}`wait-for-text` call after {toolref}`send-keys` naturally provides the needed delay, so the sequence clear-send-wait-capture is safe. --- ## Build a workspace the agent can revisit later **Situation.** The developer wants a durable project workspace -- not just a quick split, but a layout that later prompts can refer to by role ("the server pane", "the test pane"). ```{admonition} Prompt :class: prompt Set up a tmux workspace for myproject with editor, server, and test panes. ``` ### Discover ```{admonition} Agent reasoning :class: agent-thought Before creating anything, I need to check whether a session with this name already exists. Creating a duplicate will fail. ``` The agent calls {tooliconl}`list-sessions`. No session named `myproject` exists. ### Decide ```{admonition} Agent reasoning :class: agent-thought Safe to create. I need three panes: editor, server, tests. I will create the session, split twice, then apply a layout so tmux handles the geometry instead of me calculating sizes. ``` ### Act The agent calls {tooliconl}`create-session` with `session_name: "myproject"` and `start_directory: "/home/dev/myproject"`. Then {tooliconl}`split-window` twice (with `direction: "right"` and `direction: "below"`), followed by {tooliconl}`select-layout` with `layout: "main-vertical"`. The agent calls {tooliconl}`set-pane-title` on each pane: `editor`, `server`, `tests`. The agent calls {tooliconl}`send-keys` in the server pane: `npm run dev`, then {tooliconl}`wait-for-text` for `pattern: "ready|listening|Local:"` with `regex: true` and `timeout: 30`. ```{tip} If the session *does* already exist, the right move is to reuse and extend it, not recreate it. The {toolref}`list-sessions` check at the top is what makes that decision possible. ``` ### The non-obvious part Titles and naming are not cosmetic. They reduce future discovery cost. When the agent comes back in a later conversation and the user says "restart the server," the agent calls {toolref}`list-panes`, finds the pane titled `server`, and acts -- no searching, no guessing, no capturing every pane to figure out which one is which. But note: pane IDs are ephemeral across tmux server restarts, so the agent should always re-discover by metadata (session name, pane title, cwd) rather than trusting remembered `%N` values. --- ## What to read next For the principles that recur across these recipes -- discover before acting, wait instead of polling, content vs. metadata, prefer IDs, escalate gracefully -- see the {ref}`prompting guide `. For specific pitfalls like `enter: false` and the `send_keys`/`capture_pane` race condition, see {ref}`gotchas `. --- # API Reference Source: https://libtmux-mcp.git-pull.com/reference/api/ (api)= # API Reference ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Server :link: server :link-type: doc MCP server entry point and lifecycle. ::: :::{grid-item-card} Tools :link: tools :link-type: doc Tool function implementations. ::: :::{grid-item-card} Models :link: models :link-type: doc Pydantic models for requests and responses. ::: :::{grid-item-card} Middleware :link: middleware :link-type: doc Safety-tier enforcement and request hooks. ::: :::{grid-item-card} Utils :link: utils :link-type: doc Shared helpers and utilities. ::: :::: ```{toctree} :hidden: server tools models middleware utils ``` --- # Middleware Source: https://libtmux-mcp.git-pull.com/reference/api/middleware/ # Middleware ```{eval-rst} .. automodule:: libtmux_mcp.middleware :members: :undoc-members: :show-inheritance: ``` --- # Models Source: https://libtmux-mcp.git-pull.com/reference/api/models/ # Models ```{eval-rst} .. automodule:: libtmux_mcp.models :members: :undoc-members: :show-inheritance: ``` --- # Server Source: https://libtmux-mcp.git-pull.com/reference/api/server/ # Server ```{eval-rst} .. automodule:: libtmux_mcp.server :members: :undoc-members: :show-inheritance: ``` --- # Tools Source: https://libtmux-mcp.git-pull.com/reference/api/tools/ # Tools ## Server tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.server_tools :members: :undoc-members: :show-inheritance: ``` ## Session tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.session_tools :members: :undoc-members: :show-inheritance: ``` ## Window tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.window_tools :members: :undoc-members: :show-inheritance: ``` ## Pane tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.pane_tools :members: :undoc-members: :show-inheritance: ``` ## Option tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.option_tools :members: :undoc-members: :show-inheritance: ``` ## Environment tools ```{eval-rst} .. automodule:: libtmux_mcp.tools.env_tools :members: :undoc-members: :show-inheritance: ``` --- # Utilities Source: https://libtmux-mcp.git-pull.com/reference/api/utils/ # Utilities ```{eval-rst} .. automodule:: libtmux_mcp._utils :members: :undoc-members: :show-inheritance: ``` --- # Compatibility Source: https://libtmux-mcp.git-pull.com/reference/compatibility/ (compatibility)= # Compatibility ## Python versions | Python | Status | |--------|--------| | 3.10 | Supported | | 3.11 | Supported | | 3.12 | Supported | | 3.13 | Supported | | 3.14 | Supported | | PyPy | Supported | ## tmux versions | tmux | Status | |------|--------| | >= 3.2a | Supported | | < 3.2a | Not supported (libtmux requirement) | ## Dependencies | Package | Required version | |---------|-----------------| | [libtmux](https://libtmux.git-pull.com/) | >= 0.55.0, < 1.0 | | [FastMCP](https://github.com/jlowin/fastmcp) | >= 3.1.0, < 4.0.0 | ## MCP clients | Client | Tested | Transport | |--------|--------|-----------| | Claude Code | Yes | stdio | | Claude Desktop | Yes | stdio | | Codex CLI | Yes | stdio | | Gemini CLI | Yes | stdio | | Cursor | Yes | stdio | | MCP Inspector | Yes | stdio | ## OS support | Platform | Status | |----------|--------| | Linux | Supported | | macOS | Supported | | WSL2 | Supported | | Windows (native) | Not supported (tmux requires Unix) | --- # Resources Source: https://libtmux-mcp.git-pull.com/resources/ (resources-overview)= # Resources MCP resources are addressable documents the server exposes at ``tmux://`` URIs. Clients read them via ``resources/read``. All libtmux-mcp resources are [resource templates](https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-templates) — each URI includes a ``{?socket_name}`` query parameter for socket isolation, plus structural path parameters (``{session_name}``, ``{pane_id}``, …) so a single template covers every session, window, or pane. Every resource delivers a snapshot of the tmux hierarchy at call time. Agents use them for read-only inspection; any write workflow goes through the corresponding {doc}`tools `. ## Available resources ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} `get_sessions` :link: fastmcp-resource-template-get-sessions :link-type: ref List all tmux sessions. ::: :::{grid-item-card} `get_session` :link: fastmcp-resource-template-get-session :link-type: ref Get details of a specific tmux session. ::: :::{grid-item-card} `get_session_windows` :link: fastmcp-resource-template-get-session-windows :link-type: ref List all windows in a tmux session. ::: :::{grid-item-card} `get_window` :link: fastmcp-resource-template-get-window :link-type: ref Get details of a specific window in a session. ::: :::{grid-item-card} `get_pane` :link: fastmcp-resource-template-get-pane :link-type: ref Get details of a specific pane. ::: :::{grid-item-card} `get_pane_content` :link: fastmcp-resource-template-get-pane-content :link-type: ref Capture and return the content of a pane. ::: :::: --- ## Sessions ```{fastmcp-resource-template} get_sessions ``` ```{fastmcp-resource-template} get_session ``` ```{fastmcp-resource-template} get_session_windows ``` ## Windows ```{fastmcp-resource-template} get_window ``` ## Panes ```{fastmcp-resource-template} get_pane ``` ```{fastmcp-resource-template} get_pane_content ``` --- # Delete buffer Source: https://libtmux-mcp.git-pull.com/tools/buffer/delete-buffer/ # Delete buffer ```{fastmcp-tool} buffer_tools.delete_buffer ``` **Use when** you're done with a buffer and want to free server-side state. Always call this when the buffer's purpose is complete — tmux servers outlive the MCP process, so leaked buffers persist across MCP restarts. **Side effects:** Removes the named buffer from the tmux server. Subsequent {tooliconl}`paste-buffer` calls against the deleted name return an {exc}`~libtmux_mcp._utils.ExpectedToolError`. ```{fastmcp-tool-input} buffer_tools.delete_buffer ``` --- # Buffer tools Source: https://libtmux-mcp.git-pull.com/tools/buffer/ # Buffer tools tmux paste buffers are a server-global namespace shared by every client on the same socket. The buffer tools in libtmux-mcp expose a narrow, agent-namespaced subset: every allocation gets a UUID-scoped name like `libtmux_mcp_<32-hex>_`, so concurrent agents (or parallel tool calls from one agent) cannot collide on each other's payloads. There is **no** `list_buffers` tool. The user's OS clipboard often syncs into tmux paste buffers, so a generic enumeration would leak passwords, tokens, and other private content the agent has no business reading. Callers track the buffers they own via the {tool}`load-buffer` returns. ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`load-buffer` Stage content into a new tmux paste buffer. ::: :::{grid-item-card} {tooliconl}`paste-buffer` Push a staged buffer into a pane. ::: :::{grid-item-card} {tooliconl}`show-buffer` Read a staged buffer's contents back. ::: :::{grid-item-card} {tooliconl}`delete-buffer` Free the server-side state of a staged buffer. ::: :::: ```{toctree} :hidden: :maxdepth: 1 load-buffer paste-buffer show-buffer delete-buffer ``` --- # Load buffer Source: https://libtmux-mcp.git-pull.com/tools/buffer/load-buffer/ # Load buffer ```{fastmcp-tool} buffer_tools.load_buffer ``` **Use when** you need to stage multi-line text for paste — sending a shell script, prepared input for an interactive prompt, or content that's too long for a clean {tooliconl}`send-keys` invocation. **Avoid when** the text is a single command line that {tooliconl}`send-keys` can deliver directly. `load_buffer` allocates server-side state that must be cleaned up via {tooliconl}`delete-buffer`. **Side effects:** Allocates a tmux paste buffer. Use the returned `buffer_name` for follow-up calls. The `content` argument is redacted in audit logs. **Example:** ```json { "tool": "load_buffer", "arguments": { "content": "for i in 1 2 3; do\n echo line $i\ndone\n", "logical_name": "loop" } } ``` Response: ```json { "buffer_name": "libtmux_mcp_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6_loop", "logical_name": "loop" } ``` ```{fastmcp-tool-input} buffer_tools.load_buffer ``` --- ## Paste --- # Paste buffer Source: https://libtmux-mcp.git-pull.com/tools/buffer/paste-buffer/ # Paste buffer ```{fastmcp-tool} buffer_tools.paste_buffer ``` **Use when** you've staged content via {tooliconl}`load-buffer` and need to push it into a target pane. Use bracketed paste mode (default `bracket=true`) for terminals that handle it; the wrapping escape sequences signal "this is pasted text, not typed input". **Avoid when** the buffer name doesn't match the MCP namespace — `paste_buffer` refuses non-`libtmux_mcp_*` names so it cannot be turned into an arbitrary-buffer paster. **Side effects:** Pastes content into the target pane (the pane's shell receives the bytes as input). Open-world: the resulting shell behavior is whatever the pasted bytes invoke. ```{fastmcp-tool-input} buffer_tools.paste_buffer ``` --- ## Inspect --- # Show buffer Source: https://libtmux-mcp.git-pull.com/tools/buffer/show-buffer/ # Show buffer ```{fastmcp-tool} buffer_tools.show_buffer ``` **Use when** you need to confirm what was staged before pasting, or to read back a buffer between modifications. Restricted to MCP-namespaced buffers — non-agent buffers are rejected. **Side effects:** None. Readonly. ```{fastmcp-tool-input} buffer_tools.show_buffer ``` --- ## Clean up --- # Hook tools Source: https://libtmux-mcp.git-pull.com/tools/hook/ # Hook tools tmux hooks let you attach commands to lifecycle events — `pane-exited`, `session-renamed`, `command-error`, and so on. libtmux-mcp exposes **read-only** hook introspection so agents can audit what hooks the human user has configured before running automation that might trigger them. ## Why no `set_hook`? Write-hooks are deliberately not exposed. tmux servers outlive the MCP process, and FastMCP's `lifespan` teardown runs only on graceful SIGTERM/SIGINT — it's bypassed on `kill -9`, OOM-kill, and C-extension-fault crashes. Any cleanup registry in Python could be silently bypassed, leaking agent-installed shell hooks into the user's persistent tmux server where they would fire forever. Three plausible future paths exist (a tmux-side `client-detached` meta-hook for self-cleanup, requiring `LIBTMUX_SAFETY=destructive`, or exposing one-shot `run_hook` only); none is in scope. Until one of those paths is implemented, the surface here is visibility only. ## Inspect ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`show-hooks` Enumerate bindings at a scope. ::: :::{grid-item-card} {tooliconl}`show-hook` Inspect a single binding. ::: :::: ```{toctree} :hidden: :maxdepth: 1 show-hooks show-hook ``` --- # Show hook Source: https://libtmux-mcp.git-pull.com/tools/hook/show-hook/ # Show hook ```{fastmcp-tool} hook_tools.show_hook ``` **Use when** you know which hook you want to inspect by name. Returns empty when the hook is unset; raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` for unknown hook names (typos, wrong scope) so input mistakes don't masquerade as "nothing configured". **Side effects:** None. Readonly. ```{fastmcp-tool-input} hook_tools.show_hook ``` --- # Show hooks Source: https://libtmux-mcp.git-pull.com/tools/hook/show-hooks/ # Show hooks ```{fastmcp-tool} hook_tools.show_hooks ``` **Use when** you need to enumerate every hook configured on a target — the human user's tmux config, an inherited team setup, or a session that another tool may have touched. **Side effects:** None. Readonly. ```{fastmcp-tool-input} hook_tools.show_hooks ``` --- # Tools Source: https://libtmux-mcp.git-pull.com/tools/ (tools-overview)= # Tools All tools accept an optional `socket_name` parameter for multi-server support. It defaults to the {envvar}`LIBTMUX_SOCKET` env var. See {ref}`configuration`. ## Which tool do I want? **Reading terminal content?** - Re-reading the same pane while it changes? → {tool}`capture-since` - Need a one-shot read of a known pane? → {tool}`capture-pane` - Need text + cursor + mode in one call? → {tool}`snapshot-pane` - Don't know which pane? → {tool}`search-panes` - Need to wait for specific output? → {tool}`wait-for-text` - Need to wait for *any* change? → {tool}`wait-for-content-change` - Only need metadata (PID, path, size)? → {tool}`get-pane-info` - Need an arbitrary tmux variable? → {tool}`display-message` **Targeting a pane by layout?** - "The bottom-right pane", "top-left", any corner → {tool}`find-pane-by-position` - Already know the `pane_id` → use it directly **Running a command?** - {tool}`send-keys` (with `tmux wait-for -S ` 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` **Creating workspace structure?** - New session → {tool}`create-session` - New window → {tool}`create-window` - New pane → {tool}`split-window` **Navigating?** - Switch pane → {tool}`select-pane` (by ID or direction) - Switch window → {tool}`select-window` (by ID, index, or direction) **Rearranging layout?** - Swap two panes → {tool}`swap-pane` - Move window → {tool}`move-window` - Change layout → {tool}`select-layout` **Scrollback / copy mode?** - Enter copy mode → {tool}`enter-copy-mode` - Exit copy mode → {tool}`exit-copy-mode` - Log output to file → {tool}`pipe-pane` **Coordinating across panes?** - Block until signalled → {tool}`wait-for-channel` - Signal a waiter → {tool}`signal-channel` **Staging multi-line input?** - Stage content → {tool}`load-buffer` - Push into pane → {tool}`paste-buffer` - Read back → {tool}`show-buffer` - Free server state → {tool}`delete-buffer` **Auditing tmux hooks?** - Enumerate → {tool}`show-hooks` - Inspect one → {tool}`show-hook` **Changing settings?** - tmux options → {tool}`show-option` / {tool}`set-option` - Environment vars → {tool}`show-environment` / {tool}`set-environment` **Reaching for a workflow recipe?** The server also ships four short MCP prompts the client renders for the model — see {doc}`/prompts`. They cover the most common patterns (run-and-wait, diagnose-failing-pane, build-dev-workspace, interrupt-gracefully) with embedded UUID-scoped wait channels and shell-agnostic guidance. ## Inspect Read tmux state without changing anything. ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} list_sessions :link: list-sessions :link-type: ref List all active sessions. ::: :::{grid-item-card} list_windows :link: list-windows :link-type: ref List windows in a session. ::: :::{grid-item-card} list_panes :link: list-panes :link-type: ref List panes in a window. ::: :::{grid-item-card} capture_pane :link: capture-pane :link-type: ref Read visible content of a pane. ::: :::{grid-item-card} capture_since :link: capture-since :link-type: ref Start a cursor, then read only new pane output. ::: :::{grid-item-card} get_pane_info :link: get-pane-info :link-type: ref Get detailed pane metadata. ::: :::{grid-item-card} find_pane_by_position :link: find-pane-by-position :link-type: ref Resolve "the bottom-right pane" (or any corner) to a `PaneInfo`. ::: :::{grid-item-card} search_panes :link: search-panes :link-type: ref Search text across panes. ::: :::{grid-item-card} wait_for_text :link: wait-for-text :link-type: ref Wait for text to appear in a pane. ::: :::{grid-item-card} get_server_info :link: get-server-info :link-type: ref Get tmux server info. ::: :::{grid-item-card} list_servers :link: list-servers :link-type: ref Discover live tmux servers under `$TMUX_TMPDIR`. ::: :::{grid-item-card} show_option :link: show-option :link-type: ref Query a tmux option value. ::: :::{grid-item-card} show_environment :link: show-environment :link-type: ref Show tmux environment variables. ::: :::{grid-item-card} snapshot_pane :link: snapshot-pane :link-type: ref Rich capture: content + cursor + mode + scroll. ::: :::{grid-item-card} wait_for_content_change :link: wait-for-content-change :link-type: ref Wait for any screen change. ::: :::{grid-item-card} display_message :link: display-message :link-type: ref Query arbitrary tmux format strings. ::: :::{grid-item-card} show_buffer :link: show-buffer :link-type: ref Read back an MCP-staged paste buffer. ::: :::{grid-item-card} show_hooks :link: show-hooks :link-type: ref Enumerate configured tmux hooks at a scope. ::: :::{grid-item-card} show_hook :link: show-hook :link-type: ref Inspect a single tmux hook by name. ::: :::: ## Act Create or modify tmux objects. ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} create_session :link: create-session :link-type: ref Start a new tmux session. ::: :::{grid-item-card} create_window :link: create-window :link-type: ref Add a window to a session. ::: :::{grid-item-card} split_window :link: split-window :link-type: ref Split a window into panes. ::: :::{grid-item-card} send_keys :link: send-keys :link-type: ref Send commands or keystrokes to a pane. ::: :::{grid-item-card} rename_session :link: rename-session :link-type: ref Rename a session. ::: :::{grid-item-card} rename_window :link: rename-window :link-type: ref Rename a window. ::: :::{grid-item-card} resize_pane :link: resize-pane :link-type: ref Adjust pane dimensions. ::: :::{grid-item-card} resize_window :link: resize-window :link-type: ref Adjust window dimensions. ::: :::{grid-item-card} select_layout :link: select-layout :link-type: ref Set window layout. ::: :::{grid-item-card} set_pane_title :link: set-pane-title :link-type: ref Set pane title. ::: :::{grid-item-card} clear_pane :link: clear-pane :link-type: ref Clear pane content. ::: :::{grid-item-card} set_option :link: set-option :link-type: ref Set a tmux option. ::: :::{grid-item-card} set_environment :link: set-environment :link-type: ref Set a tmux environment variable. ::: :::{grid-item-card} select_pane :link: select-pane :link-type: ref Focus a pane by ID or direction. ::: :::{grid-item-card} select_window :link: select-window :link-type: ref Focus a window by ID, index, or direction. ::: :::{grid-item-card} swap_pane :link: swap-pane :link-type: ref Exchange positions of two panes. ::: :::{grid-item-card} move_window :link: move-window :link-type: ref Move window to another index or session. ::: :::{grid-item-card} pipe_pane :link: pipe-pane :link-type: ref Stream pane output to a file. ::: :::{grid-item-card} enter_copy_mode :link: enter-copy-mode :link-type: ref Enter copy mode for scrollback. ::: :::{grid-item-card} exit_copy_mode :link: exit-copy-mode :link-type: ref Exit copy mode. ::: :::{grid-item-card} paste_text :link: paste-text :link-type: ref Paste multi-line text via tmux buffer. ::: :::{grid-item-card} load_buffer :link: load-buffer :link-type: ref Stage multi-line text into an MCP-namespaced tmux buffer. ::: :::{grid-item-card} paste_buffer :link: paste-buffer :link-type: ref Paste an MCP buffer into a target pane. ::: :::{grid-item-card} wait_for_channel :link: wait-for-channel :link-type: ref Block until a tmux ``wait-for`` channel is signalled. ::: :::{grid-item-card} signal_channel :link: signal-channel :link-type: ref Wake clients blocked on a ``wait-for`` channel. ::: :::: ## Destroy Tear down tmux objects. Not reversible. ::::{grid} 1 2 3 3 :gutter: 2 2 3 3 :::{grid-item-card} kill_session :link: kill-session :link-type: ref Destroy a session and all its windows. ::: :::{grid-item-card} kill_window :link: kill-window :link-type: ref Destroy a window and all its panes. ::: :::{grid-item-card} kill_pane :link: kill-pane :link-type: ref Destroy a pane. ::: :::{grid-item-card} kill_server :link: kill-server :link-type: ref Kill the entire tmux server. ::: :::{grid-item-card} delete_buffer :link: delete-buffer :link-type: ref Delete an MCP-staged tmux paste buffer. ::: :::: ```{toctree} :hidden: :caption: Tools by tmux scope server/index session/index window/index pane/index buffer/index hook/index ``` --- # Capture pane Source: https://libtmux-mcp.git-pull.com/tools/pane/capture-pane/ # Capture pane ```{fastmcp-tool} pane_tools.capture_pane ``` **Use when** you need to read what's currently displayed in a terminal — after running a command, checking output, or verifying state. **Avoid when** you need to search across multiple panes at once — use {tooliconl}`search-panes`. If you are repeatedly watching the same pane, use {tooliconl}`capture-since` with its cursor so unchanged scrollback is not sent again. If you only need pane metadata (not content), use {tooliconl}`get-pane-info`. **Side effects:** None. Readonly. **Example:** ```json { "tool": "capture_pane", "arguments": { "pane_id": "%0", "start": -50 } } ``` Response (string): ```text $ echo "Running tests..." Running tests... $ echo "PASS: test_auth (0.3s)" PASS: test_auth (0.3s) $ echo "FAIL: test_upload (AssertionError)" FAIL: test_upload (AssertionError) $ echo "3 tests: 2 passed, 1 failed" 3 tests: 2 passed, 1 failed $ ``` ```{fastmcp-tool-input} pane_tools.capture_pane ``` --- # Capture since Source: https://libtmux-mcp.git-pull.com/tools/pane/capture-since/ # Capture since ```{fastmcp-tool} pane_tools.capture_since ``` **Use when** you need to observe the same pane repeatedly — tailing logs, watching a long-running command, checking a daemon, or revisiting a terminal without paying to re-read the same scrollback every turn. The first call returns the current visible screen plus a cursor; later calls pass that cursor back and receive only rows written or rewritten after it. **Avoid when** you control the command and only need completion — compose `tmux wait-for -S ` into the command and call {tooliconl}`wait-for-channel`. If you need a one-shot content + metadata view, use {tooliconl}`snapshot-pane`; if you do not know which pane contains text, use {tooliconl}`search-panes`. **Side effects:** None. Readonly. **Example:** Start a cursor with the currently visible screen: ```json { "tool": "capture_since", "arguments": { "pane_id": "%2" } } ``` Response: ```json { "pane_id": "%2", "cursor": "capture-since-v1:...", "lines": [ "$ pytest -vv", "tests/test_api.py::test_health PASSED" ], "elapsed_seconds": 0.003, "lines_missed": false, "truncated": false, "truncated_lines": 0, "truncated_bytes": 0 } ``` Read only content since that cursor: ```json { "tool": "capture_since", "arguments": { "cursor": "capture-since-v1:..." } } ``` The cursor carries the original pane id, so the follow-up call does not need `pane_id`. If you pass both, they must match; a cursor for another pane raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of silently reading the wrong process. If nothing new was written after the cursor, `lines` is empty and the response still includes a fresh cursor for the same pane. If the cursor row scrolled into retained history, the tool can still return an exact delta; retained scrollback is not a loss condition. `lines_missed` becomes `true` when tmux has cleared or trimmed the history needed to compute an exact delta. In that case, `lines` is a conservative current visible capture and the response includes a fresh cursor. Pane lifecycle is part of the cursor contract. If the pane dies or is respawned, the call raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of reading from a different process that reused the same pane id. `truncated`, `truncated_lines`, and `truncated_bytes` are structured metadata. No truncation marker is injected into `lines`, so clients can display terminal text without parsing an in-band header. The cursor is intentionally opaque. It is based on tmux grid state (`history_size + cursor_y`) and pane lifecycle fields (`pane_id`, `pane_pid`); see tmux's grid and capture implementation in [grid.c](https://github.com/tmux/tmux/blob/134ba6c/grid.c) and [cmd-capture-pane.c](https://github.com/tmux/tmux/blob/134ba6c/cmd-capture-pane.c), and libtmux's [`Pane.capture_pane()`](https://github.com/tmux-python/libtmux/blob/v0.58.0/src/libtmux/pane.py). ```{fastmcp-tool-input} pane_tools.capture_since ``` --- # Clear pane Source: https://libtmux-mcp.git-pull.com/tools/pane/clear-pane/ # Clear pane ```{fastmcp-tool} pane_tools.clear_pane ``` **Use when** you want a clean terminal before capturing output. **Side effects:** Clears the pane's visible content. **Example:** ```json { "tool": "clear_pane", "arguments": { "pane_id": "%0" } } ``` Response (string): ```text Pane cleared: %0 ``` ```{fastmcp-tool-input} pane_tools.clear_pane ``` --- # Evaluate tmux format string (display_message) Source: https://libtmux-mcp.git-pull.com/tools/pane/display-message/ # Evaluate tmux format string (display_message) ```{fastmcp-tool} pane_tools.display_message ``` **Use when** you need to query arbitrary tmux variables — zoom state, pane dead flag, client activity, or any `#{format}` string that isn't covered by other tools. Despite the historical name (`display_message` is the tmux verb it wraps), this tool does **not** display anything to the user; it expands the format string with `display-message -p` and returns the value. **Avoid when** a dedicated tool already provides the information — e.g. use {tooliconl}`snapshot-pane` for cursor position and mode, {tooliconl}`get-pane-info` for standard metadata (including the new `pane_left` / `pane_top` / `pane_at_*` geometry block), or {tooliconl}`find-pane-by-position` to resolve a window corner to a `PaneInfo` without parsing `#{pane_at_bottom}` / `#{pane_at_right}` yourself. **Side effects:** None. Readonly. **Example:** ```json { "tool": "display_message", "arguments": { "format_string": "zoomed=#{window_zoomed_flag} dead=#{pane_dead}", "pane_id": "%0" } } ``` Response (string): ```text zoomed=0 dead=0 ``` ```{fastmcp-tool-input} pane_tools.display_message ``` --- # Enter copy mode Source: https://libtmux-mcp.git-pull.com/tools/pane/enter-copy-mode/ # Enter copy mode ```{fastmcp-tool} pane_tools.enter_copy_mode ``` **Use when** you need to scroll through scrollback history in a pane. Optionally scroll up immediately after entering. Use {tooliconl}`snapshot-pane` afterward to read the `scroll_position` and visible content. **Side effects:** Puts the pane into copy mode. The pane stops receiving new output until you exit copy mode. **Example:** ```json { "tool": "enter_copy_mode", "arguments": { "pane_id": "%0", "scroll_up": 50 } } ``` Response: ```json { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "24", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.enter_copy_mode ``` --- # Exit copy mode Source: https://libtmux-mcp.git-pull.com/tools/pane/exit-copy-mode/ # Exit copy mode ```{fastmcp-tool} pane_tools.exit_copy_mode ``` **Use when** you're done scrolling through scrollback and want the pane to resume receiving output. **Side effects:** Exits copy mode, returning the pane to normal. **Example:** ```json { "tool": "exit_copy_mode", "arguments": { "pane_id": "%0" } } ``` Response: ```json { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "24", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.exit_copy_mode ``` --- # Find pane by position Source: https://libtmux-mcp.git-pull.com/tools/pane/find-pane-by-position/ # Find pane by position ```{fastmcp-tool} pane_tools.find_pane_by_position ``` **Use when** you need to act on a layout-relative pane — "the bottom-right pane", "whichever pane is in the top-left" — without listing every pane and computing geometry yourself. **Avoid when** you already know the `pane_id`. Use {tooliconl}`get-pane-info` or {tooliconl}`select-pane` directly. **Side effects:** None. Read-only. **Example:** ```json { "tool": "find_pane_by_position", "arguments": { "corner": "bottom-right", "window_id": "@0" } } ``` Response is a {class}`~libtmux_mcp.models.PaneInfo` for the pane occupying that corner. The new geometry fields make the result self-describing: ```json { "pane_id": "%3", "pane_left": 40, "pane_top": 12, "pane_right": 79, "pane_bottom": 23, "pane_at_left": false, "pane_at_right": true, "pane_at_top": false, "pane_at_bottom": true, "pane_tty": "/dev/pts/5" } ``` **Tie-break.** When multiple panes satisfy both edge predicates (a single-pane window touches every edge; some custom layouts can produce ambiguous corners) the visually innermost pane wins — the one with the largest `pane_left + pane_top`. ```{fastmcp-tool-input} pane_tools.find_pane_by_position ``` --- # Get pane info Source: https://libtmux-mcp.git-pull.com/tools/pane/get-pane-info/ # Get pane info ```{fastmcp-tool} pane_tools.get_pane_info ``` **Use when** you need pane dimensions, PID, current working directory, or other metadata without reading the terminal content. **Avoid when** you need the actual text — use {tooliconl}`capture-pane`. **Side effects:** None. Readonly. **Example:** ```json { "tool": "get_pane_info", "arguments": { "pane_id": "%0" } } ``` Response: ```json { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "24", "pane_left": 0, "pane_top": 0, "pane_right": 79, "pane_bottom": 23, "pane_at_left": true, "pane_at_right": true, "pane_at_top": true, "pane_at_bottom": true, "pane_tty": "/dev/pts/5", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` Coordinates are window-relative cell offsets. `pane_left` / `pane_top` are 0-based and `pane_right` / `pane_bottom` are inclusive. The four `pane_at_*` predicates account for `pane-border-status`. To target a layout-relative pane (e.g. "the bottom-right pane") use {tooliconl}`find-pane-by-position` instead of computing edges yourself. ```{fastmcp-tool-input} pane_tools.get_pane_info ``` --- # Pane tools Source: https://libtmux-mcp.git-pull.com/tools/pane/ # Pane tools Pane-scoped tools — read and drive individual terminals, wait for output, copy mode, and channel sync. ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`capture-pane` Read visible or scrollback text from a pane. ::: :::{grid-item-card} {tooliconl}`capture-since` Start a cursor, then read only new pane output. ::: :::{grid-item-card} {tooliconl}`search-panes` Search text across many panes in one call. ::: :::{grid-item-card} {tooliconl}`snapshot-pane` Capture content plus cursor, mode, and scroll state in one call. ::: :::{grid-item-card} {tooliconl}`get-pane-info` Read pane metadata without content. ::: :::{grid-item-card} {tooliconl}`find-pane-by-position` Find the pane at a given corner of a window. ::: :::{grid-item-card} {tooliconl}`display-message` Evaluate a tmux format string against a target. ::: :::{grid-item-card} {tooliconl}`send-keys` Send keystrokes or commands to a pane. ::: :::{grid-item-card} {tooliconl}`paste-text` Paste multi-line text via tmux buffer. ::: :::{grid-item-card} {tooliconl}`pipe-pane` Fork pane output to a file or program. ::: :::{grid-item-card} {tooliconl}`select-pane` Switch focus to a pane. ::: :::{grid-item-card} {tooliconl}`swap-pane` Swap two panes' positions. ::: :::{grid-item-card} {tooliconl}`set-pane-title` Set a pane's human-readable title. ::: :::{grid-item-card} {tooliconl}`clear-pane` Clear a pane's scrollback. ::: :::{grid-item-card} {tooliconl}`resize-pane` Resize a pane. ::: :::{grid-item-card} {tooliconl}`enter-copy-mode` Enter tmux copy mode for scrollback navigation. ::: :::{grid-item-card} {tooliconl}`exit-copy-mode` Exit copy mode. ::: :::{grid-item-card} {tooliconl}`wait-for-text` Block until a pattern appears in a pane. ::: :::{grid-item-card} {tooliconl}`wait-for-content-change` Block until pane content changes. ::: :::{grid-item-card} {tooliconl}`wait-for-channel` Block until a tmux wait-for channel is signalled. ::: :::{grid-item-card} {tooliconl}`signal-channel` Signal a waiting channel. ::: :::{grid-item-card} {tooliconl}`respawn-pane` Restart a pane's process in place, preserving pane_id. ::: :::{grid-item-card} {tooliconl}`kill-pane` Terminate a pane. Destructive. ::: :::: ```{toctree} :hidden: :maxdepth: 1 capture-pane capture-since search-panes snapshot-pane get-pane-info find-pane-by-position display-message send-keys paste-text pipe-pane select-pane swap-pane set-pane-title clear-pane resize-pane enter-copy-mode exit-copy-mode wait-for-text wait-for-content-change wait-for-channel signal-channel respawn-pane kill-pane ``` --- # Kill pane Source: https://libtmux-mcp.git-pull.com/tools/pane/kill-pane/ # Kill pane ```{fastmcp-tool} pane_tools.kill_pane ``` **Use when** you're done with a specific terminal and want to remove it without affecting sibling panes. **Avoid when** you want to remove the entire window — use {tooliconl}`kill-window`. **Side effects:** Destroys the pane. Not reversible. **Example:** ```json { "tool": "kill_pane", "arguments": { "pane_id": "%1" } } ``` Response (string): ```text Pane killed: %1 ``` ```{fastmcp-tool-input} pane_tools.kill_pane ``` --- # Paste text Source: https://libtmux-mcp.git-pull.com/tools/pane/paste-text/ # Paste text ```{fastmcp-tool} pane_tools.paste_text ``` **Use when** you need to paste multi-line text into a pane — e.g. a code block, a config snippet, or a heredoc. Uses tmux paste buffers for clean multi-line input instead of sending text line-by-line via {tooliconl}`send-keys`. **Side effects:** Pastes text into the pane. With `bracket=true` (default), uses bracketed paste mode so the terminal knows this is pasted text. **Example:** ```json { "tool": "paste_text", "arguments": { "text": "def hello():\n print('world')\n", "pane_id": "%0" } } ``` Response (string): ```text Text pasted to pane %0 ``` ```{fastmcp-tool-input} pane_tools.paste_text ``` ## Destroy --- # Pipe pane Source: https://libtmux-mcp.git-pull.com/tools/pane/pipe-pane/ # Pipe pane ```{fastmcp-tool} pane_tools.pipe_pane ``` **Use when** you need to log pane output to a file — useful for monitoring long-running processes or capturing output that scrolls past the visible area. **Avoid when** you only need a one-time capture — use {tooliconl}`capture-pane` with `start`/`end` to read scrollback. **Side effects:** Starts or stops piping output to a file. Call with `output_path=null` to stop. **Example:** ```json { "tool": "pipe_pane", "arguments": { "pane_id": "%0", "output_path": "/tmp/build.log" } } ``` Response (start): ```text Piping pane %0 to /tmp/build.log ``` **Stopping the pipe:** ```json { "tool": "pipe_pane", "arguments": { "pane_id": "%0", "output_path": null } } ``` Response (stop): ```text Piping stopped for pane %0 ``` ```{fastmcp-tool-input} pane_tools.pipe_pane ``` --- # Resize pane Source: https://libtmux-mcp.git-pull.com/tools/pane/resize-pane/ # Resize pane ```{fastmcp-tool} pane_tools.resize_pane ``` **Use when** you need to adjust pane dimensions. **Side effects:** Changes pane size. May affect adjacent panes. **Example:** ```json { "tool": "resize_pane", "arguments": { "pane_id": "%0", "height": 15 } } ``` Response: ```json { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "15", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.resize_pane ``` --- # Respawn pane Source: https://libtmux-mcp.git-pull.com/tools/pane/respawn-pane/ # Respawn pane ```{fastmcp-tool} pane_tools.respawn_pane ``` **Use when** a pane's shell or command has wedged (hung REPL, runaway process, bad terminal mode) and you need a clean restart *without* destroying the `pane_id` references other tools or callers may still be holding. With `kill=True` (the default) tmux kills the current process first; optional `shell` relaunches with a different command; optional `start_directory` sets its cwd; optional `environment` adds per-process env vars (one `-e KEY=VALUE` flag per entry). **Avoid when** the pane genuinely needs to go away — use {tooliconl}`kill-pane` instead. Also avoid when you want to change the layout: `respawn-pane` preserves the pane in place. **Side effects:** Kills the current process (with `kill=True`) and starts a new one. **The `pane_id` is preserved** — that's the whole point of the tool. `pane_pid` updates to the new process. **Tip:** Call {tooliconl}`get-pane-info` first if you need to capture `pane_current_command` before respawn — the new process loses its argv. Omitting `shell` makes tmux replay the original argv (good default for shells; may differ for processes spawned via custom shell at split time). **Example — recover a wedged pane, relaunching the default shell:** ```json { "tool": "respawn_pane", "arguments": { "pane_id": "%5" } } ``` **Example — relaunch with a different command and working directory:** ```json { "tool": "respawn_pane", "arguments": { "pane_id": "%5", "shell": "pytest -x", "start_directory": "/home/user/project" } } ``` **Example — relaunch with extra environment variables:** ```json { "tool": "respawn_pane", "arguments": { "pane_id": "%5", "shell": "pytest -x", "environment": { "PYTHONPATH": "/home/user/project/src", "DATABASE_URL": "postgres://localhost/test" } } } ``` The audit log redacts each `environment` *value* via `{len, sha256_prefix}` digests but keeps the keys visible (env var names like `DATABASE_URL` are operator-debug-useful, while their values are the secret). Note that values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details. Response (PaneInfo): ```json5 { "pane_id": "%5", "pane_pid": "98765", "pane_current_command": "pytest", "pane_current_path": "/home/user/project", // ... } ``` ```{fastmcp-tool-input} pane_tools.respawn_pane ``` --- # Search panes Source: https://libtmux-mcp.git-pull.com/tools/pane/search-panes/ # Search panes ```{fastmcp-tool} pane_tools.search_panes ``` **Use when** you need to find specific text across multiple panes — locating which pane has an error, finding a running process, or checking output without knowing which pane to look in. **Avoid when** you already know the target pane — use {tooliconl}`capture-pane` for a one-shot read, or {tooliconl}`capture-since` for repeated observation. **Side effects:** None. Readonly. **Example:** ```json { "tool": "search_panes", "arguments": { "pattern": "FAIL", "session_name": "dev" } } ``` Response is a `SearchPanesResult` wrapper: the matching panes live under `matches`, and the wrapper fields (`truncated`, `truncated_panes`, `total_panes_matched`, `offset`, `limit`) support pagination. For larger result sets, iterate by re-calling with `offset += len(matches)`; stop when `truncated == false` and `truncated_panes == []`. Plain text searches with no content range use tmux's fast visual-row search. That path is quick, but it cannot match text split by terminal wrapping. Pass `regex=true` or a `content_start` / `content_end` range when long build, test, or log lines may cross the pane's wrap column. :::{note} Migrating from the flat-list shape Earlier alpha releases returned a bare `list[PaneContentMatch]`. Clients iterating the old shape directly (e.g. `for m in search_panes(...)`) must switch to `for m in search_panes(...).matches`. See the [CHANGES](../../../CHANGES) entry for context. ::: ```json { "matches": [ { "pane_id": "%0", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "window_id": "@0", "window_name": "editor", "session_id": "$0", "session_name": "dev", "matched_lines": [ "FAIL: test_upload (AssertionError)", "3 tests: 2 passed, 1 failed" ], "is_caller": false } ], "truncated": false, "truncated_panes": [], "total_panes_matched": 1, "offset": 0, "limit": 500 } ``` ```{fastmcp-tool-input} pane_tools.search_panes ``` --- # Select pane Source: https://libtmux-mcp.git-pull.com/tools/pane/select-pane/ # Select pane ```{fastmcp-tool} pane_tools.select_pane ``` **Use when** you need to focus a specific pane — by ID for a known target, or by direction (`up`, `down`, `left`, `right`, `last`, `next`, `previous`) to navigate a multi-pane layout. **Side effects:** Changes the active pane in the window. **Example:** ```json { "tool": "select_pane", "arguments": { "direction": "down", "window_id": "@0" } } ``` Response: ```json { "pane_id": "%1", "pane_index": "1", "pane_width": "80", "pane_height": "11", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12400", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.select_pane ``` --- # Send keys Source: https://libtmux-mcp.git-pull.com/tools/pane/send-keys/ # Send keys ```{fastmcp-tool} pane_tools.send_keys ``` **Use when** you need to type commands, press keys, or interact with a terminal. This is the primary way to execute commands in tmux panes. **Avoid when** you need to run something and immediately capture the result — compose `tmux wait-for -S ` into the keys and call {tooliconl}`wait-for-channel` for deterministic completion, or fall back to {tooliconl}`wait-for-text` / {tooliconl}`wait-for-content-change` when you must observe output the agent does not author. **Side effects:** Sends keystrokes to the pane. If `enter` is true (default), the command executes. **Example:** ```json { "tool": "send_keys", "arguments": { "keys": "npm start", "pane_id": "%2" } } ``` Response (string): ```text Keys sent to pane %2 ``` ```{fastmcp-tool-input} pane_tools.send_keys ``` --- # Set pane title Source: https://libtmux-mcp.git-pull.com/tools/pane/set-pane-title/ # Set pane title ```{fastmcp-tool} pane_tools.set_pane_title ``` **Use when** you want to label a pane for identification. **Side effects:** Changes the pane title. **Example:** ```json { "tool": "set_pane_title", "arguments": { "pane_id": "%0", "title": "build" } } ``` Response: ```json { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "24", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "build", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.set_pane_title ``` --- # Signal channel Source: https://libtmux-mcp.git-pull.com/tools/pane/signal-channel/ # Signal channel ```{fastmcp-tool} wait_for_tools.signal_channel ``` **Use when** you need to wake a blocked {tooliconl}`wait-for-channel` caller from a different MCP context (e.g. when a long-running task in one pane completes and another pane should proceed). Signalling an unwaited channel is a successful no-op — safe to call defensively. **Side effects:** Wakes any clients blocked on the named channel. Doesn't allocate or persist state. ```{fastmcp-tool-input} wait_for_tools.signal_channel ``` --- # Snapshot pane Source: https://libtmux-mcp.git-pull.com/tools/pane/snapshot-pane/ # Snapshot pane ```{fastmcp-tool} pane_tools.snapshot_pane ``` **Use when** you need a complete picture of a pane in a single call — visible text plus cursor position, whether the pane is in copy mode, scroll offset, and scrollback history size. Replaces separate `capture_pane` + `get_pane_info` calls when you need to reason about cursor location or terminal mode. **Avoid when** you only need raw text — {tooliconl}`capture-pane` is lighter. **Side effects:** None. Readonly. **Example:** ```json { "tool": "snapshot_pane", "arguments": { "pane_id": "%0" } } ``` Response: ```json { "pane_id": "%0", "content": "$ npm test\n\nPASS src/auth.test.ts\nTests: 3 passed\n$", "cursor_x": 2, "cursor_y": 4, "pane_width": 80, "pane_height": 24, "pane_left": 0, "pane_top": 0, "pane_right": 79, "pane_bottom": 23, "pane_at_left": true, "pane_at_right": true, "pane_at_top": true, "pane_at_bottom": true, "pane_tty": "/dev/pts/5", "pane_in_mode": false, "pane_mode": null, "scroll_position": null, "history_size": 142, "title": null, "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "is_caller": false } ``` The geometry block (`pane_left` / `pane_top` / `pane_right` / `pane_bottom` and the four `pane_at_*` predicates) is fetched in the same `display-message` round-trip as the cursor and mode fields, so there is no extra tmux call. To target a layout-relative pane (e.g. "the bottom-right pane") use {tooliconl}`find-pane-by-position` instead of computing edges from this snapshot. ```{fastmcp-tool-input} pane_tools.snapshot_pane ``` --- # Swap pane Source: https://libtmux-mcp.git-pull.com/tools/pane/swap-pane/ # Swap pane ```{fastmcp-tool} pane_tools.swap_pane ``` **Use when** you want to rearrange pane positions without changing content — e.g. moving a log pane from bottom to top. **Side effects:** Exchanges the visual positions of two panes. **Example:** ```json { "tool": "swap_pane", "arguments": { "source_pane_id": "%0", "target_pane_id": "%1" } } ``` Response: ```json { "pane_id": "%0", "pane_index": "1", "pane_width": "80", "pane_height": "11", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} pane_tools.swap_pane ``` --- # Wait for channel Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-channel/ # Wait for channel tmux's `wait-for` command exposes named, server-global channels that clients can signal and block on. These give agents an explicit synchronization primitive — strictly cheaper in agent turns than polling pane content via {tooliconl}`capture-pane` or {tooliconl}`wait-for-text`. The composition pattern: {tooliconl}`send-keys` a command followed by `; tmux wait-for -S NAME`, then call `wait_for_channel`. Shell `;` semantics fire the second statement whether the first succeeds or fails, so the edge-triggered signal never deadlocks the agent on a crashed command. ```python send_keys( pane_id="%1", keys="pytest; tmux wait-for -S tests_done", ) wait_for_channel("tests_done", timeout=60) ``` The `; tmux wait-for -S NAME` suffix is the load-bearing safety contract — `wait-for` is edge-triggered, so a crash before the signal would deadlock until the wait's `timeout`. The shell separator `;` runs the next statement unconditionally, so the signal fires on both success and failure paths. The payload deliberately does not append `exit $?` — in an interactive shell that exits the shell itself, taking single-pane sessions down with it. If exit-status preservation matters, capture the status out-of-band (e.g. write it to a file the agent reads later, or use a dedicated scratch pane). ```{fastmcp-tool} wait_for_tools.wait_for_channel ``` **Use when** the shell command can reliably emit the signal (single test runs, build scripts, dev-server boot, anything composable with `; tmux wait-for -S name`). **Avoid when** the signal cannot be guaranteed — for example, when the command might be killed externally. Use {tooliconl}`wait-for-text` to poll for an output marker instead; state-polling is structurally safer than edge-triggered signalling for fragile commands. **Side effects:** Blocks the call up to `timeout` seconds (default 30). Mandatory subprocess timeout — a crashed signaller raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` rather than blocking indefinitely. ```{fastmcp-tool-input} wait_for_tools.wait_for_channel ``` --- # Wait for content change Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-content-change/ # Wait for content change ```{fastmcp-tool} pane_tools.wait_for_content_change ``` **Use when** you've sent a command and need to wait for *something* to happen, but you don't know what the output will look like. Unlike {tooliconl}`wait-for-text`, this waits for *any* screen change rather than a specific pattern. **Avoid when** you know the expected output — {tooliconl}`wait-for-text` is more precise and avoids false positives from unrelated output. **Side effects:** None. Readonly. Blocks until content changes or timeout. Raises an {exc}`~libtmux_mcp._utils.ExpectedToolError` if the pane dies or is respawned while waiting, because the entry content baseline no longer describes the same pane process. **Example:** ```json { "tool": "wait_for_content_change", "arguments": { "pane_id": "%0", "timeout": 10 } } ``` Response: ```json { "changed": true, "pane_id": "%0", "elapsed_seconds": 1.234 } ``` ```{fastmcp-tool-input} pane_tools.wait_for_content_change ``` --- # Wait for text Source: https://libtmux-mcp.git-pull.com/tools/pane/wait-for-text/ # Wait for text ```{fastmcp-tool} pane_tools.wait_for_text ``` **Use when** you need to block until specific output appears — waiting for a server to start, a build to complete, or a prompt to return. **Avoid when** the expected text may never appear — always set a reasonable `timeout`. For repeated observation or tailing, use {tooliconl}`capture-since`; for command completion you control, use {tooliconl}`wait-for-channel`. **Side effects:** None. Readonly. Blocks until text appears or timeout. **Example:** ```json { "tool": "wait_for_text", "arguments": { "pattern": "Server listening", "pane_id": "%2", "timeout": 30 } } ``` Response: ```json { "found": true, "matched_lines": [ "Server listening on port 8000" ], "pane_id": "%2", "elapsed_seconds": 0.002, "risk_band_warned": false } ``` `risk_band_warned` is `true` when polling entered tmux's history-limit trim-risk band. In that state, matching remains best-effort because older scrollback can be discarded while the wait is active; use {tooliconl}`wait-for-channel` for deterministic command completion. ```{fastmcp-tool-input} pane_tools.wait_for_text ``` --- # Create session Source: https://libtmux-mcp.git-pull.com/tools/server/create-session/ # Create session ```{fastmcp-tool} server_tools.create_session ``` **Use when** you need a new isolated workspace. Sessions are the top-level container — create one before creating windows or panes. **Avoid when** a session with the target name already exists — check with {tooliconl}`list-sessions` first, or the command will fail. **Side effects:** Creates a new tmux session with one window and one pane. **Example:** ```json { "tool": "create_session", "arguments": { "session_name": "dev" } } ``` Response: ```json { "session_id": "$1", "session_name": "dev", "window_count": 1, "session_attached": "0", "session_created": "1774521872", "active_pane_id": "%0" } ``` ```{tip} The returned ``active_pane_id`` is the pane ID (``%N``) of the session's initial pane. It's guaranteed non-``None`` immediately after ``create_session`` (libtmux always creates the session with one initial pane), so you can target subsequent ``send_keys`` / ``split_window`` / ``capture_pane`` calls directly without a follow-up {tooliconl}`list-panes` round-trip — saving an MCP call in the most common "new session, then act on it" workflow. ``` ```{fastmcp-tool-input} server_tools.create_session ``` --- # Get server info Source: https://libtmux-mcp.git-pull.com/tools/server/get-server-info/ # Get server info ```{fastmcp-tool} server_tools.get_server_info ``` **Use when** you need to verify the tmux server is running, check its PID, or inspect server-level state before creating sessions. **Avoid when** you only need session names — use {tooliconl}`list-sessions`. **Side effects:** None. Readonly. **Example:** ```json { "tool": "get_server_info", "arguments": {} } ``` Response: ```json { "is_alive": true, "socket_name": null, "socket_path": null, "session_count": 2, "version": "3.6a" } ``` ```{fastmcp-tool-input} server_tools.get_server_info ``` --- # Server tools Source: https://libtmux-mcp.git-pull.com/tools/server/ # Server tools Server & process-level tools — discover sessions, control the tmux daemon, read/write server-scoped state. ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`list-sessions` List all sessions on the tmux server. ::: :::{grid-item-card} {tooliconl}`list-servers` Discover tmux daemons on the system. ::: :::{grid-item-card} {tooliconl}`get-server-info` Query server process identity and version. ::: :::{grid-item-card} {tooliconl}`create-session` Create a new tmux session. ::: :::{grid-item-card} {tooliconl}`kill-server` Terminate the tmux daemon. Destructive. ::: :::{grid-item-card} {tooliconl}`show-option` Read a tmux option (server / session / window / pane scope). ::: :::{grid-item-card} {tooliconl}`set-option` Set a tmux option. ::: :::{grid-item-card} {tooliconl}`show-environment` Read the server's environment variables. ::: :::{grid-item-card} {tooliconl}`set-environment` Set an environment variable on the server. ::: :::: ```{toctree} :hidden: :maxdepth: 1 list-sessions list-servers get-server-info create-session kill-server show-option set-option show-environment set-environment ``` --- # Kill server Source: https://libtmux-mcp.git-pull.com/tools/server/kill-server/ # Kill server ```{fastmcp-tool} server_tools.kill_server ``` **Use when** you need to tear down the entire tmux server. This kills every session, window, and pane. **Avoid when** you only need to remove one session — use {tooliconl}`kill-session`. **Side effects:** Destroys everything. Not reversible. **Example:** ```json { "tool": "kill_server", "arguments": {} } ``` Response (string): ```text Server killed successfully ``` ```{fastmcp-tool-input} server_tools.kill_server ``` --- # List servers Source: https://libtmux-mcp.git-pull.com/tools/server/list-servers/ # List servers ```{fastmcp-tool} server_tools.list_servers ``` **Use when** you need to discover other live tmux servers on this machine — for example, when an agent's tools were configured for the default server but the user is also running a separate tmux for a side project. **Avoid when** you already know the socket name or path you want to target — pass it directly to the tool that needs it via `socket_name`. **Side effects:** None. Readonly. Stale socket files are filtered via a kernel-fast UNIX `connect()` probe so the call stays under one second even on machines with thousands of orphaned `tmux-/` inodes. **Scope:** Only servers under `${TMUX_TMPDIR:-/tmp}/tmux-/` are discovered by the canonical scan. Custom `tmux -S /some/path/...` daemons that live outside that directory must be supplied via `extra_socket_paths`. **Example:** ```json { "tool": "list_servers", "arguments": {} } ``` Response: ```json [ { "is_alive": true, "socket_name": "default", "socket_path": null, "session_count": 3, "version": "3.6a" }, { "is_alive": true, "socket_name": "ci-runner", "socket_path": null, "session_count": 1, "version": "3.6a" } ] ``` To include a custom-path daemon: ```json { "tool": "list_servers", "arguments": { "extra_socket_paths": ["/home/user/.cache/tmux/socket"] } } ``` Paths that do not exist, are not sockets, or have no listener are silently skipped. ```{fastmcp-tool-input} server_tools.list_servers ``` ## Act --- # List sessions Source: https://libtmux-mcp.git-pull.com/tools/server/list-sessions/ # List sessions ```{fastmcp-tool} server_tools.list_sessions ``` **Use when** you need session names, IDs, or attached status before deciding which session to target. **Avoid when** you need window or pane details — use {tooliconl}`list-windows` or {tooliconl}`list-panes` instead. **Side effects:** None. Readonly. **Example:** ```json { "tool": "list_sessions", "arguments": {} } ``` Response: ```json [ { "session_id": "$0", "session_name": "myproject", "window_count": 2, "session_attached": "0", "session_created": "1774521871" } ] ``` ```{fastmcp-tool-input} server_tools.list_sessions ``` --- # Set environment Source: https://libtmux-mcp.git-pull.com/tools/server/set-environment/ # Set environment ```{fastmcp-tool} env_tools.set_environment ``` **Use when** you need to set a tmux environment variable. **Side effects:** Sets the variable in the tmux server. **Example:** ```json { "tool": "set_environment", "arguments": { "name": "MY_VAR", "value": "hello" } } ``` Response: ```json { "name": "MY_VAR", "value": "hello", "status": "set" } ``` ```{fastmcp-tool-input} env_tools.set_environment ``` --- # Set option Source: https://libtmux-mcp.git-pull.com/tools/server/set-option/ # Set option ```{fastmcp-tool} option_tools.set_option ``` **Use when** you need to change tmux behavior — adjusting history limits, enabling mouse support, changing status bar format. **Side effects:** Changes the tmux option value. **Example:** ```json { "tool": "set_option", "arguments": { "option": "history-limit", "value": "50000" } } ``` Response: ```json { "option": "history-limit", "value": "50000", "status": "set" } ``` ```{fastmcp-tool-input} option_tools.set_option ``` --- # Show environment Source: https://libtmux-mcp.git-pull.com/tools/server/show-environment/ # Show environment ```{fastmcp-tool} env_tools.show_environment ``` **Use when** you need to inspect tmux environment variables. **Side effects:** None. Readonly. **Example:** ```json { "tool": "show_environment", "arguments": {} } ``` Response: ```json { "variables": { "SHELL": "/bin/zsh", "TERM": "xterm-256color", "HOME": "/home/user", "USER": "user", "LANG": "C.UTF-8" } } ``` ```{fastmcp-tool-input} env_tools.show_environment ``` ## Act --- # Show option Source: https://libtmux-mcp.git-pull.com/tools/server/show-option/ # Show option ```{fastmcp-tool} option_tools.show_option ``` **Use when** you need to check a tmux configuration value — buffer limits, history size, status bar settings, etc. **Side effects:** None. Readonly. **Example:** ```json { "tool": "show_option", "arguments": { "option": "history-limit" } } ``` Response: ```json { "option": "history-limit", "value": "2000" } ``` ```{fastmcp-tool-input} option_tools.show_option ``` --- # Create window Source: https://libtmux-mcp.git-pull.com/tools/session/create-window/ # Create window ```{fastmcp-tool} session_tools.create_window ``` **Use when** you need a new terminal workspace within an existing session. **Side effects:** Creates a new window. Attaches to it if `attach` is true. **Example:** ```json { "tool": "create_window", "arguments": { "session_name": "dev", "window_name": "logs" } } ``` Response: ```json { "window_id": "@2", "window_name": "logs", "window_index": "3", "session_id": "$0", "session_name": "dev", "pane_count": 1, "window_layout": "b25f,80x24,0,0,5", "window_active": "1", "window_width": "80", "window_height": "24" } ``` ```{fastmcp-tool-input} session_tools.create_window ``` --- # Get session info Source: https://libtmux-mcp.git-pull.com/tools/session/get-session-info/ # Get session info ```{fastmcp-tool} session_tools.get_session_info ``` **Use when** you need metadata for a single session (ID, name, window count, attachment status, activity timestamp) and you already know its `session_id` or `session_name`. Avoids the `list_sessions` + filter dance. **Avoid when** you need every session — call `list_sessions` or iterate via the `tmux://sessions` resource. **Side effects:** None. Readonly. **Example:** ```json { "tool": "get_session_info", "arguments": { "session_id": "$0" } } ``` Response: ```json { "session_id": "$0", "session_name": "dev", "window_count": 3, "session_attached": "1", "session_created": "1713600000", "active_pane_id": "%0" } ``` Resolve by name when only the session_name is known: ```json { "tool": "get_session_info", "arguments": { "session_name": "dev" } } ``` ```{fastmcp-tool-input} session_tools.get_session_info ``` --- # Session tools Source: https://libtmux-mcp.git-pull.com/tools/session/ # Session tools Session-scoped tools — enumerate windows, rename or kill a session, switch windows within it. ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`list-windows` Enumerate windows inside a session. ::: :::{grid-item-card} {tooliconl}`get-session-info` Read metadata for one session. ::: :::{grid-item-card} {tooliconl}`select-window` Switch to a window by id, index, or direction. ::: :::{grid-item-card} {tooliconl}`create-window` Create a new window inside a session. ::: :::{grid-item-card} {tooliconl}`rename-session` Rename an existing session. ::: :::{grid-item-card} {tooliconl}`kill-session` Terminate a session. Destructive. ::: :::: ```{toctree} :hidden: :maxdepth: 1 list-windows get-session-info select-window create-window rename-session kill-session ``` --- # Kill session Source: https://libtmux-mcp.git-pull.com/tools/session/kill-session/ # Kill session ```{fastmcp-tool} session_tools.kill_session ``` **Use when** you're done with a workspace and want to clean up. Kills all windows and panes in the session. **Avoid when** you only want to close one window — use {tooliconl}`kill-window`. **Side effects:** Destroys the session and all its contents. Not reversible. **Example:** ```json { "tool": "kill_session", "arguments": { "session_name": "old-workspace" } } ``` Response (string): ```text Session killed: old-workspace ``` ```{fastmcp-tool-input} session_tools.kill_session ``` --- # List windows Source: https://libtmux-mcp.git-pull.com/tools/session/list-windows/ # List windows ```{fastmcp-tool} session_tools.list_windows ``` **Use when** you need window names, indices, or layout metadata within a session before selecting a window to work with. **Avoid when** you need pane-level detail — use {tooliconl}`list-panes`. **Side effects:** None. Readonly. **Example:** ```json { "tool": "list_windows", "arguments": { "session_name": "dev" } } ``` Response: ```json [ { "window_id": "@0", "window_name": "editor", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 2, "window_layout": "c195,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", "window_active": "1", "window_width": "80", "window_height": "24" }, { "window_id": "@1", "window_name": "server", "window_index": "2", "session_id": "$0", "session_name": "dev", "pane_count": 1, "window_layout": "b25f,80x24,0,0,2", "window_active": "0", "window_width": "80", "window_height": "24" } ] ``` ```{fastmcp-tool-input} session_tools.list_windows ``` --- # Rename session Source: https://libtmux-mcp.git-pull.com/tools/session/rename-session/ # Rename session ```{fastmcp-tool} session_tools.rename_session ``` **Use when** a session name no longer reflects its purpose. **Side effects:** Renames the session. Existing references by old name will break. **Example:** ```json { "tool": "rename_session", "arguments": { "session_name": "old-name", "new_name": "new-name" } } ``` Response: ```json { "session_id": "$0", "session_name": "new-name", "window_count": 2, "session_attached": "0", "session_created": "1774521871" } ``` ```{fastmcp-tool-input} session_tools.rename_session ``` --- # Select window Source: https://libtmux-mcp.git-pull.com/tools/session/select-window/ # Select window ```{fastmcp-tool} session_tools.select_window ``` **Use when** you need to switch focus to a different window — by ID, index, or direction (`next`, `previous`, `last`). **Side effects:** Changes the active window in the session. **Example:** ```json { "tool": "select_window", "arguments": { "direction": "next", "session_name": "dev" } } ``` Response: ```json { "window_id": "@1", "window_name": "server", "window_index": "2", "session_id": "$0", "session_name": "dev", "pane_count": 1, "window_layout": "b25f,80x24,0,0,2", "window_active": "1", "window_width": "80", "window_height": "24" } ``` ```{fastmcp-tool-input} session_tools.select_window ``` ## Destroy --- # Get window info Source: https://libtmux-mcp.git-pull.com/tools/window/get-window-info/ # Get window info ```{fastmcp-tool} window_tools.get_window_info ``` **Use when** you need metadata for a single window (name, index, layout, dimensions, pane count) and you already know the `window_id` or `window_index`. Avoids the `list_windows` + filter dance. **Avoid when** you need every window in a session — call `list_windows` with `session_id` or iterate via the `tmux://sessions/{name}/windows` resource. **Side effects:** None. Readonly. **Example:** ```json { "tool": "get_window_info", "arguments": { "window_id": "@1" } } ``` Response: ```json { "window_id": "@1", "window_name": "editor", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 2, "window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]", "window_active": "1", "window_width": "80", "window_height": "24" } ``` Resolve by `window_index` when only the index is known — requires `session_name` or `session_id` to disambiguate: ```json { "tool": "get_window_info", "arguments": { "window_index": "1", "session_name": "dev" } } ``` ```{fastmcp-tool-input} window_tools.get_window_info ``` --- # Window tools Source: https://libtmux-mcp.git-pull.com/tools/window/ # Window tools Window-scoped tools — enumerate panes, split / rename / relayout / resize / move / kill windows. ::::{grid} 1 2 2 3 :gutter: 2 2 3 3 :::{grid-item-card} {tooliconl}`list-panes` Enumerate panes inside a window. ::: :::{grid-item-card} {tooliconl}`get-window-info` Read metadata for one window. ::: :::{grid-item-card} {tooliconl}`split-window` Split a window into a new pane. ::: :::{grid-item-card} {tooliconl}`rename-window` Rename an existing window. ::: :::{grid-item-card} {tooliconl}`select-layout` Apply one of tmux's named layouts. ::: :::{grid-item-card} {tooliconl}`resize-window` Resize a window (client view). ::: :::{grid-item-card} {tooliconl}`move-window` Reorder a window or move it across sessions. ::: :::{grid-item-card} {tooliconl}`kill-window` Terminate a window. Destructive. ::: :::: ```{toctree} :hidden: :maxdepth: 1 list-panes get-window-info split-window rename-window select-layout resize-window move-window kill-window ``` --- # Kill window Source: https://libtmux-mcp.git-pull.com/tools/window/kill-window/ # Kill window ```{fastmcp-tool} window_tools.kill_window ``` **Use when** you're done with a window and all its panes. **Avoid when** you only want to remove one pane — use {tooliconl}`kill-pane`. **Side effects:** Destroys the window and all its panes. Not reversible. **Example:** ```json { "tool": "kill_window", "arguments": { "window_id": "@1" } } ``` Response (string): ```text Window killed: @1 ``` ```{fastmcp-tool-input} window_tools.kill_window ``` --- # List panes Source: https://libtmux-mcp.git-pull.com/tools/window/list-panes/ # List panes ```{fastmcp-tool} window_tools.list_panes ``` **Use when** you need to discover which panes exist in a window before sending keys or capturing output. **Side effects:** None. Readonly. **Example:** ```json { "tool": "list_panes", "arguments": { "session_name": "dev" } } ``` Response: ```json [ { "pane_id": "%0", "pane_index": "0", "pane_width": "80", "pane_height": "15", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12345", "pane_title": "build", "pane_active": "1", "window_id": "@0", "session_id": "$0", "is_caller": false }, { "pane_id": "%1", "pane_index": "1", "pane_width": "80", "pane_height": "8", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "12400", "pane_title": "", "pane_active": "0", "window_id": "@0", "session_id": "$0", "is_caller": false } ] ``` ```{fastmcp-tool-input} window_tools.list_panes ``` ## Act --- # Move window Source: https://libtmux-mcp.git-pull.com/tools/window/move-window/ # Move window ```{fastmcp-tool} window_tools.move_window ``` **Use when** you need to reorder windows within a session or move a window to a different session entirely. **Side effects:** Changes the window's index or parent session. **Example:** ```json { "tool": "move_window", "arguments": { "window_id": "@1", "destination_index": "1" } } ``` Response: ```json { "window_id": "@1", "window_name": "server", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 1, "window_layout": "b25f,80x24,0,0,2", "window_active": "0", "window_width": "80", "window_height": "24" } ``` ```{fastmcp-tool-input} window_tools.move_window ``` ## Destroy --- # Rename window Source: https://libtmux-mcp.git-pull.com/tools/window/rename-window/ # Rename window ```{fastmcp-tool} window_tools.rename_window ``` **Use when** a window name no longer reflects its purpose. **Side effects:** Renames the window. **Example:** ```json { "tool": "rename_window", "arguments": { "session_name": "dev", "new_name": "build" } } ``` Response: ```json { "window_id": "@0", "window_name": "build", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 2, "window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]", "window_active": "1", "window_width": "80", "window_height": "24" } ``` ```{fastmcp-tool-input} window_tools.rename_window ``` --- # Resize window Source: https://libtmux-mcp.git-pull.com/tools/window/resize-window/ # Resize window ```{fastmcp-tool} window_tools.resize_window ``` **Use when** you need to adjust the window dimensions. **Side effects:** Changes window size. **Example:** ```json { "tool": "resize_window", "arguments": { "session_name": "dev", "width": 120, "height": 40 } } ``` Response: ```json { "window_id": "@0", "window_name": "editor", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 2, "window_layout": "baaa,120x40,0,0[120x20,0,0,0,120x19,0,21,1]", "window_active": "1", "window_width": "120", "window_height": "40" } ``` ```{fastmcp-tool-input} window_tools.resize_window ``` --- # Select layout Source: https://libtmux-mcp.git-pull.com/tools/window/select-layout/ # Select layout ```{fastmcp-tool} window_tools.select_layout ``` **Use when** you want to rearrange panes — `even-horizontal`, `even-vertical`, `main-horizontal`, `main-vertical`, or `tiled`. **Side effects:** Rearranges all panes in the window. **Example:** ```json { "tool": "select_layout", "arguments": { "session_name": "dev", "layout": "even-vertical" } } ``` Response: ```json { "window_id": "@0", "window_name": "editor", "window_index": "1", "session_id": "$0", "session_name": "dev", "pane_count": 2, "window_layout": "even-vertical,80x24,0,0[80x12,0,0,0,80x11,0,13,1]", "window_active": "1", "window_width": "80", "window_height": "24" } ``` ```{fastmcp-tool-input} window_tools.select_layout ``` --- # Split window Source: https://libtmux-mcp.git-pull.com/tools/window/split-window/ # Split window ```{fastmcp-tool} window_tools.split_window ``` **Use when** you need side-by-side or stacked terminals within the same window. **Side effects:** Creates a new pane by splitting an existing one. **Example:** ```json { "tool": "split_window", "arguments": { "session_name": "dev", "direction": "right" } } ``` Response: ```json { "pane_id": "%4", "pane_index": "1", "pane_width": "39", "pane_height": "24", "pane_current_command": "zsh", "pane_current_path": "/home/user/myproject", "pane_pid": "3732", "pane_title": "", "pane_active": "0", "window_id": "@0", "session_id": "$0", "is_caller": false } ``` ```{fastmcp-tool-input} window_tools.split_window ``` --- # Architecture Source: https://libtmux-mcp.git-pull.com/topics/architecture/ (architecture)= # Architecture For contributors who need to understand the codebase internals. ## Source layout ``` src/libtmux_mcp/ __init__.py # Entry point: main() __main__.py # python -m libtmux_mcp support server.py # FastMCP instance and configuration _utils.py # Server caching, resolvers, serializers, error handling 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 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 option_tools.py # show_option, set_option env_tools.py # show_environment, set_environment resources/ hierarchy.py # tmux:// URI resources ``` ## Request flow Middleware wraps tool calls outermost-first (full ordering rationale in the `server.py` stack comment): ``` MCP Client (Claude, Cursor, etc.) → stdio transport → FastMCP server (server.py) → TimingMiddleware (wall-time observer) → TailPreservingResponseLimitingMiddleware (response size backstop) → ToolErrorResultMiddleware (exceptions → is_error results) → AuditMiddleware (one log record per call) → ReadonlyRetryMiddleware (retries readonly tools only) → SafetyMiddleware (tier gate, fail-closed) → Tool function (tools/*.py) → libtmux Server/Session/Window/Pane → tmux binary (via subprocess) ``` ## Key design decisions ### Tool registration Each tool module defines a `register(mcp)` function that registers tools with metadata: - `title` — human-readable name - `annotations` — MCP tool annotations (readOnlyHint, destructiveHint, idempotentHint) - `tags` — safety tier tags for middleware filtering ### Server caching `_utils.py` maintains a thread-safe cache keyed by `(socket_name, socket_path, tmux_bin)`. Dead servers are evicted on access via `is_alive()` checks. ### Object resolution Tools use resolver functions (`_resolve_session`, `_resolve_window`, `_resolve_pane`) that accept multiple targeting parameters and resolve to the correct libtmux object. Resolution follows a priority chain: direct ID → name lookup → error. ### Safety middleware `SafetyMiddleware` implements FastMCP's middleware interface. It operates as a secondary gate behind FastMCP's native tag visibility system, providing clear error messages when a tool above the configured tier is invoked. ### Error handling Three boundaries split the work: 1. **Tool classification** — the {func}`~libtmux_mcp._utils.handle_tool_errors` decorator wraps tool functions, mapping libtmux exceptions to {exc}`~libtmux_mcp._utils.ExpectedToolError` (agent-correctable: unknown ids, invalid arguments, transient tmux errors; logged at WARNING) or stock `ToolError` (operator faults and unexpected bugs; logged at ERROR). The raise chains the original exception via `from e`, which is what lets {class}`~libtmux_mcp.middleware.ReadonlyRetryMiddleware` match transient `LibTmuxException` causes. 2. **Schema classification** — FastMCP validates tool arguments before tool code runs, so Pydantic validation failures never reach the decorator. {class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware` classifies those schema-validation errors as expected, agent-correctable WARNINGs before converting them. 3. **Conversion** — {class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware` catches the exception once it has cleared the audit/retry/safety trio and returns `ToolResult(is_error=True)` carrying the message exactly as raised, plus a `_meta` payload (`error_type`, `expected`, and an optional agent-facing `suggestion` for recovery hints such as discovery tools or rejected-argument fixes). Errors must stay exceptions through the audit/retry/safety trio — audit detects failures by catching, retry matches via `__cause__` — so conversion happens only in the outermost error layer. The response limiter sits outside conversion and may truncate large success or error results on the return path; its truncation path preserves `is_error` and `_meta` so oversized expected failures stay tool errors. Level policy lives in {doc}`/topics/logging`. ## References - [libtmux](https://libtmux.git-pull.com/) — Core tmux Python library - [FastMCP](https://github.com/jlowin/fastmcp) — MCP server framework - [MCP Specification](https://modelcontextprotocol.io/) — Model Context Protocol - [tmux man page](http://man.openbsd.org/OpenBSD-current/man1/tmux.1) --- # Completion Source: https://libtmux-mcp.git-pull.com/topics/completion/ (completion-overview)= # Completion libtmux-mcp inherits FastMCP's built-in [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. ## What the spec does A client calls ``completion/complete`` with a partial argument for a prompt or resource URI template; the server replies with up to 100 suggestions. Agents use this to offer auto-complete UX — e.g. a session picker popup when filling ``session_name=`` on ``get_session``. ## 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. - **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. ```{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. ``` ## Workarounds for clients that need live enumeration Agents that need to pick a real session / window / pane can call {tool}`list-sessions`, {tool}`list-windows`, or {tool}`list-panes` directly before rendering a prompt, then feed the chosen ID back into the prompt's arguments. ## Further reading - [MCP completion spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion) - {doc}`/prompts` — the prompt argument surface - {doc}`/resources` — the resource-template parameter surface --- # Concepts Source: https://libtmux-mcp.git-pull.com/topics/concepts/ (concepts)= # Concepts The mental model you need to use libtmux-mcp effectively. ## tmux hierarchy tmux organizes terminals in a strict hierarchy: ``` Server (tmux server instance) └─ Session (named group of windows, e.g. "dev") └─ Window (tab within a session, e.g. "editor") └─ Pane (terminal split within a window) ``` libtmux-mcp mirrors this hierarchy. Every tool operates on one of these objects. ## Identifiers Each tmux object has identifiers you can use to target it: | Object | ID format | Name | Index | |--------|----------|------|-------| | Session | `$1`, `$2` | `"dev"`, `"build"` | — | | Window | `@1`, `@2` | `"editor"`, `"tests"` | `0`, `1`, `2` | | Pane | `%1`, `%2` | — | — | **Pane IDs are globally unique** within a tmux server and are the preferred targeting method. When you know the pane ID, use it directly — no session or window context needed. Session names and window names are human-readable but may not be unique. Window indexes are unique within a session. ## Targeting Most tools accept multiple targeting parameters. The resolution order is: 1. **Direct ID** — `pane_id`, `window_id`, or `session_id` (fastest, unambiguous) 2. **Name lookup** — `session_name` + optional `window_index` (convenient but may be ambiguous) 3. **Default** — If no targeting parameter is given, tools that need a single object will fail; list tools return everything For pane tools, you can combine parameters to narrow the search: `session_name` + `window_id` → find the pane in that specific window. ## Discovery vs. mutation Tools fall into three categories: - **Discovery** — Read-only operations: `list_sessions`, `list_windows`, `list_panes`, `capture_pane`, `capture_since`, `get_pane_info`, `find_pane_by_position`, `search_panes`, `wait_for_text`, `show_option`, `show_environment` - **Mutation** — Create, modify, or send input: `create_session`, `create_window`, `split_window`, `send_keys`, `rename_*`, `resize_*`, `set_pane_title`, `clear_pane`, `select_layout`, `set_option`, `set_environment` - **Destruction** — Remove tmux objects: `kill_server`, `kill_session`, `kill_window`, `kill_pane` These map to {ref}`safety tiers `. ## Agent self-awareness When the MCP server runs inside a tmux pane (detected via the `TMUX_PANE` environment variable), it: - Includes the caller's pane context in server instructions - Annotates the caller's own pane with `is_caller=true` in tool results - Prevents destructive tools from killing the caller's own pane, window, session, or server This means agents can safely explore and manage tmux without accidentally terminating themselves. ## Server caching Server instances are cached by `(socket_name, socket_path, tmux_bin)` tuple with thread-safe access. Dead servers are automatically evicted via `is_alive()` checks. This means multiple tool calls reuse the same server connection efficiently. ## Filtering List tools (`list_sessions`, `list_windows`, `list_panes`) support Django-style filters: ```json {"session_name__contains": "dev"} ``` Supported operators: `exact`, `contains`, `startswith`, `endswith`, `regex`, `icontains`, `iexact`, `istartswith`, `iendswith`, `iregex`. ## Resources In addition to tools, the MCP server exposes `tmux://` URI resources for browsing the hierarchy: - `tmux://sessions` — All sessions - `tmux://sessions/{session_name}` — Session detail with windows - `tmux://sessions/{session_name}/windows` — Session's windows - `tmux://sessions/{session_name}/windows/{window_index}` — Window detail with panes - `tmux://panes/{pane_id}` — Pane details - `tmux://panes/{pane_id}/content` — Pane captured content --- # Gotchas Source: https://libtmux-mcp.git-pull.com/topics/gotchas/ (gotchas)= # Gotchas Things that will bite you if you don't know about them in advance. For symptom-based debugging, see {ref}`troubleshooting `. ## Metadata vs. content {tooliconl}`list-panes` and {tooliconl}`list-windows` search **metadata** — names, IDs, current command. They do not search what is displayed in the terminal. To find text that is visible in terminals, use {tooliconl}`search-panes`. To read what a specific pane shows once, use {tooliconl}`capture-pane`; to keep watching that pane, use {tooliconl}`capture-since`. This is the most common source of agent confusion. The server instructions already warn about this, but it bears repeating: if a user asks "which pane mentions error", the answer is `search_panes`, not `list_panes`. ## `send_keys` sends Enter by default When you call `send_keys` with `keys: "C-c"`, it sends Ctrl-C **and then presses Enter**. For control sequences, set `enter: false`: ```json {"tool": "send_keys", "arguments": {"keys": "C-c", "pane_id": "%0", "enter": false}} ``` The `enter` parameter defaults to `true`, which is correct for commands (`make test` + Enter) but wrong for control keys, partial input, or key sequences. ## `capture_pane` after `send_keys` is a race condition `send_keys` returns immediately after sending keystrokes to tmux. It does **not** wait for the command to execute or produce output. ```json {"tool": "send_keys", "arguments": {"keys": "pytest", "pane_id": "%0"}} {"tool": "capture_pane", "arguments": {"pane_id": "%0"}} ``` The capture above may return the terminal state **before** pytest runs. Compose `tmux wait-for -S ` into the command and block on {tooliconl}`wait-for-channel` — deterministic, race-free: ```json {"tool": "send_keys", "arguments": {"keys": "pytest; tmux wait-for -S pytest_done", "pane_id": "%0"}} {"tool": "wait_for_channel", "arguments": {"channel": "pytest_done", "timeout": 60}} {"tool": "capture_pane", "arguments": {"pane_id": "%0"}} ``` For output the agent does not author (third-party logs, daemon prompts, interactive supervisors), substitute {tooliconl}`wait-for-text` for `wait_for_channel`. See {ref}`recipes` for the complete pattern. ## Repeated `capture_pane` calls resend old output If you are tailing a pane or checking a long-running process over several turns, repeated {tooliconl}`capture-pane` calls keep returning the same visible screen and scrollback. Use {tooliconl}`capture-since` instead: the first call returns a cursor, and follow-up calls return only output written or rewritten after that cursor. If tmux has already trimmed or cleared the needed history, the result marks `lines_missed=true` and gives you a fresh cursor. ## Window names are not unique across sessions Two sessions can each have a window named "editor". Targeting by `window_name` alone is ambiguous — always include `session_name` or use the globally unique `window_id` (e.g., `@0`, `@1`). Pane IDs (`%0`, `%1`, etc.) are globally unique and are the preferred targeting method. ## Pane IDs are globally unique but ephemeral Pane IDs like `%0`, `%5`, `%12` are unique across all sessions and windows within a tmux server. They do not reset when windows are created or destroyed. However, they reset when the tmux **server** restarts. Do not cache pane IDs across server restarts. After killing and recreating a session, re-discover pane IDs with {ref}`list-panes`. ## `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`. ## Gemini CLI injects `wait_for_previous` into tool arguments When Gemini CLI batches several tool calls in one turn, its scheduler merges the internal sequencing flag of the later calls into the MCP tool's arguments: ```json {"tool": "get_pane_info", "arguments": {"wait_for_previous": true, "pane_id": "%0"}} ``` This has been observed with stock Gemini CLI behavior (no extensions involved). In local non-interactive `gemini -p` harness runs, batching can front-load the topic tool and the first MCP calls into one parallel turn, which is where the leaked flag usually appears; interactive sessions tend to schedule more sequentially and trigger it less often. Tool schemas are strict (`additionalProperties: false`), so the call is rejected with a validation error — classified as expected (WARNING log, `expected: true` in the result's `_meta`) and carrying a suggestion that names the rejected argument and identifies `wait_for_previous` as a client scheduling flag to retry without. Gemini's model reads it, drops the flag, and retries successfully on its own. The visible symptom is harmless noise: `Error executing tool mcp_tmux_: ... reported an error` lines in Gemini's output for calls that then succeed on retry, and matching WARNING records in the server log. Similar reports in other MCP servers have handled this injected key by stripping it or whitelisting arguments against the schema ([MemPalace/mempalace#816](https://github.com/MemPalace/mempalace/issues/816)). This server deliberately keeps the rejection: silently dropping unknown arguments would also swallow genuine argument typos from every client — on a server with mutating and destructive tools, a mis-named flag (`enter` on `send_keys`, say) must fail loudly, not run with defaults. --- # Topics Source: https://libtmux-mcp.git-pull.com/topics/ # Topics Explore libtmux-mcp's core ideas and design at a high level. ::::{grid} 1 1 2 2 :gutter: 2 2 3 3 :::{grid-item-card} Architecture :link: architecture :link-type: doc Source layout, request flow, and extension points. ::: :::{grid-item-card} Concepts :link: concepts :link-type: doc tmux hierarchy, MCP protocol, and the mental model. ::: :::{grid-item-card} Safety Tiers :link: safety :link-type: doc Three-tier safety system for controlling tool access. ::: :::{grid-item-card} Troubleshooting :link: troubleshooting :link-type: doc Symptom-based guide for common issues. ::: :::{grid-item-card} Gotchas :link: gotchas :link-type: doc Things that will bite you if you don't know about them. ::: :::{grid-item-card} Agent Prompting :link: prompting :link-type: doc Write effective instructions for AI agents using tmux tools. ::: :::: ## MCP protocol utilities How libtmux-mcp maps to three optional utility capabilities from the Model Context Protocol specification. ::::{grid} 1 1 3 3 :gutter: 2 2 3 3 :::{grid-item-card} Completion :link: completion :link-type: doc Argument auto-complete — what FastMCP derives automatically and what libtmux-mcp does not yet wire up. ::: :::{grid-item-card} Logging :link: logging :link-type: doc Server-to-client log forwarding and the ``libtmux_mcp.*`` logger hierarchy. ::: :::{grid-item-card} Pagination :link: pagination :link-type: doc Protocol cursors, ``search_panes`` paging, and ``capture_since`` observation cursors. ::: :::: ```{toctree} :hidden: architecture concepts safety gotchas prompting completion logging pagination troubleshooting ``` --- # Logging Source: https://libtmux-mcp.git-pull.com/topics/logging/ (logging-overview)= # Logging libtmux-mcp uses Python's standard ``logging`` module under the ``libtmux_mcp.*`` namespace. FastMCP forwards server-side log records to connected MCP clients via the [MCP logging capability](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging). No manual wiring needed. ## Logger hierarchy All loggers are children of ``libtmux_mcp``. The primary streams are: - ``libtmux_mcp.audit`` — one structured line per tool call, emitted by {class}`~libtmux_mcp.middleware.AuditMiddleware`. Includes tool name, digest-redacted arguments, latency, outcome. See {doc}`/topics/safety` for the argument-redaction rules. - ``libtmux_mcp.retry`` — warnings from {class}`~libtmux_mcp.middleware.ReadonlyRetryMiddleware` when a readonly tool retried after a transient {exc}`libtmux.exc.LibTmuxException`. - ``libtmux_mcp.server`` / ``libtmux_mcp.tools.*`` / etc. — ad-hoc warnings and debug messages from the codebase. - ``fastmcp.errors`` — one record per failed tool call, emitted by {class}`~libtmux_mcp.middleware.ToolErrorResultMiddleware`. ## Error levels Tool failures are logged at a level matching who needs to act: - **WARNING** — expected, agent-correctable failures: unknown pane/window/session ids, invalid arguments, safety-tier denials, transient tmux errors. The calling agent receives the message and can correct course; operators don't need to. - **ERROR** — operator faults and potential bugs: a missing ``tmux`` binary, or any unexpected exception escaping a tool. A WARNING-heavy, ERROR-quiet log stream is therefore healthy — it means agents are probing and recovering. ERROR records are the ones worth alerting on. ## Level control Set the logger level via standard Python mechanisms — for local development, the simplest is an environment variable: ```console $ FASTMCP_LOG_LEVEL=DEBUG libtmux-mcp ``` FastMCP reads ``FASTMCP_LOG_LEVEL`` at startup and applies it to every ``fastmcp.*`` and ``libtmux_mcp.*`` logger. ## What clients see MCP clients render incoming ``notifications/message`` records in their log pane (e.g. Claude Desktop's "MCP server logs" panel, or ``claude-cli``'s ``--verbose`` output). The records include the server name (``libtmux-mcp``), level, and the log message — but not the Python logger name, which the protocol doesn't model. ```{tip} If a tool call fails silently (no user-visible error, no side effect), the ``libtmux_mcp.audit`` log will show the invocation and its return value. That's usually the fastest way to tell whether a tool ran at all. ``` ## Further reading - [MCP logging spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging) - {doc}`/topics/safety` — audit log redaction rules - {class}`~libtmux_mcp.middleware.AuditMiddleware` — the primary audit emitter --- # Pagination Source: https://libtmux-mcp.git-pull.com/topics/pagination/ (pagination-overview)= # Pagination libtmux-mcp follows the [MCP pagination spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination): ``tools/list``, ``prompts/list``, ``resources/list``, and ``resources/templates/list`` all return an opaque ``nextCursor`` when a page is truncated, and accept ``cursor`` on the next call to resume. ## Where cursors and pages show up ### Protocol-level list calls FastMCP handles ``tools/list`` / ``prompts/list`` / ``resources/list`` / ``resources/templates/list`` pagination automatically. Neither libtmux-mcp nor the agent needs to do anything: the server chooses a sensible page size, encodes the cursor in an opaque base64 blob, and replays state from it. Callers only need to thread through ``nextCursor`` if they consume the raw MCP protocol. ### Tool-level result paging on ``search_panes`` One libtmux-mcp tool owns its own paging surface because a single tmux server can carry tens of thousands of pane lines: - {tool}`search-panes` returns a {class}`~libtmux_mcp.models.SearchPanesResult` wrapper with ``matches``, ``truncated``, ``truncated_panes``, ``total_panes_matched``, ``offset``, and ``limit``. - Agents detect ``truncated=True`` and re-call with a higher ``offset`` to page through the match set. This is application-level paging (not MCP-cursor pagination) — the agent decides how many matches it needs and when to stop. ### Tool-level observation cursors on ``capture_since`` {tool}`capture-since` also has a ``cursor`` parameter, but it is not a pagination cursor. The first call captures the current visible pane and returns an opaque observation checkpoint. Follow-up calls pass that cursor back to receive only rows written or rewritten after the checkpoint while tmux still retains the needed history. Because the cursor points into live tmux grid state, it has different failure modes from protocol pagination: - If the pane output scrolls into retained history, the cursor can still produce an exact delta. - If tmux clears or trims the needed history, the response sets ``lines_missed=True`` and returns a conservative current visible capture with a fresh cursor. - If the pane dies or is respawned, the cursor is invalid because it would otherwise point at a different process's terminal state. This is application-level observation, not a stable collection scan. Use it to reduce repeated pane reads, not to page through search matches. ## Why separate paths Protocol-level cursors are for **collections the server owns end-to-end**: the tool / prompt / resource registries. The server knows what it has, so an opaque cursor is cheap. Tool-level paging and observation cursors are for **state derived from live tmux panes**. Capturing every pane's contents and running a regex is expensive, and the result set can change mid-scan (new panes open, old ones close). Repeatedly reading one pane has the opposite cost shape: the target is known, but unchanged scrollback wastes model context. libtmux-mcp exposes each contract separately instead of pretending live terminal state is one stable list. ## Further reading - [MCP pagination spec](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/pagination) - {class}`~libtmux_mcp.models.SearchPanesResult` — the structured wrapper for ``search_panes`` - {tool}`search-panes` — the tool itself - {class}`~libtmux_mcp.models.CaptureSinceResult` — the structured response for ``capture_since`` - {tool}`capture-since` — incremental observation for a known pane --- # Agent prompting guide Source: https://libtmux-mcp.git-pull.com/topics/prompting/ (prompting)= # Agent prompting guide How to write effective instructions for AI agents using libtmux-mcp. ## What the server tells your agent automatically Every MCP client receives these instructions when connecting to the libtmux-mcp server. You do not need to repeat this information — the agent already knows it. ```{code-block} text :class: server-prompt libtmux MCP server for programmatic tmux control. tmux hierarchy: Server > Session > Window > Pane. Use pane_id (e.g. '%1') as the preferred targeting method - it is globally unique within a tmux server. Use send_keys to execute commands, capture_pane for one-shot reads, and capture_since for repeated observation. All tools accept an optional socket_name parameter for multi-server support (defaults to LIBTMUX_SOCKET env var). IMPORTANT — metadata vs content: list_windows, list_panes, and list_sessions only search metadata (names, IDs, current command). To find text that is actually visible in terminals — when users ask what panes 'contain', 'mention', 'show', or 'have' — use search_panes to search across all pane contents, capture_since for repeated reads of a known pane, or capture_pane for a one-shot manual inspection. ``` The server also dynamically adds: - **Safety tier context**: Which tier is active and what tools are available - **Caller pane awareness**: If the server runs inside tmux, it tells the agent which pane is its own (via `TMUX_PANE`). See {ref}`concepts` "Agent self-awareness" for details. ## Activation and discovery This server is designed to fire on bare terminal-surface phrasing — *"split this pane"*, *"what's in my current window"*, *"start a session for the build"* — without you having to say "tmux". The server treats `pane`, `session`, `window`, and `split` as positive triggers; tmux ID prefixes (`%1`, `@1`, `$1`) are unambiguous. The server stays out of the way when you mean a browser window, an editor split, an i3/Hyprland workspace, or a Jupyter notebook cell — if your phrasing is ambiguous, the agent will ask before acting. ### Always-on tool listing (Claude Code only) [Claude Code](https://code.claude.com/docs/en/mcp) defers loading MCP tool schemas when they exceed ~10% of your context window. By default, the three discovery anchors ({toolref}`list-panes`, {toolref}`list-windows`, {toolref}`snapshot-pane`) carry an `anthropic/alwaysLoad: true` hint so bare *"pane"* / *"window"* prompts surface this server without a `ToolSearch` hop. To force libtmux-mcp's *full* schema list to load upfront — useful if you also want {toolref}`send-keys`, {toolref}`capture-pane`, {toolref}`select-window` etc. preloaded — add `"alwaysLoad": true` at the server entry level (requires Claude Code v2.1.121+): ```json { "mcpServers": { "tmux": { "command": "uvx", "args": ["libtmux-mcp"], "alwaysLoad": true } } } ``` Cost: ~3-5K tokens of permanent context budget. Use only if libtmux-mcp is one of your top-3 most-used MCPs. ## Effective prompt patterns These natural-language prompts reliably trigger the right tool sequences: | Prompt | Agent interprets as | |--------|-------------------| | [Run `pytest` in my build pane and show results]{.prompt} | {toolref}`send-keys` (with `tmux wait-for -S` composed in) → {toolref}`wait-for-channel` → {toolref}`capture-pane` | | [Start the dev server and wait until it's ready]{.prompt} | {toolref}`send-keys` → {toolref}`wait-for-text` (for "listening on" — third-party output the agent doesn't author) | | [Spin up the dev server in the bottom-right pane]{.prompt} | {toolref}`find-pane-by-position` (corner=bottom-right) → {toolref}`send-keys` → {toolref}`wait-for-text` (for the server's readiness banner) | | [Check if any pane has errors]{.prompt} | {toolref}`search-panes` with pattern "error" | | [Keep watching the server pane]{.prompt} | {toolref}`capture-since` with the previous cursor | | [Set up a workspace with editor, server, and tests]{.prompt} | {toolref}`create-session` → {toolref}`split-window` (x2) → {toolref}`set-pane-title` (x3) | | [What's running in my tmux sessions?]{.prompt} | {toolref}`list-sessions` → {toolref}`list-panes` → {toolref}`capture-pane` | | [Kill the old workspace session]{.prompt} | {toolref}`kill-session` (after confirming target) | ## Anti-patterns to avoid | Prompt | Problem | Better version | |--------|---------|---------------| | [Run this command]{.prompt} | Ambiguous — agent may use its own shell instead of tmux | [Run `make test` in a tmux pane]{.prompt} | | [Check my terminal]{.prompt} | Which pane? Agent must discover first | [Check the pane running `npm dev`]{.prompt} or [Search all panes for errors]{.prompt} | | [Clean up everything]{.prompt} | Too broad for destructive operations | [Kill the `ci-test` session]{.prompt} | | [Show me the output]{.prompt} | Capture immediately? Or wait? | [Wait for the command to finish, then show me the output]{.prompt} | ## System prompt fragments Copy these into your agent's system instructions (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or MCP client config) to improve behavior: ### For general tmux workflows ```{code-block} text :class: system-prompt When executing long-running commands (servers, builds, test suites), use tmux via the libtmux MCP server rather than running them directly. This keeps output accessible for later inspection. For command completion, compose `tmux wait-for -S ` into the shell command and call wait_for_channel — deterministic, no polling. Use wait_for_text or wait_for_content_change for observation flows (third-party logs, daemon prompts), and use capture_since when you need to read the same pane repeatedly. Never capture_pane immediately after send_keys — the command may still be running. ``` ### For safe agent behavior ```{code-block} text :class: system-prompt Before creating tmux sessions, check list_sessions to avoid duplicates. Always use pane_id for targeting — it is globally unique. Never run destructive operations (kill_session, kill_server) without confirming the target with the user first. ``` ### For development workflows ```{code-block} text :class: system-prompt When the user asks you to run tests or start servers, use dedicated tmux panes. Split windows to run related processes side-by-side. Use wait_for_text to know when a server is ready before running tests that depend on it. ``` ### To bias activation on bare "pane" / "window" / "session" ```{code-block} text :class: system-prompt This project uses tmux for its development workflow. When the user says "pane", "window", or "session" without further qualification, prefer the tmux MCP tools (libtmux-mcp) before assuming GUI semantics. ``` This is the lever closest to the failure mode for *"current window"* / *"this session"* anaphora — a project-level instructions file (`AGENTS.md`, `CLAUDE.md`, or whichever your host honors) lives one prompt-layer above the MCP server's `instructions` and your host composes it into the system prompt with higher priority. ## Tool selection heuristics When an agent is unsure which tool to use, these rules help: 1. **Discovery first**: Call {toolref}`list-sessions` or {toolref}`list-panes` before acting on specific targets 2. **Prefer IDs**: Once you have a `pane_id`, use it for all subsequent calls — it never changes during the pane's lifetime 3. **Wait, don't poll**: For commands the agent authors, prefer {toolref}`wait-for-channel` with `tmux wait-for -S ` composed into the command — deterministic and race-free. Use {toolref}`capture-since` for repeated observation, and fall back to {toolref}`wait-for-text` or {toolref}`wait-for-content-change` for output the agent doesn't author. Never call {toolref}`capture-pane` in a retry loop. 4. **Content vs. metadata**: If looking for text *in* a terminal, use {toolref}`search-panes`. If looking for pane *properties* (name, PID, path), use {toolref}`list-panes` or {toolref}`get-pane-info` 5. **Destructive tools are opt-in**: Never kill sessions, windows, or panes unless the user explicitly asks --- # Safety tiers Source: https://libtmux-mcp.git-pull.com/topics/safety/ (safety)= # Safety tiers libtmux-mcp uses a three-tier safety system to control which tools are available to AI agents. ## Overview | Tier | Label | Access | Use case | |------|-------|--------|----------| | `readonly` | {badge}`readonly` | List, capture, search, info | Monitoring, browsing | | `mutating` (default) | {badge}`mutating` | + create, send_keys, rename, resize | Normal agent workflow | | `destructive` | {badge}`destructive` | + kill_server, kill_session, kill_window, kill_pane | Full control | ## Configuration Set the safety tier via the {envvar}`LIBTMUX_SAFETY` environment variable: ```json { "mcpServers": { "libtmux": { "command": "uvx", "args": ["libtmux-mcp"], "env": { "LIBTMUX_SAFETY": "readonly" } } } } ``` ## How it works ### Dual-layer gating 1. **FastMCP tag visibility**: Tools are tagged with their tier. Only tags at or below the configured tier are enabled via `mcp.enable(tags=..., only=True)`. 2. **Safety middleware**: A secondary middleware layer hides tools from listings and blocks execution with clear error messages if a tool above the tier is somehow invoked. ### Tool tags Every tool is tagged with exactly one safety tier: - {badge}`readonly` `readonly` — Read-only operations that don't modify tmux state - {badge}`mutating` `mutating` — Operations that create, modify, or send input to tmux objects - {badge}`destructive` `destructive` — Operations that destroy tmux objects (kill commands) ### Fail-closed design Tools without a recognized tier tag are **denied by default**. This prevents accidentally exposing new tools without explicit safety classification. ## Self-kill protection Destructive tools include safeguards against self-harm: - {tool}`kill-server` refuses to run if the MCP server is inside the target server - {tool}`kill-session` refuses to kill the session containing the MCP pane - {tool}`kill-window` refuses to kill the window containing the MCP pane - {tool}`kill-pane` refuses to kill the pane running the MCP server These protections read both the `TMUX` and `TMUX_PANE` environment variables that tmux injects into pane child processes. The `TMUX` value is formatted `socket_path,server_pid,session_id` — libtmux-mcp parses the socket path and compares it to the target server's so the guard only fires when the caller is actually on the same tmux server. A kill across unrelated sockets is allowed; a kill of the caller's own pane/window/session/server is refused. If the caller's socket can't be determined (rare — `TMUX_PANE` set without `TMUX`), the guard errs on the side of blocking. ### macOS `TMUX_TMPDIR` caveat The self-kill guard resolves the target server's socket path in three steps (`_effective_socket_path` in `src/libtmux_mcp/_utils.py`): 1. Use `Server.socket_path` if libtmux already has it. 2. Otherwise query the running server via `display-message -p '#{socket_path}'` — authoritative because tmux itself reports the path it is actually using, regardless of the MCP process environment. This closes the launchd-vs-interactive-shell gap on macOS where {envvar}`TMUX_TMPDIR` commonly differs between contexts. 3. Fall back to reconstruction from {envvar}`TMUX_TMPDIR` (or `/tmp`) + euid + socket name. Only reached when the target server is unreachable (not running), in which case no self-kill is possible anyway and `_caller_is_on_server`'s None-socket branch blocks conservatively. The structural fix shipped in 0.1.x; setting {envvar}`TMUX_TMPDIR` explicitly is no longer required for the guard to work, though it remains a useful diagnostic when investigating mismatched-path bug reports. ## Footguns inside the `mutating` tier Most `mutating` tools are bounded: `resize_pane` only resizes, `rename_window` only renames. A few have broader reach because tmux itself exposes broader reach. Treat these as elevated risk even though they share the default tier: ### `pipe_pane` {tool}`pipe-pane` pipes a pane's output to a shell command that the server runs. In practice this means the caller chooses an arbitrary path or pipeline on the server host. There is no allow-list. Assume it can create files anywhere the server process can write. Mitigations: - Run the server as an unprivileged user with a scoped home directory. - Consider `LIBTMUX_SAFETY=readonly` for untrusted MCP clients. - Audit log records (see below) capture the `output_path` argument so reviewers can spot unexpected destinations. ### `set_environment` {tool}`set-environment` writes into tmux's global, session, or window environment. Those values propagate into every shell tmux spawns afterwards. An agent that writes `PATH`, `LD_PRELOAD`, or `AWS_*` variables can influence every future command on that scope — including commands the user runs directly, not just commands the agent issues. Mitigations: - The audit log redacts the `value` argument to a `{len, sha256_prefix}` digest so log files don't leak the secrets agents set, but operators should still treat the tool as high-privilege. - If only a single command needs an env override, prefer having the agent invoke `env VAR=value command` via `send_keys` instead — the blast radius is one command, not every future child. ### `respawn_pane` {tool}`respawn-pane` restarts a pane's process while preserving the pane id and layout — exactly what an agent wants when a shell wedges. Default `kill=True` terminates the running process before relaunch. The `pane_id` and layout are preserved (the point of the tool), but any unsaved REPL state, ssh session, or in-flight job in that pane is lost. Repeated calls are *not* idempotent — each call kills a new process. Unlike other `mutating` tools, the registration carries `destructiveHint=True` and `idempotentHint=False` (via the `ANNOTATIONS_MUTATING_DESTRUCTIVE` preset) so MCP clients see honest annotations even though the tier tag stays at `mutating` for default-profile recovery. Mitigations: - `pane_id` is required (no fallback to "first pane in session/window"). Agents that pass only `session_name` get an {exc}`~libtmux_mcp._utils.ExpectedToolError` instead of an unintended kill — resolve via {tool}`list-panes` first. - Any `shell` argument is briefly visible in the OS process table and tmux's `pane_current_command` metadata before the spawned shell takes over; the audit log redacts `shell` payloads (see below), but do not pass credentials directly even with redaction. - The optional `environment` argument (`dict[str, str]`) maps to one tmux `-e KEY=VALUE` flag per item. The audit log redacts each *value* via a `{len, sha256_prefix}` digest while keeping the *keys* visible — env var names like `DATABASE_URL` are usually operator-debug-useful, but their values are the secret. The same OS-process-table caveat as `shell` applies: `respawn-pane -e DB_PASSWORD=...` may briefly appear in `ps` output before the spawned process inherits the env. - The same self-pane guard that protects the destructive kill commands also refuses to respawn the pane running the MCP server. ### `send_keys` / `paste_text` These can execute anything the pane's shell accepts. There is no payload validation. The audit log stores a digest of the content, not the content itself, so a secret typed via `send_keys` does not land in logs. ## Audit log Every tool call emits one `INFO` record on the `libtmux_mcp.audit` logger carrying: - `tool` — the tool name - `outcome` — `ok` or `error`, with `error_type` on failure - `duration_ms` - `client_id` / `request_id` — from the fastmcp context when available - `args` — a summary of arguments. Sensitive scalar keys (`keys`, `text`, `value`, `content`, `shell`) are replaced by `{len, sha256_prefix}`; the dict-shaped sensitive key `environment` keeps its keys but digests each value individually. Non-sensitive strings over 200 characters are truncated. Route this logger to a dedicated sink if you want a durable audit trail; it is deliberately namespaced separately from the main `libtmux_mcp` logger. ## Tool annotations Each tool carries MCP tool annotations that hint at its behavior: | Tool | Tier | readOnly | destructive | idempotent | |------|------|----------|-------------|------------| | {ref}`list-sessions` | {badge}`readonly` | true | false | true | | {ref}`get-server-info` | {badge}`readonly` | true | false | true | | {ref}`list-windows` | {badge}`readonly` | true | false | true | | {ref}`list-panes` | {badge}`readonly` | true | false | true | | {ref}`capture-pane` | {badge}`readonly` | true | false | true | | {ref}`capture-since` | {badge}`readonly` | true | false | true | | {ref}`get-pane-info` | {badge}`readonly` | true | false | true | | {ref}`search-panes` | {badge}`readonly` | true | false | true | | {ref}`wait-for-text` | {badge}`readonly` | true | false | true | | {ref}`show-option` | {badge}`readonly` | true | false | true | | {ref}`show-environment` | {badge}`readonly` | true | false | true | | {ref}`create-session` | {badge}`mutating` | false | false | false | | {ref}`create-window` | {badge}`mutating` | false | false | false | | {ref}`split-window` | {badge}`mutating` | false | false | false | | {ref}`send-keys` | {badge}`mutating` | false | false | false | | {ref}`rename-session` | {badge}`mutating` | false | false | true | | {ref}`rename-window` | {badge}`mutating` | false | false | true | | {ref}`resize-pane` | {badge}`mutating` | false | false | true | | {ref}`resize-window` | {badge}`mutating` | false | false | true | | {ref}`set-pane-title` | {badge}`mutating` | false | false | true | | {ref}`clear-pane` | {badge}`mutating` | false | false | true | | {ref}`select-layout` | {badge}`mutating` | false | false | true | | {ref}`set-option` | {badge}`mutating` | false | false | true | | {ref}`set-environment` | {badge}`mutating` | false | false | true | | {ref}`respawn-pane` | {badge}`mutating` | false | true | false | | {ref}`kill-server` | {badge}`destructive` | false | true | false | | {ref}`kill-session` | {badge}`destructive` | false | true | false | | {ref}`kill-window` | {badge}`destructive` | false | true | false | | {ref}`kill-pane` | {badge}`destructive` | false | true | false | --- # Troubleshooting Source: https://libtmux-mcp.git-pull.com/topics/troubleshooting/ (troubleshooting)= # Troubleshooting Symptom-based guide. Find your problem, follow the steps. ## Server doesn't appear in client **Symptoms**: Client shows no libtmux tools, or "server not found" errors. **Check**: 1. Verify the server starts manually: ```console $ uvx libtmux-mcp ``` You should see no output (it's waiting for stdio input). Press Ctrl+C to stop. 2. Check your client config points to the right command. Common issues: - `uvx` not in PATH — [install uv](https://docs.astral.sh/uv/getting-started/installation/) - Typo in `"command"` or `"args"` in JSON config - TOML config syntax errors (Codex CLI) 3. Restart your MCP client after config changes. ## Tools fail with "no sessions found" **Symptoms**: `list_sessions` returns empty, other tools can't find targets. **Check**: 1. Is tmux running? ```console $ tmux list-sessions ``` 2. Are you on the right socket? If `LIBTMUX_SOCKET` is set, the server only sees sessions on that socket: ```console $ tmux -L ai_workspace list-sessions ``` 3. Create a session on the expected socket: ```console $ tmux -L ai_workspace new-session -d -s test ``` ## Wrong tmux socket **Symptoms**: Server sees different sessions than expected, or sees nothing. **Cause**: `LIBTMUX_SOCKET` in the MCP config isolates the server to a specific socket. Your personal sessions are on the default socket. **Fix**: Either remove `LIBTMUX_SOCKET` from the config to use the default socket, or ensure sessions exist on the configured socket. ## Pane targeting mismatch **Symptoms**: Tool targets the wrong pane, or "pane not found" errors. **Cause**: Using ambiguous targeting (session name + window name) instead of direct IDs. **Fix**: Use `pane_id` (e.g. `%1`) for unambiguous targeting. Pane IDs are globally unique within a tmux server. Run `list_panes` first to discover IDs. ## Command works in shell but not via MCP **Symptoms**: `send_keys` sends the command but output isn't what you expect. **Check**: 1. **Enter key**: `send_keys` sends Enter by default (`enter=true`). If you're sending a partial command, set `enter=false`. 2. **Special characters**: tmux interprets some key names (e.g. `C-c`, `Enter`). If sending literal text, use `literal=true`. 3. **Timing**: After {toolref}`send-keys`, prefer composing `tmux wait-for -S ` into the shell command and calling {toolref}`wait-for-channel` for deterministic completion. Use {toolref}`capture-since` for repeated observation, and use {toolref}`wait-for-text` or {toolref}`wait-for-content-change` only when waiting on output you do not author. Don't call {toolref}`capture-pane` immediately — the command may still be running. ## Silent startup failure **Symptoms**: MCP client says connected but no tools are available. **Check**: 1. Missing dependency — ensure `fastmcp` is installed: ```console $ uvx libtmux-mcp ``` If using pip install, check: ```console $ python -c "import fastmcp; print(fastmcp.__version__)" ``` 2. Python version — requires 3.10+: ```console $ python --version ``` ## Safety tier blocking tools **Symptoms**: Some tools are missing from the tool list, or return "blocked by safety tier" errors. **Cause**: `LIBTMUX_SAFETY` is set to a restrictive tier. **Fix**: Check the configured tier. Default is `mutating`, which includes most tools. Only `destructive` enables kill commands. See {ref}`safety`. ## How to see logs The MCP server uses Python's `logging` module. To see debug output, set the log level before starting: ```console $ PYTHONUNBUFFERED=1 uvx libtmux-mcp 2>server.log ``` For Claude Desktop on macOS, MCP server logs are at: `~/Library/Logs/Claude/mcp-server-libtmux.log` ---