mcp
Serve one tmux server to Model Context Protocol clients, built on the
tmux module and the Go MCP SDK.

$ go install github.com/libtmux/libtmux-go/mcp/cmd/libtmux-mcp@latest
Alpha software. Releases carry an -alpha prerelease tag and the API is not
settled. Pin an exact version.
Contents — Quick start ·
Connect another client ·
Choose which tmux it drives ·
See an agent work ·
Run work and wait for it ·
The tools ·
Limiting what a client can do ·
Settings · The pane it runs in ·
Known limits · Troubleshooting ·
An agent-shaped program ·
Embedding it · Packages ·
What it leaves out ·
Support and status · See also
Quick start
You need Go 1.26 or newer and tmux 3.3a or newer on PATH. The install
command above puts libtmux-mcp in GOBIN when that is
set, and in $(go env GOPATH)/bin otherwise.
An MCP client launches it as a subprocess and speaks to it over stdin and
stdout, so nothing stays running. If your shell cannot find libtmux-mcp, add
that directory to PATH.
The Python server installs a command of the same name. Run
command -v libtmux-mcp to see which one comes first, and
libtmux-mcp -version to name this one.
claude mcp add: tell Claude Code about it
$ claude mcp add tmux -- libtmux-mcp
Claude Code starts each server it lists, to check that it connects:
It answers without a client in the way, and prints something like this:
libtmux-mcp doctor
tmux: 3.6 (requires 3.3a or newer)
socket: /tmp/tmux-1000/libtmux-mcp (from default dedicated socket)
holds: nothing — no tmux server is running on that socket
(not a fault; tmux starts one when something asks it to)
caller: not running inside a tmux pane
Then ask the client for something only tmux can do, such as "create a tmux
session called demo and tell me what panes it has".
Connect another client
Each client takes the same command and the same flags. Connect an MCP
client on libtmux.org covers startup and shutdown.
codex mcp add: Codex CLI
$ codex mcp add tmux -- libtmux-mcp
gemini mcp add: Gemini CLI
$ gemini mcp add tmux libtmux-mcp
claude_desktop_config.json: Claude Desktop
Claude Desktop starts its servers without your shell's PATH, so give it the
path command -v libtmux-mcp prints. Flags go in args and the environment in
env:
{
"mcpServers": {
"tmux": {
"command": "/path/to/libtmux-mcp",
"args": ["-socket-name", "my-application"],
"env": {"LIBTMUX_TOOLSETS": "inspect,manage,execute"}
}
}
}
Choose which tmux it drives
The choice is made once, when the server starts. A client cannot change it
afterwards. Settings lists the other flags, and
the contract has the precedence rules.
With no flag, the server drives a tmux server of its own, on a socket named
libtmux-mcp, started with a minimal configuration the package ships. An agent
given that cannot touch the sessions you already have.
Once it has made a session, watch it work from another terminal:
$ tmux -L libtmux-mcp attach
To let it drive the tmux you use every day, name that socket. tmux calls its
usual one default:
$ claude mcp add tmux -- libtmux-mcp -socket-name default
A workspace that tmux-workspace load -L project built
lives on the socket project. To let a client read it and change nothing:
$ claude mcp add tmux \
-e LIBTMUX_TOOLSETS=inspect \
-- libtmux-mcp -socket-name project
-socket-path names the socket by its absolute path, and -binary names the
tmux executable. -doctor reports what a choice selects:
$ libtmux-mcp \
-doctor \
-socket-path /tmp/my-application.sock
The environment makes the same choices, which is how a client's configuration
sets them:
$ LIBTMUX_SOCKET=my-application libtmux-mcp -doctor
See an agent work
These are the calls examples/agent-workflow makes
to run work in a pane beside its own, and what came back. Cursor tokens are
left out, and each reply shows only the fields that answer the call:
→ split_window {"direction":"right","pane_id":"%0","percent":40}
← {"paneId":"%1"}
→ paste_text {"enter":true,"pane_id":"%1","text":"if sleep 1; then printf 'agent-workflow-%s\\n' ready; else printf 'agent-workflow-%s\\n' failed; fi"}
← {"bytes":97}
→ wait_for_text {"pane_id":"%1","patterns":["agent-workflow-ready"],"stop":["agent-workflow-failed"],"timeout":30}
← {"found":true,"matched":"agent-workflow-ready","outcome":"matched"}
→ run_shell_command {"command":"printf 'bounded work\\n'","pane_id":"%1","timeout":30}
← {"exit_status":0,"output":["bounded work"],"timed_out":false}
The example prints these with -trace, and its page
has the command. The agent drives tmux directly, so nothing is copy-pasted out
of a terminal, and waiting for a server to come up is one call.
Run work and wait for it
For one tmux send-keys, shelling out is shorter. The server earns its keep
once the agent has to wait, watch, inspect, or leave your
terminal alone.
A dev server printing its port, a test run finishing, or a deploy log settling
turns a shell-out into a polling loop. That loop reads the shell's echo of its
own command and reports success before anything happened.
split_window: make room beside your pane
split_window adds a pane and returns its id. A new pane is
given time to start, so run_shell_command can follow at
once.
The trace splits %0 to the right with "percent":40 and gets back
{"paneId":"%1"}.
wait_for_text: wait for what a pane prints
wait_for_text watches a pane and returns when a pattern
appears, or when a stop pattern appears first.
capture_since returns what a pane wrote since the last
look.
The trace waits for agent-workflow-ready and names agent-workflow-failed as
the stop pattern. It gets "outcome":"matched".
A wait longer than the ceiling is shortened, and the reply says so. The
ceiling is LIBTMUX_MCP_WAIT_MAX_SECONDS.
run_shell_command: run a command and read its exit status
run_shell_command waits for the command and returns its
exit status and output, without reading the screen at all.
send_keys and paste_text send input and return.
The trace runs a command that prints "bounded work" and gets back
"exit_status":0.
run_shell_command refuses a pane linked with others by
synchronize-panes, because one exit status cannot describe several panes.
Keep reads small
Other parts of the design exist because an agent's context and its turn are
the scarce resources.
- Reads are bounded. Captures keep the newest lines asked for and say what
they left out. Searches have fixed limits, which
the contract lists.
- One call can make several reads.
call_read_tools_batch answers a list of inspect
calls with one row each.
- Checking is not capturing. The listing tools report what exists without
reading what any pane shows.
snapshot_pane returns a pane's
metadata and content together.
The tools come in toolsets. A toolset is a named group
of tools, and it is what an operator grants or withholds:
inspect reads tmux and what panes show, and changes nothing.
manage rearranges what exists: it renames, resizes, selects, and moves.
execute creates sessions, windows, and panes, and sends input to them.
teardown kills sessions, windows, and panes, and clears scrollback.
| Toolset | Tools |
|---|
inspect | call_read_tools_batch, capture_pane, capture_since, find_pane_by_position, get_pane_info, get_server_info, get_session_info, get_tmux_variables, get_window_info, list_panes, list_sessions, list_windows, search_panes, show_environment, show_hooks, show_option, snapshot_pane, wait_for_text |
manage | move_window, rename_session, rename_window, resize_pane, resize_window, select_layout, select_pane, select_window, set_history_limit, set_mouse_enabled, set_pane_title, signal_channel, swap_pane, wait_for_channel |
execute | create_session, create_window, paste_text, respawn_pane, run_shell_command, send_keys_batch, send_keys, set_synchronize_panes, split_window |
teardown | clear_pane_scrollback, kill_pane, kill_session, kill_window |
TOOLS.md has every tool's arguments and what it returns, and the
documentation site gives each tool the page linked above. That reference is
for searching, which is why it is not here.
Three behaviours matter before you grant execute:
- Input goes where tmux sends it. With
synchronize-panes on, tmux types
into every pane of the window. send_keys reports the linked
panes before it sends.
- A new pane is given time to start. Input tools wait for its shell to
reach the prompt, so
split_window then
run_shell_command works.
- Copy mode is left to the person. Reads never take over an attached
view, and
get_pane_info reports when a pane is in a mode.
The contract has the rest of each.
Limiting what a client can do
It takes toolset names separated by commas, in any order. Without it the
server grants inspect, manage, and execute.
teardown joins that default only when this process started the tmux server
on the libtmux-mcp socket. Everywhere else, ask for it by name.
- A value that is set and empty grants no toolset, and there is no
none
keyword.
To see the effect without a client, list what the server offers with one
toolset:
$ LIBTMUX_TOOLSETS=inspect libtmux-mcp -tools
A client gets the same selection from the environment it starts the server
with:
$ claude mcp add tmux \
-e LIBTMUX_TOOLSETS=inspect \
-- libtmux-mcp -socket-name default
LIBTMUX_TOOLS adds tools by name to what the
toolsets grant, and
LIBTMUX_EXCLUDE_TOOLS then removes
tools by name. Excluding a tool also removes it from what
call_read_tools_batch may call.
This lists the default tools without send_keys and
send_keys_batch:
$ LIBTMUX_EXCLUDE_TOOLS=send_keys,send_keys_batch libtmux-mcp -tools
A name that matches no tool, or an empty item in either list, stops the
server at startup, before it opens tmux.
Choosing tools shapes what a client is offered. Every call runs with your
user's authority. The contract covers the variables
from earlier releases.
It prints the tools the server would advertise: each one's toolset, which
processes it can reach, and a sentence on what it does. Give it the flags and
environment the client gives the server, because they decide the listing.
Each tool describes what it reaches and changes, under
_meta["com.git-pull.libtmux-mcp/capability"]. The server's one resource,
tmux://capabilities, reports the same descriptions beside the selection in
force. The contract lists them.
Settings
Flags choose the tmux server once, at startup:
| Flag | Meaning |
|---|
-socket-path | an explicit socket path; when set, it wins over everything else |
-socket-name | a tmux socket name; when set, it wins over both socket variables below |
-binary | the tmux executable; empty uses LIBTMUX_TMUX_BIN, then finds tmux on PATH |
Other flags answer a question and exit, which is what a config entry that
will not start needs:
| Flag | Answers |
|---|
-version | which build this is |
-tools | the tools and toolsets a client would see |
-doctor | which socket it reaches, what is on it, and whether it is running inside that tmux itself |
The environment sets the rest:
- A flag wins over its variable, and everything is fixed when the server
starts. The names match the Python server's.
- The tmux executable is resolved once. A bad path fails at startup. A tmux
server that is not running is fine, because tmux starts one on demand.
- A long wait is shortened. The reply says so in
effectiveTimeoutSeconds
and timeoutClamped.
The contract has the full text.
The pane it runs in
isCaller: knowing its own pane
Every pane the server reports carries isCaller: true for the pane it runs
in, false for any other, and null when the server is not in a pane at all.
get_server_info answers the same question directly.
The server reads TMUX_PANE and TMUX first. When they say nothing, it finds
the pane whose process is one of its own ancestors.
Asking before it types into your terminal
A write to the pane with isCaller true asks you first, through MCP's
elicitation request: a question the client puts to you on the server's behalf.
- Declining fails the call, and a client that cannot ask is refused.
- A batch asks the same way, and declining stops the batch there.
- With
synchronize-panes on, the question covers every pane the input would
reach.
The contract has the rest.
Known limits
- Every tool is annotated as destructive.
list_sessions
included, each carries destructiveHint=true and no readOnlyHint. Classify
by tmuxEffects in each tool's _meta.
tools/list is large. More than half of it is the _meta
description. A client that wants a smaller reply can ignore _meta.
- Some tools spell results differently.
run_shell_command, send_keys,
paste_text, and
clear_pane_scrollback return fields such as
exit_status; the rest return fields such as paneId.
- An empty
TMUX counts as set. Input tools refuse when TMUX or
TMUX_PANE is set to nothing. Unset them instead.
- Before tmux 3.6, killing one session can end the server. tmux 3.6 and
later move the server's connection to a session that remains.
The contract gives the reason for each, and
PARITY.md covers the result spellings.
Troubleshooting
Ask the server first. -doctor answers most of what follows without a client
in the way:
$ libtmux-mcp -doctor -socket-name default
- The client shows no tmux tools. The server never started. Run the
command from your client's config by hand.
- Tools are missing.
LIBTMUX_TOOLSETS,
LIBTMUX_TOOLS, or
LIBTMUX_EXCLUDE_TOOLS shaped the
listing. -tools prints what a client receives.
- It sees none of your sessions. Without
-socket-name it drives a tmux
server of its own. -doctor names the socket.
- It reaches the wrong tmux. A client's environment is not your shell's,
so
TMUX_TMPDIR from your profile may be unset for it.
- Sessions you can see are reported as nothing there. Compare the tmux
version
-doctor prints with tmux -V, and point -binary at the right one.
- A command works in your shell but not through a tool.
show_environment reports what a new pane would inherit.
The contract explains each.
An agent-shaped program
examples/agent-workflow is the whole loop in one
file. It finds its own pane, splits one off, starts work there and waits for
what it prints, runs a command and reads its exit status, then reads back what
it built.
It lives in the repository, outside the module you installed, so run it from a
clone:
$ git clone https://github.com/libtmux/libtmux-go.git
$ go -C libtmux-go/mcp run ./examples/agent-workflow -socket-name demo
On a socket with nothing on it, it starts a session there and prints:
no tmux server on that socket yet; starting one
not running inside this tmux server; using active pane %0
split into %1
2 panes in this window while the work runs
the work printed agent-workflow-ready
the command exited 0 and printed:
| bounded work
the window now holds 2 panes: %0 %1
The client and server are joined in memory, so it is one program. The tool
names, schemas, and results are the ones a client sees over stdin and stdout.
Clean up with tmux -L demo kill-server.
Embedding it
mcp.NewServer: serve one tmux server from your own program
mcp.Run serves a tmux server over standard input and output,
which is all the libtmux-mcp command does. For another transport, build an
Instance and connect it yourself.
This block comes from the package's own example test. It connects one end of
the SDK's in-memory transport pair and gives the other end to a client:
instance, err := tmuxmcp.NewServer(target)
if err != nil {
return nil, nil, err
}
serverSession, err := instance.Connect(
ctx, tmuxmcp.AssumeResponseCommit(serverTransport), nil,
)
This package is named mcp and so is the SDK's, so the block imports this one
as tmuxmcp and the SDK as sdk. NewServer checks the
target and starts nothing.
Standard input and output are known to deliver each response as soon as the
write returns. Wrap any other transport in
AssumeResponseCommit, as the block does for the
in-memory one.
Close the instance when you have finished serving. The
reference has the rest of what an embedder is held to, and
the contract has the full text.
Packages
mcp: the server as a library
mcp.Run and mcp.NewServer serve a tmux server
to a client. Reference: pkg.go.dev
and libtmux.org.
libtmux-mcp: the command a client launches
It serves one tmux server over stdin and stdout. Reference:
pkg.go.dev and the
command README.
What it leaves out
- A second tmux server. The server serves one tmux server for the life of
the process, and a client cannot name another socket in a call.
- Anything but tools. There is one resource, and no prompts, resource
templates, completions, or background job handles.
- Confinement. Toolsets shape what a client is offered. Every call runs
with your user's authority, so a client holding
send_keys can
run anything you can.
- The Python server's per-call socket. The socket is chosen once, at
launch. PARITY.md lists the other differences.
Support and status
- Go 1.26+ and tmux 3.3a or newer. The server refuses an older tmux before
the MCP transport starts. CI runs tmux 3.3a through 3.8.
- Linux, including WSL, and macOS. On macOS, CI runs this module's tests
against Homebrew's current tmux. The matrix runs on Linux.
- A module of its own.
go get on the tmux library never pulls in the MCP
SDK.
- Tagged as
mcp/vN. Pin an exact version in your own go.mod; the
commands here fetch the newest.
See also