MCP server guide
libtmux-mcp 0.1.0.alpha.3 · Source
Expose an existing tmux server over MCP stdio. Read snapshots, capture pane output and wait for events, or enable creation, input and shell commands one tool at a time.
API reference · Tools · Guides · RubyGems
Part of libtmux for Ruby. Built on libtmux and libtmux-async.
An MCP client starts libtmux-mcp and talks to it over stdin and stdout. The
official MCP SDK handles the protocol, and bounded Async tasks handle
transport.
Contents — Quick start · Connect a client · Choose the server · The tools · Read · Create · Run · Embed · Leaves out · Support · Documentation
Quick start¶
Install the alpha and its libtmux-mcp executable. It needs Ruby 3.3 or newer
and tmux 3.3a or newer:
$ gem install libtmux-mcp --preThe server borrows a tmux server that is already running. Start one on a socket of its own:
$ tmux -L agent new-session -d -s workAdd the server to Claude Code. Until you enable more tools, it serves
tmux_capabilities and
tmux_snapshot:
$ claude mcp add tmux -- libtmux-mcp --socket-name agent --endpoint devRun by hand, libtmux-mcp waits for input until EOF. libtmux-mcp --help
lists every option.
Connect a client¶
claude mcp add: Claude Code¶
The command in the quick start registers the server with Claude Code. Start the tmux server first, as above.
codex mcp add: Codex CLI¶
Codex CLI takes the same command after --:
$ codex mcp add tmux -- libtmux-mcp --socket-name agent --endpoint devmcpServers: clients that read JSON¶
A client that reads a JSON configuration, such as Claude Desktop’s
claude_desktop_config.json, takes the same command as an mcpServers entry:
{ "mcpServers": { "tmux": { "command": "libtmux-mcp", "args": ["--socket-name", "agent", "--endpoint", "dev"] } }}If the client does not start servers with your shell’s PATH, set command to
the absolute path that command -v libtmux-mcp prints.
Choose which tmux server it serves¶
--socket and --socket-name: pick the daemon¶
Set TMUX_SOCKET to an existing tmux socket. This command borrows that daemon
and serves MCP on stdin and stdout:
$ libtmux-mcp \ --socket "$TMUX_SOCKET" \ --endpoint localUse --socket-name NAME instead of --socket PATH to select a named socket,
as the quick start does:
$ libtmux-mcp \ --socket-name agent- EOF retires the clients the server owns and leaves the daemon running.
--endpointsets the public alias in discovery and resource URIs, and defaults tolocal.
The tools¶
Each tool name links to its page on libtmux.org, with the arguments, limits and
result schema. Repeat --enable-tool for each additional tool.
| Tools | Default | Purpose |
|---|---|---|
tmux_capabilities, tmux_snapshot¶ | Enabled | Discover capabilities and query captured metadata |
tmux_capture, tmux_wait¶ | Disabled | Capture a screen or wait for text/process exit |
tmux_create, tmux_send, tmux_close¶ | Disabled | Create entities, send text/keys and tear down exact targets |
tmux_run¶ | Disabled | Run a script in an explicitly enrolled zsh shell |
- Disabled tools are absent from discovery and denied on direct application calls.
- An unknown tool name is refused when the server starts.
The complete protocol recipe exercises discovery, snapshots, default denial, enabled mutations, cancellation and EOF cleanup through actual pipes, including the installed executable.
Read the layout and the screen¶
tmux_snapshot: the layout, in pages¶
- Pages come from one immutable capture, and a cursor continues that capture.
- An expired, evicted or foreign cursor returns an error. The server never substitutes a fresh listing.
tmux_capture: a pane’s screen¶
Enable the screen tools together:
$ libtmux-mcp \ --socket "$TMUX_SOCKET" \ --enable-tool tmux_capture \ --enable-tool tmux_wait- It reads a bounded screen and keeps its line endings. Bytes that are not valid UTF-8 come back base64-encoded.
- With tracking, a call returns a cursor, and later calls return a splice against that exact captured state. A cursor from another server generation or from a respawned pane is refused.
- It refuses a pane whose effective
after-capture-panehook is not empty, including inherited entries. - On tmux 3.3a to 3.4 the hook is checked before the capture command runs, so keep hook configuration stable while observing.
tmux_wait: wait for text or exit¶
- It waits for screen text, or for the pane process to exit. Screen text comes from control events on tmux 3.7 and newer, and from polling the pane every 50 ms on older daemons, which take no control client. Process exit uses process descriptors.
- Wrapped rows are joined, so text that the pane width split across rows still matches.
- If the pane, or the tmux server holding it, ends before the text appears,
the call fails with
pane_died,delivery: observedand the last rows it read in the error’scapture. If only the control stream ends while the pane lives, such as after the client detaches, the failure isobservation_lost. - A cancelled or expired wait closes its observers and never signals the pane program.
- Process tracking needs a native identity backend: Linux peer pidfds with matching PID namespaces, or Darwin kqueue. Without one, the call is refused.
Resource templates: the same data as URIs¶
tmux_metadata,
tmux_metadata_page and
tmux_screen expose metadata pages and pane screens
under endpoint and generation URIs, with the same policy and limits as their
tools.
- The server does not advertise resource subscriptions.
- Snapshots, cursors and references has the full contract.
Create, type and close¶
tmux_create: new sessions, windows and panes¶
Enable the three tools together:
$ libtmux-mcp \ --socket-name agent \ --enable-tool tmux_create \ --enable-tool tmux_send \ --enable-tool tmux_close- It takes argument arrays as
argv. - Results carry the references tmux returned.
tmux_send: literal text or named keys¶
- Text and keys are separate variants.
- It reports
dispatch_only: the input was dispatched, and the command has not necessarily finished. - After a cancellation, the effect stays unknown and is not reported as none.
tmux_close: an exact target¶
- It takes an exact session, window or pane reference, and never selects by name.
Run a script in a pane¶
tmux_run: authored commands in an enrolled zsh¶
tmux_run runs a POSIX script in a pane’s interactive zsh
5.9. It needs both the tool and an enrolled pane.
List the pane IDs of the server:
$ tmux -L agent list-panes -a -F '#{pane_id}'%0Enable the tool and write a setup file for one exact pane:
$ libtmux-mcp \ --socket-name agent \ --enable-tool tmux_run \ --enroll-pane %0=enroll.zshThen, in that pane’s zsh, run source enroll.zsh. A relative path resolves
from the directory where the server starts.
--enroll-panerequires--enable-tool tmux_run, and each pane and each file may appear once.- The server writes a private file (mode
0600) and never types into the terminal. It refuses an existing file or a symlink. - Setup files expire.
--enrollment-timeout SECONDSsets the lifetime, at most 300. - EOF retires pending enrollment and removes only the files the server wrote.
- A call takes an exact pane
target, a POSIXscript, and optionalstdout_limitandstderr_limitbyte counts. - The script runs with the shell’s working directory and exported environment.
- Stdout and stderr come back separately, as UTF-8 or base64.
- A nonzero exit or a signal is a completion result. Output over its limit is an error with completion unobserved, and never a truncated success.
- Cancelling a run does not prove that its child processes stopped.
Embed it in Ruby¶
Require libtmux/mcp. Imports start no tmux process, scheduler or MCP server.
The execution guide covers result and ownership
boundaries.
LibTmux::MCP::Application: serve from your own program¶
Application borrows an application-owned
LibTmux::Async::Server. Its
sdk_server supplies the SDK server that
StdioTransport consumes:
application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example")sdk = application.sdk_serverinput, client_input = IO.pipeclient_output, output = IO.pipetransport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output)runner = parent.async { transport.run }The complete program runs this excerpt inside
an Async task with an open LibTmux::Async scope. It sends real
JSON-RPC messages through the pipes, checks the replies, and prints
PASS mcp_protocol.
Application#invite_shell: enroll a shell from Ruby¶
Call invite_shell with a pane reference, timeout: and expires_in:, then
pass the invitation to accept_shell.
- The invitation’s
shell_argumentsmake up the setup command to source in that pane’s zsh. timeout:bounds acquisition, andexpires_in:sets the enrollment lifetime.- The application owns invitations and accepted connections until
close. Direct enrollment calls enforce tool policy.
What it leaves out¶
- Tools you have not enabled. Disabled tools are absent from discovery, and a direct application call is denied. See The tools.
- Resource subscriptions. The server does not advertise them. See the resource templates.
- Typing into your terminal. The server writes a private setup file and
never types into a pane’s terminal to enroll it. See
tmux_run. - A guaranteed stop. Cancelling a run does not prove that its child
processes stopped. See
tmux_run.
Support and status¶
Alpha. APIs may change between releases.
- Ruby 3.3 or newer. The gem depends on
libtmuxandlibtmux-asyncat the same version, themcpSDK 1.5,digestandoptparse. - tmux 3.3a or newer. The server refuses an older daemon when it starts, before it serves any tool, and prints the reason and the remedy on stderr.
- A daemon that changes while it runs. A tool call returns an
unsupportedresult withdelivery: not_sent. The result names the floor and says to upgrade tmux and restart the server. - The
screen_containswait. It uses control events on tmux 3.7 and newer and polls the pane screen on older daemons, which take no control client. - Linux, including WSL, and macOS. The core compatibility workflow runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a through 3.8 on Ubuntu 24.04. macOS is supported on a best-effort basis, and CI coverage for it is highly limited.
Documentation¶
| Reference¶ | The embedding types and methods |
| Tools¶ | One page per tool, with arguments, limits and result schema |
| Guides¶ | Connect a client to a private tmux server and select its tools |
| Resources¶ | Metadata pages and pane screens as URIs |
| Public behavior¶ | The application, transport and command |