A tmux server, exposed to a model over the Model Context Protocol.
Point Claude Code, Claude Desktop, Codex, or any MCP client at a tmux socket and it can find its way around, read what a pane is showing, run a command and wait for it, and build a whole session with typed operations.
io.github.libtmux:libtmux-mcp — on Maven Central.
Alpha. Tool names and shapes will change without notice.
$ ./gradlew :libtmux-mcp:installDistThat writes a launcher at libtmux-mcp/build/install/libtmux-mcp/bin/libtmux-mcp.
An MCP client starts it as a subprocess and speaks JSON-RPC over its stdin and
stdout.
Commands below that use libtmux-mcp assume that launcher's bin directory is
on PATH; otherwise substitute the full path.
| flag | what it chooses |
|---|---|
--socket <path> |
which tmux server, by socket path |
--socket-name <name> |
which tmux server, by name under tmux's own directory |
--tmux <binary> |
which tmux to run |
Without a socket flag, the launcher pins the named socket libtmux-mcp. When
that socket does not exist, it starts tmux with the package's minimal
configuration and enables all four toolsets. A process that finds an existing
or explicitly selected server cannot prove how it was configured, so teardown
is omitted from its default surface.
| environment | what it chooses |
|---|---|
LIBTMUX_SOCKET |
one socket name, mutually exclusive with the path |
LIBTMUX_SOCKET_PATH |
one absolute socket path |
LIBTMUX_TMUX_CONFIG |
one nonempty absolute tmux configuration path |
LIBTMUX_TOOLSETS |
any unordered subset of inspect,manage,execute,teardown |
LIBTMUX_TOOLS |
exact tool names to add |
LIBTMUX_EXCLUDE_TOOLS |
exact tool names to remove last |
The surface is frozen before tmux opens. An empty LIBTMUX_TOOLSETS value
selects no toolset; unknown names and empty comma-separated elements stop
startup. Existing launcher configurations must migrate:
--safetyandLIBTMUX_SAFETYare retired and fail startup. The oldreadonly,mutating, anddestructivevalues map toinspect;inspect,manage,execute; and all four toolsets, respectively. UseLIBTMUX_TOOLSandLIBTMUX_EXCLUDE_TOOLSfor exact exceptions.--watchandLIBTMUX_WATCHare retired and fail startup. MCP no longer sends dynamic resource notifications. Usewait_for_text,wait_for_channel, orcapture_since; Java applications can useControlClient.
See Watching, instead of polling for bounded waits and Safety for the capability and trust boundary.
$ claude mcp add tmux -- /absolute/path/to/libtmux-mcp --socket /tmp/my-app/s$ codex mcp add tmux -- /absolute/path/to/libtmux-mcp --socket /tmp/my-app/sAdd to claude_desktop_config.json:
{
"mcpServers": {
"tmux": {
"command": "/absolute/path/to/libtmux-mcp",
"args": ["--socket", "/tmp/my-app/s"]
}
}
}It speaks JSON-RPC over stdin and stdout, so anything implementing MCP's stdio transport can launch it. There is nothing to configure but the path and the flags.
You: What's running in my tmux panes, and is the test suite still going?
Agent: Three panes.
%0is a shell,%1is runningpytest,%2is a shell in thelogswindow. Reading%1now — it is ontests/test_auth.py, 84 passed so far, nothing failed yet. Want me to wait for it to finish?
The agent reads and drives the terminal directly. No pasting output back and forth, no switching windows to check on something long-running.
Target by id, never by position. A model works from a listing it read some
turns ago, and indexes move as neighbours come and go. %1 is a pane, @1 a
window, $1 a session. A bare 1 is refused, because tmux would read it as an
index and act on a real but unintended pane.
Wait, do not poll. Every wait is a tool. An agent that sends a command and then reads the pane repeatedly to guess whether it finished spends a call per look and still cannot tell a finished command from a stalled one.
Reads are bounded and say so. Every read is capped, keeps the newest lines, and reports what it dropped. An answer silently shortened reads as a complete one, which is how a model concludes a build printed nothing.
The complete frozen inventory below is generated from the code registry.
| toolset | public tools |
|---|---|
inspect |
list_sessions · list_windows · list_panes · get_server_info · get_session_info · get_window_info · get_pane_info · capture_pane · capture_since · snapshot_pane · search_panes · find_pane_by_position · wait_for_text · get_tmux_variables · show_option · show_environment · show_hooks · call_read_tools_batch |
manage |
rename_session · rename_window · select_window · select_pane · select_layout · resize_window · resize_pane · move_window · swap_pane · set_pane_title · wait_for_channel · signal_channel · set_mouse_enabled · set_history_limit |
execute |
create_session · create_window · split_window · respawn_pane · run_shell_command · send_keys · send_keys_batch · paste_text · set_synchronize_panes |
teardown |
clear_pane_scrollback · kill_pane · kill_window · kill_session |
Existing callers from earlier alpha releases must also migrate tool names:
tmux_capture_pane,tmux_capture_since,tmux_list_panes,tmux_list_sessions,tmux_list_windows,tmux_paste_text,tmux_resize_pane,tmux_search_panes,tmux_select_layout,tmux_send_keys,tmux_show_environment,tmux_show_hooks,tmux_signal_channel,tmux_wait_for_channel, andtmux_wait_for_textretain their suffix withouttmux_.list_panesno longer accepts a filter; filter its bounded metadata client-side. Usesearch_panesonly for displayed text.tmux_run,tmux_new_session,tmux_new_window,tmux_split_pane, andtmux_show_optionsbecomerun_shell_command,create_session,create_window,split_window, andshow_option, in the same order.tmux_whoamisplits intoget_server_infoand the caller marker fromlist_panes.tmux_renamebecomesrename_sessionorrename_window;tmux_selectbecomesselect_windoworselect_pane;tmux_killbecomeskill_session,kill_window, orkill_pane. Server termination is not exposed.tmux_set_optionhas no generic equivalent. Migrate supported uses toset_mouse_enabled,set_history_limit,set_synchronize_panes, orset_pane_title.tmux_apply_workspacebecomes explicitcreate_session,create_window,split_window, andselect_layoutcalls followed byrun_shell_command,send_keys, orpaste_text.tmux_list_servers,tmux_list_clients, andtmux_drain_channelhave no direct equivalents. Each process pins one server, described byget_server_info;list_sessionsmarks attached sessions but exposes no client details; stale channel signals cannot be drained through MCP.
| tool | gives back |
|---|---|
get_server_info |
the pinned server's identity, version, and current state |
list_sessions |
sessions, with stable $id values |
list_windows |
windows, with the @id other tools take |
list_panes |
panes, with the %id other tools take |
get_session_info, get_window_info, get_pane_info |
one target's metadata |
find_pane_by_position |
one pane at a named window corner |
| tool | gives back |
|---|---|
capture_pane |
what a pane shows now, plus a cursor |
capture_since |
only what is new since a cursor, plus the next cursor — finished lines only, so half a line is never handed over as though it were the whole of one |
snapshot_pane |
bounded content and pane metadata together |
search_panes |
which panes show bounded plain text or a bounded RE2 pattern |
show_environment, show_hooks, show_option |
selected configuration state |
get_tmux_variables |
a capped set of validated variable names |
call_read_tools_batch |
up to sixteen typed inspect calls with full nested MCP results when they fit |
list_panes reads metadata — what is running, and where. search_panes
reads content — what is displayed. "Which pane mentions the error" is a search.
Copy mode is an attached-client interface, not a prerequisite for reading pane
text. Set history: true on capture_pane or snapshot_pane for bounded
scrollback, use search_panes to locate displayed text, and continue from a
cursor with capture_since instead of entering or cancelling a person's mode.
Key sends resolve the target's current effective synchronized cohort and refuse
the whole send when one configured recipient is modal, dead, the caller pane,
or displayed by a terminal client. Paste applies the same guard to its target
only, while framed shell runs require one guarded effective recipient at both
preflights. Each preflight reads pane and client state in one snapshot; it is
still an observation rather than a delivery receipt. resolved_pane_ids
reports configured membership rather than confirmed recipients or delivery.
Batch rows retain the nested MCP envelope rather than flattening its text or
structured content. The complete JSON-RPC response, including line framing, is
capped at 1,000,000 bytes. A row that would cross that boundary remains in
order with result: null and
resultTruncated: true; the outer result sets truncated and reports the
removed byte count in truncatedBytes.
A serialized request ID may use at most 524,288 bytes; a larger ID returns an
id: null invalid-request error before any tool runs.
| tool | for |
|---|---|
run_shell_command |
a command you wrote — sends it, waits, returns output and exit status in one call |
wait_for_text |
output you did not start: a dev server, a daemon, someone else's build |
wait_for_channel |
anything you can compose ; tmux wait-for -S name into |
signal_channel |
the other end of that |
rename_session · rename_window · select_window · select_pane ·
select_layout · resize_window · resize_pane · move_window · swap_pane ·
set_pane_title · set_mouse_enabled · set_history_limit · create_session ·
create_window · split_window ·
respawn_pane · send_keys · send_keys_batch · paste_text ·
set_synchronize_panes
clear_pane_scrollback · kill_pane · kill_window · kill_session. The kill
tools refuse to end the pane this conversation is running through, or one of its
containers, unless confirm_self is set. No tool ends the tmux server itself.
An agent driving a terminal spends most of its time waiting, and MCP gives it no sleep primitive — so a wait that is not a tool becomes a polling loop in the agent's turn, where it has no ceiling at all.
You wrote the command. One call, and the answer is a number rather than an inference:
{"name": "run_shell_command",
"arguments": {"pane_id": "%1", "command": "pytest -q", "timeout": 120}}{"outcome": "SIGNALLED", "exit_status": 1, "output": ["...", "1 failed, 84 passed"]}outcome is SIGNALLED when the command finished, TIMED_OUT when it was still
running at the deadline, and SERVER_GONE when tmux itself died underneath the
wait. Those mean different things, and only the first makes exit_status
meaningful — tmux reports a server that died under a waiter as a successful
wake, so "it worked" is never the answer on its own.
Completion runs inside the pane's trusted POSIX shell: an inherited inner
subshell contains the authored command, while an outer trap emits its status and
signals through one absolute tmux client and the server's resolved -S socket.
That trap is armed for interrupt and terminate as well as exit. A command still
running at the deadline keeps the pane, so nothing else can type into the line it
occupies; send_keys with only stop keys — C-c, C-\ — is let through that,
because it is what ends the command rather than competing with it, and it is what
the timeout's own note tells you to send. Ordinary output aliases and functions are tolerated; pre-existing
functions named trap, eval, exit, or exactly like that resolved client are
outside this boundary. Marker display-message calls honor the selected trusted
server's command aliases and hooks.
ASCII control characters and DEL are refused in executable and socket routes
before those values can enter framing.
You did not write it. Always pass stop:
{"name": "wait_for_text",
"arguments": {"pane_id": "%2", "patterns": ["Listening on"],
"stop": ["error:", "EADDRINUSE"], "timeout": 60}}Without stop, a run that fails in the first second is still waited on until the
deadline, and what comes back is a timeout instead of the error. Patterns are
plain text unless you pass regex — a model asking for [FAILED] means those
eight characters, not a character class.
Text already on the screen when a cursorless call starts is never reported as a
fresh match — it did not just happen — but it is not hidden either: the outcome
comes back PRESENT_AT_ENTRY rather than MATCHED, with the text included, so a
call made moments after the output landed does not read as a timeout with
nothing in it. Pass the returned cursor to a later call to watch only for what
comes after.
Every wait is capped (30 s by default, 2 minutes hard) and reports the ceiling it actually enforced. The cap protects the agent's turn, not the connection: a tool call that blocks does not stop this server answering anything else.
A client's request deadline is separate. The Java SDK 2.0.1 client defaults to 20 seconds, so configure it above any longer wait you request. With that SDK, cancelling or timing out abandons the answer but does not stop the synchronous handler or undo tmux changes it already dispatched.
The MCP surface no longer keeps a hidden control client or advertises dynamic resource subscriptions. Instead it gives an agent three bounded ways to wait without rereading a screen in a loop.
wait_for_textwatches one pane for wanted or stop patterns and returns the output that arrived during the call.wait_for_channellets tmux itself block until a cooperating command signals a channel.capture_sincetakes an opaque cursor and returns only finished lines added since that point.
{"name": "capture_since",
"arguments": {"pane_id": "%1", "cursor": "<cursor from capture_pane>"}}The Java library still exposes control-mode subscriptions directly when an application genuinely needs a long-lived event stream; the streaming guide covers that lower-level API.
Four unordered toolsets replace the old safety ceiling: inspect, manage,
execute, and teardown. They are capabilities, not increasing levels. Ask for
the independent sets a client needs, then add or exclude exact tool names.
$ LIBTMUX_TOOLSETS=inspect,manage libtmux-mcp --socket-name my-project$ LIBTMUX_TOOLSETS= LIBTMUX_TOOLS=capture_pane,wait_for_text libtmux-mcp \
--socket-name my-projectThe same immutable selection governs both listing and calls. A hidden tool is
not callable, exclusions win, and an aggregate-only
call_read_tools_batch retains its eligible nested inspect operations unless
they are excluded explicitly.
Filtering the catalog does not confine effects. Every call runs with the tmux user's authority; pane input can reach a shell, and reads may return terminal content, process environment, or configured commands. Use a separate OS account, socket permissions, or a container when effects must be contained.
Every tool carries MCP's own effect hints — readOnlyHint, destructiveHint,
idempotentHint, and openWorldHint — plus its full native capability row.
Those claims remain conservative when the selected server's configuration is
unknown.
There is one resource: tmux://capabilities. It is static for the process
lifetime and reports the effective tool surface, its selection provenance, the
pinned tmux connection, and the same capability row published on every tool.
{"method": "resources/read", "params": {"uri": "tmux://capabilities"}}Earlier dynamic resource routes migrate to typed reads:
tmux://serverbecomesget_server_info; the static capability resource adds connection and selection provenance.tmux://sessionsandtmux://panesbecomelist_sessionsandlist_panes.tmux://sessions/{session_name}andtmux://panes/{pane_id}becomeget_session_infoandget_pane_info.tmux://panes/{pane_id}/contentbecomescapture_pane,snapshot_pane, orcapture_since. Usewait_for_textwhen the old subscription was waiting for a terminal condition.
The removed prompts remain useful as explicit tool workflows:
run_and_waitbecomes onerun_shell_commandcall.watch_until_readyuseswait_for_text, orsnapshot_panefollowed bycapture_sincewhen output must be carried across turns. After a timeout, continue from the returned cursor instead of restarting the observation.find_the_panecomposeslist_panes,search_panes,find_pane_by_position, andget_pane_infoas needed.build_workspacecomposes the create, split, layout, title, selection, and execution tools documented below.clean_up_safelystarts withlist_panesto identify the MCP pane andlist_sessionsto identify attached sessions, then uses the specific pane, window, or session teardown tool. An attached session may have a person watching it, so apparently abandoned state may still be live.
Earlier live completion/complete suggestions for pane_id have no direct
replacement. Call list_panes, then pass the exact id through the typed tool
schema.
There are deliberately no dynamic hierarchy or pane-content resources, templates, subscriptions, prompts, or live completion routes. State belongs in typed tools, while the resource answers the one question a client should not have to infer: what this frozen process can reach and disclose.
A server with forty panes gives a model forty things to reason about. MCP now
does two narrower kinds of filtering: startup selection removes tools the client
does not need, and search_panes narrows terminal content without returning
every pane capture.
{
"name": "search_panes",
"arguments": {
"pattern": "FAILED|ERROR",
"regex": true,
"max_matches_per_pane": 5,
"max_lines": 50
}
}One call examines at most 200 panes, 20,000 lines, 1,000,000 UTF-8 bytes, and five seconds of matching work. The answer says when a pane, line, byte, time, or result limit stopped it. Pattern count and UTF-8 size are rejected before tmux opens; regular expressions use the bounded RE2 dialect.
For several different observations, batch exact inspect calls instead of asking for one broad untyped projection:
{"name": "call_read_tools_batch", "arguments": {"operations": [
{"tool": "list_panes", "arguments": {}},
{"tool": "show_option", "arguments": {"scope": "server", "name": "status"}}
]}}The Java library's richer query API and its versioned
filter-expr-v1.schema.json
remain available to application code. MCP does not accept that open expression
document: its authoritative schemas expose only the bounded inputs above, and a
field not in those schemas never reaches tmux.
A workspace may still begin as the same readable shape tmuxp and
libtmux-workspace use:
session_name: api-work
windows:
- window_name: editor
panes:
- nvim
- window_name: services
layout: even-horizontal
panes:
- npm run dev
- docker compose logs -fThe MCP server no longer accepts that whole document as one opaque mutation. Creation stays explicit and typed: create the detached session, retain the IDs it returns, then create and split windows and choose a layout.
{"name": "create_session", "arguments": {"session_name": "api-work",
"window_name": "editor"}}{"name": "create_window", "arguments": {"session_id": "$1",
"window_name": "services"}}{"name": "split_window", "arguments": {"pane_id": "%2",
"direction": "right", "percent": 50}}{"name": "select_layout", "arguments": {"window_id": "@2",
"layout": "even-horizontal"}}These tools accept no command or environment payload. Start the configured
process first, then use run_shell_command, send_keys, or paste_text for
workload input. If a later step fails, the earlier typed results still identify
exactly what exists and what can be removed.
The tools are separate from the protocol wiring, because what a tool does to tmux is worth testing against real tmux and attaching it to a transport is not.
// Given: ServerConfig config
Server server = Server.open(config);
// Serves on stdin and stdout, reading until the client closes the stream.
TmuxMcpServer.overStdio(server);TmuxMcpServer.serving(server, transport) takes an MCP transport of your own,
which is how this is tested. It registers the same startup-frozen manifest and
single static capability resource as the stdio launcher.
The removed serving(server, ceiling, transport) and watching-boolean overloads
become serving(server, transport). It reads selection from the process
environment; Java applications own ControlClient subscriptions directly.
dependencies {
implementation(platform("io.github.libtmux:libtmux-bom:0.0.1-alpha.14"))
implementation("io.github.libtmux:libtmux-mcp")
}Depends on libtmux and the MCP Java SDK. The sibling
libtmux-jackson and
libtmux-workspace modules remain available to Java
applications that need filter documents or declarative workspace building.
- MCP guide — the design, and why each tool is shaped as it is
- Filtering guide — the expression model behind the wire format
- Streaming guide — the lower-level control client used by Java applications
libtmux— the library underneath- Root README