tmux-mcp-server
This MCP server lets you inspect and control tmux sessions, windows, panes, and run commands via a flat, client-compatible tool set.
List/inspect sessions, windows, panes, clients, and server details.
Create/select sessions and windows; split, select, resize, and zoom panes.
Read pane text/scrollback, search pane history, and get pane metadata.
Send literal text or special keys (Enter, arrows, Ctrl combos) to panes.
Run shell commands with exact exit codes and captured output; poll, wait, or list tracked commands.
Drive remote tmux over SSH with per-call host routing, host allowlists, socket overrides, and protected targets.
Use read-only mode or custom tool profiles, and optionally log command history.
Provides tools for managing tmux sessions, windows, panes, layouts, status inspection, and secure command execution using event-driven pipe-pane and wait-for primitives.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tmux-mcp-server list all active tmux sessions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tmux-mcp
Model Context Protocol (MCP) server for tmux, written in Python and distributed via
uvx.
Exposes 37 granular tools covering sessions, windows, panes, layouts, status inspection, and secure command execution using event-driven pipe-pane + wait-for primitives.
Key Features & Security Design
No Shell Injection Vulnerabilities: All tmux interactions execute strictly via argv parameter arrays without
shell=True.Event-Driven Execution Engine: Uses
pipe-panestreams andtmux wait-forevents instead of polling or scrollback regex. Captures exact return codes and complete output streams (>50,000 lines tested without truncation).Interactive App Guard: Automatically blocks non-shell panes (e.g.
vim,htop,python) from accidental command injection, prompting the caller to use keypress tools (send_keys) instead.Strict Protocol & Output Purity: Zero stray stdout prints to prevent corrupting the MCP stdio JSON-RPC transport stream (logging strictly routed to
sys.stderr).Portable Flat Tool Schemas: Compatible with all MCP clients (Claude Code, Claude Desktop, OpenCode, Cursor, Windsurf, Zed, Continue, VS Code) by enforcing flat JSON schemas without unsupported
$ref/$defs/anyOfconstructs.
Related MCP server: Ryan's Tmux MCP Server
Quick Start
Run instantly without installing Python dependencies using uvx:
# Run with standard profile (default 22 tools)
uvx tmux-mcp-server
# Run with full profile (all 37 tools including destructive actions)
uvx tmux-mcp-server --tools=full
# Run in read-only mode (13 read tools)
uvx tmux-mcp-server --read-only
# Keep production sessions readable but reject every write to them
uvx tmux-mcp-server --protect 'prod-*' --protect 'skinstrading:3*'Driving a Remote tmux over ssh
One MCP server instance now drives tmux on any machine. --remote-host sets the
default host for calls that do not name one, and every tool accepts an optional
host argument to route that single call elsewhere over ssh. You only need one MCP
entry:
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": [
"tmux-mcp-server",
"--remote-host", "prod-01",
"--ssh-opt", "-p", "--ssh-opt", "2222",
"--protect", "host:prod-*",
"--allow-host", "prod-*",
"--allow-host", "staging-*"
]
}
}
}One entry, many machines -- the host travels with the call:
read_pane { "target": "deploy:0.0", "host": "glinteco-website" }
run_command { "command": "systemctl is-active nginx", "host": "prod-2" }
read_pane { "target": "%3" } // no host: the local tmuxBatchMode=yes is always set, so ssh authentication must be non-interactive: a key or an
agent. A host that would prompt for a password fails immediately with a clear error rather
than hanging until the timeout. --remote-host is also settable as TMUX_MCP_REMOTE_HOST.
Reading a remote pane this way beats reading one through a local pane that happens to hold
an ssh session: capture-pane runs on the remote server, so the full scrollback is
available instead of only the visible screen.
run_command works against a remote host too. Its capture and exit-code files are created
and read on whichever machine the tmux server lives on, so the exit code you get back is
the remote command's own. --remote-tmp-dir (default /tmp) chooses where those files
go, since the local temporary directory does not exist over there.
Why --allow-host?
An agent holding an ssh agent key can otherwise reach every machine that key opens, and
nothing in this server would have said no. --allow-host is a fnmatch allowlist that
limits which hosts the server is willing to drive. An empty host (the server's default)
always passes. When no --allow-host is given, any host is allowed. --allow-host is
also settable as TMUX_MCP_ALLOWED_HOSTS.
When the remote socket is not where tmux looks
tmux derives its default socket from the current uid. If ssh lands you on the remote host as a different user than the one running tmux, the default socket is simply the wrong path and tmux reports "no server running" -- which reads like the server is down when it is merely somebody else's.
A real case: ssh glinteco-website arrives as root, while the tmux server there belongs
to ubuntu on /tmp/tmux-1000/default. root can read that socket perfectly well once told
where it is:
uvx tmux-mcp-server --host-socket 'glinteco-website=/tmp/tmux-1000/default'--host-socket HOST=SOCKET is repeatable and the host side is an fnmatch pattern, so a
fleet with a shared convention needs one line: --host-socket 'prod-*=/tmp/tmux-1000/default'.
A value starting with / becomes -S, anything else becomes -L. It only applies to calls
that name a host; the local tmux keeps using --socket-name / --socket-path as before.
Also settable as TMUX_MCP_HOST_SOCKETS (comma-separated pairs).
Protecting a whole host
--protect matches by identity, just like target names. When a host is active, an
identity of the form host:<name> is included, so:
uvx tmux-mcp-server --protect 'host:prod-*'makes every mutating tool refuse on those machines while reads still work. This works even when the remote host is unreachable: the identity is checked before tmux is touched.
Protecting Specific Targets
--read-only is all-or-nothing: it strips every mutating tool from the server. When
you want an agent that can freely drive scratch sessions but must never type into the
one attached to production, protect that target by name instead:
uvx tmux-mcp-server --protect 'prod-*,skinstrading:3*'
# or
TMUX_MCP_PROTECTED_TARGETS='prod-*,skinstrading:3*' uvx tmux-mcp-serverProtected targets stay fully readable (read_pane, search_pane, get_pane_info);
only mutating tools refuse them, and they refuse before tmux is invoked.
Matching is by identity, not by the string the caller happened to use. A target is resolved through tmux first, then every name it answers to is tested against the patterns as a shell glob:
Form | Example |
session |
|
session:window index |
|
session:window name |
|
session:window.pane |
|
raw ids |
|
So --protect 'skinstrading:3*' also blocks a call that named the pane %88. An
omitted target is checked too, since it resolves to tmux's current pane.
Tool Profiles
Profile | Tools Count | Description |
| 22 | All read, interact, split, resize, layout, and command execution tools (excludes destructive |
| 37 | Complete set of 37 tools, including destructive operations ( |
| 13 | Read-only inspection tools. Safe for unrestricted agent usage. |
Custom | N | Comma-separated list of tool names (e.g. |
Client Configuration Examples
Watch the package name. This project is
tmux-mcp-server. Do not configure"args": ["tmux-mcp"]—tmux-mcpis an unrelated, already-published PyPI package, anduvxwould silently download and run that one instead.Working on the code? Point the client at your checkout so edits take effect immediately:
"args": ["run", "--directory", "/absolute/path/to/tmux-mcp-server", "tmux-mcp-server"]with
"command": "uv". Note thatuvx --from <path>caches the built wheel per version, so local edits are not picked up until you bumpversioninpyproject.toml—uv run --directoryhas no such caching.
1. Claude Code
Add to ~/.claude.json or .claude.json:
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["tmux-mcp-server"]
}
}
}2. Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["tmux-mcp-server"]
}
}
}3. OpenCode
Add to ~/.config/opencode/opencode.json (or project .opencode/mcp.json):
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["tmux-mcp-server"]
}
}
}4. Cursor
Add to Settings -> Features -> MCP:
Name:
tmuxType:
commandCommand:
uvx tmux-mcp-server
5. Windsurf
Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["tmux-mcp-server"]
}
}
}6. Zed
Add to ~/.config/zed/settings.json:
{
"context_servers": {
"tmux": {
"command": {
"path": "uvx",
"args": ["tmux-mcp-server"]
}
}
}
}7. Continue
Add to ~/.continue/config.json:
{
"mcpServers": [
{
"name": "tmux",
"command": "uvx",
"args": ["tmux-mcp-server"]
}
]
}8. VS Code (Roo Code / Cline)
Add to .vscode/mcp.json:
{
"mcpServers": {
"tmux": {
"command": "uvx",
"args": ["tmux-mcp-server"]
}
}
}9. Generic Stdio Client
Any MCP-compliant client can launch via stdio:
{
"command": "uvx",
"args": ["tmux-mcp-server"]
}Tool Reference (37 Tools)
Sessions (6)
list_sessionsget_sessioncreate_sessionrename_sessionswitch_clientkill_session⚠️
create_session pins the size you ask for. tmux's default window-size latest would
otherwise resize a detached window to whatever client is newest on the server and
silently discard -x/-y. This matters when driving a TUI: the same screen captured at
320 columns costs roughly five times more to read back than at 120.
Windows (6)
list_windowscreate_windowrename_windowselect_windowmove_windowkill_window⚠️
Panes — Read & Interact (7)
list_panesget_pane_inforead_panesearch_panesend_keyssend_special_keyclear_pane
Full-screen apps and nested tmux. While a pane runs vim, htop, less, or a tmux attached over ssh, the terminal is on its alternate screen and tmux keeps no reachable scrollback for it — only the visible screen exists. Reading works normally (a nested tmux is just rendered text, at any nesting depth), but history does not, so:
get_pane_inforeportsalternate_onread_panereturnsscrollback_available: false, awarning, and setstruncatedwhen history was requested and could not be deliveredsearch_panereportssearched_linesand warns that a zero result covers only the visible screen
read_pane(join_wrapped=true) rejoins lines the terminal split at pane width, so a
long value broken across two rows comes back as one string.
Note that list_panes never sees panes inside a nested tmux: that inner server has
its own socket, usually on another machine.
Panes someone is viewing. copy-mode belongs to the pane, not to a client —
copy-mode has no -c flag, and every client watching the pane sees the same mode.
A pane being scrolled and a pane being typed into are therefore mutually exclusive
states, and typing into a pane in copy-mode does not fail cleanly. tmux routes each
key into the mode's key table: leading characters are consumed as copy-mode commands,
one of them cancels the mode, and the tail falls through to the shell and runs. Sending
echo MARKER at a pane in copy-mode leaves bash: RKER: command not found.
send_keys, send_special_key, and run_command therefore refuse a pane in
copy-mode. Pass exit_copy_mode=true to cancel the mode first — which yanks a human
viewer's screen back to the live output, so it is opt-in rather than automatic.
To watch a pane's output while an agent keeps typing into it, do not scroll the pane itself. Tee it and read the copy instead:
tmux pipe-pane -t <agent-pane> -o 'cat >> /tmp/agent.log'
less +F /tmp/agent.log # your own pane, your own scrollbackLarge payloads. send-keys -l carries the whole text as one argv element, and tmux
caps a client→server command message at 16KB: past roughly 16,300 bytes tmux answers
command too long and nothing is typed at all. Below that ceiling a full-screen
application can still lose part of a long burst, because the bytes reach it as thousands
of individual keypresses and a TUI that re-renders between reads processes only some of
them — the classic symptom is a prompt whose head is missing and whose tail arrived.
send_keys therefore switches to a tmux paste buffer above 200 bytes: load-buffer -
takes the payload on stdin (no command-line limit at all) and paste-buffer -p wraps it
in bracketed-paste markers when the application asked for them, so the app sees one paste
event. The JSON reply reports which path ran as method: keys, paste or chunked
(the fallback for a tmux without load-buffer -). Pass paste=true to force the paste
path for short text. A 100KB payload arrives byte-identical; Enter is sent after a short
settle delay so a pasting TUI submits it rather than absorbing it into the burst.
One limit this cannot lift: a pane whose foreground process reads the tty in canonical
mode (cat, read, a password prompt) is capped by the line discipline at MAX_CANON
(1024 bytes) per line, and the kernel discards the rest however the bytes were delivered.
Panes — Layout & Sizing (9)
split_paneselect_paneresize_panezoom_paneswap_panesmove_panebreak_paneset_layoutkill_pane⚠️
Command Execution (5)
run_commandget_command_resultwait_commandlist_commandscancel_command
Command history. run_command types into a real interactive shell, so every
command it runs also lands in that shell's history. That history belongs to the
human — it is what a prefix search walks through — and it cannot hold what an audit
actually needs anyway: no exit code, no timestamp, no pane, no duration. So the
server keeps its own log, tab-separated, one line per event:
ts event command_id pane exit elapsed cwd command
2026-08-17T00:49:30+0700 RUN cmd_790182afe03d %0 - - /repo uv run pytest
2026-08-17T00:49:31+0700 OK cmd_790182afe03d %0 0 0.23s /repo uv run pytestDispatch is logged before the command finishes, so tail -f shows work in flight
rather than only what has already completed. Events are RUN, OK, FAIL and
CANCEL, which makes grep FAIL worth typing. pane is the resolved %id, not
the string the caller passed, since session names get reused.
# default: /tmp/.tmux-mcp-server-history, so the OS reclaims it on its own schedule
uvx tmux-mcp-server --commands-history-file ~/.tmux-mcp/commands.log
uvx tmux-mcp-server --no-save-commands-history # record nothing; wins over the flag aboveThe file is created if missing, opened append-only and mode 0600, and never
followed through a symlink — the default sits in a world-writable /tmp, and these
lines carry every command and working directory of the session. A write failure is
logged once and never propagates: the history must not be able to break the command
it is recording.
Server & Inspection (4)
server_infolist_clientsdisplay_messageshow_options
Development & Testing
Run all unit and integration tests (using an isolated test socket -L tmux-mcp-test):
uv run pytestThe suite runs under asyncio_mode = "auto": every test here is a coroutine driving
tmux, so a missing @pytest.mark.asyncio can no longer make a test pass by never
being awaited.
Lint runs on commit. Install the hook once:
uv run pre-commit install
uv run ruff check . # same check, on demandThe hook shells out to uv run ruff, so it uses exactly the ruff pinned in the dev
dependency group rather than a separate copy that could drift.
License
MIT
Available Tools
22 toolscreate_sessionB
Create a new detached tmux session
Inputs: name, width, height, start_directory. Returns: result
create_windowA
Create a new window in target session
Inputs: name, select, target, start_directory. Returns: result
get_command_resultBRead-onlyIdempotent
Poll status or fetch result of a tracked command execution
Inputs: command_id. Returns: result
get_pane_infoCRead-onlyIdempotent
Get status, command, path, and dimensions of a pane
Inputs: target. Returns: result
get_sessionARead-onlyIdempotent
Get detailed info for a specific session by ID or name
Inputs: target. Returns: result
list_clientsARead-onlyIdempotent
List connected tmux clients
Inputs: no named parameters. Returns: result
list_commandsARead-onlyIdempotent
List tracked command executions in current session
Inputs: target. Returns: result
list_panesARead-onlyIdempotent
List tmux panes for a target window/session or all sessions
Inputs: target, all_panes. Returns: result
list_sessionsARead-onlyIdempotent
List active tmux sessions with details
Inputs: target. Returns: result
list_windowsARead-onlyIdempotent
List tmux windows for a session or across all sessions
Inputs: target, all_sessions. Returns: result
read_paneBRead-onlyIdempotent
Read visible text and scrollback history from a pane
Inputs: end, lines, start, target, +2 more. Returns: result
resize_paneC
Resize pane relative or to absolute dimensions
Inputs: width, height, target, direction, +1 more. Returns: result
run_commandC
Run shell command with exact exit code and pipe-pane output capture
Inputs: wait, target, command, timeout. Returns: result
search_paneARead-onlyIdempotent
Search pane scrollback history using regex pattern
Inputs: target, pattern, max_results. Returns: result
select_paneA
Select active pane by ID or direction (L, R, U, D)
Inputs: target, direction. Returns: result
select_windowB
Select target window to become active
Inputs: target. Returns: result
send_keysA
Send literal string text to a target pane
Inputs: keys, enter, target. Returns: result
send_special_keyC
Send a special key shortcut (e.g. Enter, Up, C-c)
Inputs: key, repeat, target. Returns: result
server_infoARead-onlyIdempotent
Get tmux server version, socket info, and counts
Inputs: no named parameters. Returns: result
split_paneC
Split target pane horizontally or vertically
Inputs: size, target, direction, start_directory. Returns: result
wait_commandARead-onlyIdempotent
Wait for a running command execution to finish
Inputs: timeout, command_id. Returns: result
zoom_paneB
Toggle zoom state (maximize/restore) for target pane
Inputs: target. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 22 tools
Each tool targets a distinct resource and action, with no true overlaps. The only near pair is get_pane_info vs read_pane, but one reports metadata and the other reads content, making them easy to distinguish.
Nearly all tools follow a consistent verb_noun snake_case pattern (e.g., list_sessions, create_window, send_keys). The single exception is server_info, which uses a noun phrase instead of get_server_info, breaking the pattern slightly.
With 22 tools, the set is on the heavier side, but the tmux domain is broad and the tools are organized into clear clusters (sessions, windows, panes, command execution, server info). The count feels justified rather than bloated.
The tool surface covers major tmux interactions comprehensively, but it lacks explicit destroy or rename operations for sessions, windows, and panes. Since run_command can execute arbitrary shell commands (including tmux kill/rename commands), these gaps are workable.
Maintenance
Related MCP Connectors
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Remote MCP server exposing your Node Command fleet (files, shell, processes, browser) to AI hosts.
MCP server for Gainium — manage trading bots, deals, and balances via AI assistants
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAn MCP server that gives AI assistants the ability to create, manage, and control terminal sessions through a safe, isolated tmux environment.1ISC
- FlicenseNot gradedqualityDmaintenanceMCP server for tmux operations that provides comprehensive control over tmux sessions, windows, and panes.1-
- AlicenseAqualityDmaintenanceMCP server for terminal control enabling AI to manage terminal sessions via iTerm2 or tmux, with features like screen reading, keyboard/mouse input, and background execution.912 npm22MIT
- AlicenseBqualityBmaintenanceA comprehensive MCP server for driving tmux sessions, windows, panes, sending keystrokes, and reading pane output locally or over SSH, enabling real-time collaborative pairing with AI.7120 PyPI6MIT