MCPconceptHome

Choose documentation 3

latest

Current version

latest
English

Early release libtmux is released, and its APIs may still change between releases. This site's structure and URLs may change too.

Edit this page on GitHub

tmux MCP, built in Ruby

tmux terminal control for AI agents, built on libtmux for Ruby and MCP Ruby SDK. This server maps tmux's object hierarchy — sessions, windows, panes — into MCP tools. It offers only the tools selected when it starts.

tmux through MCP

Replay Hover a call to preview the tmux state after it. Click to pin it.
tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux before the first call.

tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux after tmux_capabilities: unchanged, the call only reads.

tmux window of 1 pane
%1 sh
[checks] 0:sh*

tmux after tmux_create.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_create.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_snapshot: unchanged, the call only reads.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_send.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_wait.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_send.

tmux window of 2 panes
%1 sh
%2 sh
[checks] 0:sh*

tmux after tmux_wait.

tmux window of 1 pane
%0 cat
[docs] 0:cat*

tmux after tmux_close.

you

Create a tmux session called checks, split its window into two panes, run pytest -q in one and show checks.log in the other, show me the results, then remove the session.

agentAsk the server which tools and limits are enabled.

Result
{
"ok": true,
"data": {
"endpoint": "docs",
"server_identity": {
"generation": "3f9a1c",
"pid": 1119217,
"start_time": 1794050582,
"tmux_version": "3.8a"
},
"enabled_tools": [
"tmux_capabilities",
"tmux_snapshot",
"tmux_send",
"tmux_create",
"… 4 more"
],
"criteria_schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:libtmux-ruby:where:1",
"title": "Ruby libtmux criteria version 1",
"description": "The Ruby decoder additionally enforces byte, depth and node limits, rejects duplicate keys and floating-point tokens for integer fields.",
"oneOf": [
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
{
"type": "object",
"required": "…",
"additionalProperties": false,
"properties": "…"
},
"… 1 more"
],
"$defs": {
"session.id": {
"anyOf": "…"
},
"session.name": {
"anyOf": "…"
},
"session.created": {
"anyOf": "…"
},
"session.attached": {
"anyOf": "…"
},
"session.window_count": {
"anyOf": "…"
},
"session": {
"type": "object",
"additionalProperties": false,
"properties": "…"
},
"window.id": {
"anyOf": "…"
},
"window.name": {
"anyOf": "…"
},
"window.width": {
"anyOf": "…"
},
"window.height": {
"anyOf": "…"
},
"window.pane_count": {
"anyOf": "…"
},
"window.layout": {
"anyOf": "…"
},
"…": "30 more"
}
},
"limits": {
"max_captures": 16,
"max_capture_bytes": 8388608,
"capture_ttl_seconds": 30,
"request_timeout_seconds": 30,
"max_response_bytes": 1048576,
"max_mutation_bytes": 65536,
"max_observers": 16,
"max_enrollments": 8,
"default_enrollment_seconds": 60,
"max_enrollment_seconds": 300,
"max_script_bytes": 65536,
"default_run_output_bytes": 65536,
"…": "2 more"
},
"owns_daemon": false,
"resource_subscriptions": false,
"observation": {
"screen": "bounded_rows",
"history_continuity": "unknown",
"process_cursor": "conditional",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 3 more"
],
"wait_conditions": [
"screen_contains",
"process_exit"
]
},
"authored_run": {
"availability": "conditional",
"shell_profile": "zsh-5.9-zle",
"enrollment": "explicit_source",
"authorization": "exact_generation_at_queue_grant",
"stdin": "closed",
"persistent_shell_changes": false,
"descendant_termination": "unobserved",
"requirements": [
"tmux >= 3.3a",
"64-bit Linux with peer pidfds (kernel >= 6.6)",
"same PID namespace and matching procfs",
"live pane process",
"… 5 more"
]
}
}
}

agentCreate the session.

tmux_create {"kind":"session","name":"checks","argv":["sh"]}
Result
{
"ok": true,
"data": {
"entity": {
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
"created": [
{
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
{
"generation": "3f9a1c",
"kind": "window",
"id": "@1"
},
{
"generation": "3f9a1c",
"kind": "pane",
"id": "%1"
}
],
"delivery": "observed",
"program_completion": "unobserved"
}
}

agentSplit the pane the create result returned (%1) in two.

tmux_create {"kind":"pane","parent":{"kind":"pane","id":"%1","generation":"3f9a1c"},"direction":"horizontal","argv":["sh"]}
Result
{
"ok": true,
"data": {
"entity": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
},
"created": [
{
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
}
],
"delivery": "observed",
"program_completion": "unobserved"
}
}

agentRead the window's layout.

tmux_snapshot {"entity":"window"}
Result
{
"ok": true,
"data": {
"capture_id": "a13829fbcf68c4d13192d49431427978",
"server_identity": {
"generation": "3f9a1c",
"pid": 1119217,
"start_time": 1794050582,
"tmux_version": "3.8a"
},
"entity": "window",
"coverage": {
"session": "complete",
"window": "complete",
"pane": "complete",
"window_link": "complete",
"client": "complete"
},
"interval": {
"clock": "monotonic_seconds",
"started": 6950.386331011,
"finished": 6950.398742721,
"reads": 5
},
"items": [
{
"kind": "window",
"fields": {
"id": "@0",
"name": "cat",
"width": 80,
"height": 24,
"paneCount": 1,
"layout": "{\"V\":2,\"L\":{\"t\":\"p\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%0\"}}"
},
"ref": {
"generation": "3f9a1c",
"kind": "window",
"id": "@0"
}
},
{
"kind": "window",
"fields": {
"id": "@1",
"name": "sh",
"width": 80,
"height": 24,
"paneCount": 2,
"layout": "{\"V\":2,\"L\":{\"t\":\"h\",\"w\":80,\"h\":24,\"x\":0,\"y\":0,\"c\":[{\"t\":\"p\",\"w\":40,\"h\":24,\"x\":0,\"y\":0,\"a\":true,\"i\":0,\"I\":\"%1\"},{\"t\":\"p\",\"w\":39,\"h\":24,\"x\":41,\"y\":0,\"i\":1,\"I\":\"%2\"}]}}"
},
"ref": {
"generation": "3f9a1c",
"kind": "window",
"id": "@1"
}
}
],
"truncated": false
}
}

agentType the command into the pane the create result returned (%1).

tmux_send {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"input":{"type":"text","text":"pytest -q\n"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%1"
},
"delivery": "observed",
"client_exit_status": 0,
"completion": "dispatch_only"
}
}

agentWait for the test summary in the pane the create result returned (%1).

tmux_wait {"target":{"kind":"pane","id":"%1","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"2 passed"},"timeout":10}
Result
{
"ok": true,
"data": {
"target": {
"kind": "pane",
"id": "%1",
"generation": "3f9a1c"
},
"condition": "screen_contains",
"capture": {
"mode": "snapshot",
"target": {
"kind": "pane",
"id": "%1",
"generation": "3f9a1c"
},
"capture_id": "7614cc6ece6c7b3bbcb5119bb6291b2f",
"process_generation": "23fb4e239db0000be47a27a2b54059d5",
"rows": [
"$ pytest -q\n",
".. [100%]\n",
"2 passed in 0.01s\n",
"$ \n",
"… 20 more"
],
"encoding": "utf-8",
"row_count": 24,
"bytes": 93,
"truncated": false,
"history_continuity": "unknown",
"scope": {
"max_lines": 200,
"max_bytes": 65536,
"history_lines": 0
},
"interval": {
"clock": "monotonic_seconds",
"started": 6953.983863023,
"finished": 6954.004227945
}
}
}
}

agentType the command into the new pane the split returned (%2).

tmux_send {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"input":{"type":"text","text":"cat checks.log\n"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "pane",
"id": "%2"
},
"delivery": "observed",
"client_exit_status": 0,
"completion": "dispatch_only"
}
}

agentWait for the last line of the log in the pane the split returned (%2).

tmux_wait {"target":{"kind":"pane","id":"%2","generation":"3f9a1c"},"condition":{"type":"screen_contains","text":"checks finished"},"timeout":10}
Result
{
"ok": true,
"data": {
"target": {
"kind": "pane",
"id": "%2",
"generation": "3f9a1c"
},
"condition": "screen_contains",
"capture": {
"mode": "snapshot",
"target": {
"kind": "pane",
"id": "%2",
"generation": "3f9a1c"
},
"capture_id": "14373ab34e565f19369f7e1bc9e0b210",
"process_generation": "0dd4defa031cdb8636cd70dc67eede49",
"rows": [
"$ cat checks.log\n",
"2026-01-01 checks started\n",
"2026-01-01 checks finished\n",
"$ \n",
"… 20 more"
],
"encoding": "utf-8",
"row_count": 24,
"bytes": 93,
"truncated": false,
"history_continuity": "unknown",
"scope": {
"max_lines": 200,
"max_bytes": 65536,
"history_lines": 0
},
"interval": {
"clock": "monotonic_seconds",
"started": 6957.556795452,
"finished": 6957.578571397
}
}
}
}

agentRemove the session by the id the create result returned ($1).

tmux_close {"target":{"kind":"session","id":"$1","generation":"3f9a1c"}}
Result
{
"ok": true,
"data": {
"target": {
"generation": "3f9a1c",
"kind": "session",
"id": "$1"
},
"delivery": "observed",
"client_exit_status": 0
}
}
0/9

Install

Choose your client and configuration scope, then run the command or paste the configuration.

With RubyGems installed:

$ claude mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With RubyGems installed:

$ claude mcp add tmux --scope user -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With RubyGems installed:

$ claude mcp add tmux --scope project -- libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With Bundler installed:

$ claude mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (this project)

With Bundler installed:

$ claude mcp add tmux --scope user -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.claude.json (all projects)

With Bundler installed:

$ claude mcp add tmux --scope project -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .mcp.json (in repo, version-controlled)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: claude_desktop_config.json

With RubyGems installed:

$ codex mcp add tmux -- libtmux-mcp --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With RubyGems installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "libtmux-mcp"
args = ["--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With Bundler installed:

$ codex mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.codex/config.toml

With Bundler installed:

Codex's CLI doesn't support project scope yet — paste this into .codex/config.toml at the repo root.

[mcp_servers.tmux]
command = "bundle"
args = ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .codex/config.toml (in repo)

With RubyGems installed:

$ gemini mcp add --scope user tmux libtmux-mcp -- --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With RubyGems installed:

$ gemini mcp add --scope project tmux libtmux-mcp -- \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With Bundler installed:

$ gemini mcp add --scope user tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/settings.json

With Bundler installed:

$ gemini mcp add --scope project tmux bundle -- exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .gemini/settings.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: .cursor/mcp.json (in repo)

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.cursor/mcp.json

With RubyGems installed:

$ grok mcp add --scope user tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With RubyGems installed:

$ grok mcp add --scope project tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With Bundler installed:

$ grok mcp add --scope user tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.grok/config.toml

With Bundler installed:

$ grok mcp add --scope project tmux -- bundle exec libtmux-mcp \
    --socket-name libtmux-docs --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ./.grok/config.toml (in repo)

With RubyGems installed:

{
    "mcpServers": {
        "tmux": {
            "command": "libtmux-mcp",
            "args": ["--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With Bundler installed:

{
    "mcpServers": {
        "tmux": {
            "command": "bundle",
            "args": ["exec", "libtmux-mcp", "--socket-name", "libtmux-docs", "--endpoint", "local"]
        }
    }
}

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.gemini/config/mcp_config.json

With RubyGems installed:

$ opencode mcp add tmux -- libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

With Bundler installed:

$ opencode mcp add tmux -- bundle exec libtmux-mcp --socket-name libtmux-docs \
    --endpoint local

The server borrows an existing named socket. Start it first with `tmux -L libtmux-docs new-session -d`; `--endpoint` is only its public alias.

Config file: ~/.config/opencode/opencode.jsonc

libtmux-mcp exposes one existing tmux server over MCP standard input and output. Requiring libtmux/mcp starts no server; the installed libtmux-mcp executable owns the protocol transport and borrows the selected tmux daemon.

The default catalog contains tmux_capabilities and tmux_snapshot. Observation and mutation tools are opt-in, so a client cannot acquire them by calling an undisclosed name.

Start here

  • Install configures an MCP client to launch the Ruby server.
  • Tools records the actual wire schemas and per-tool policy.
  • Guides connect a client to a private tmux server.
  • Topics explain captures, cursors, references, and tool policy.
  • Examples run a client launcher or embed the MCP application.
  • Language API documents the Ruby embedding surface.

The server requires Ruby 3.3 or newer. Strong process tracking for waits and authored runs requires tmux 3.3a or newer plus native process identity support. The MCP server guide covers the server’s own source documentation.

Tools

The Ruby server declares no toolsets, so its tools are listed together. Read each tool's page before enabling it.

  • tmux_capabilities Discover the fixed endpoint, server binding, enabled tools, criteria schema and limits.
  • tmux_capture Read exact-pane screen/history rows preserving LF.
  • tmux_close Destroy one exact session, window or pane.
  • tmux_create Create a session, window in an exact session, or split an exact pane using argv.
  • tmux_run Run an authored POSIX script in an explicitly enrolled idle, empty zsh 5.9 editor.
  • tmux_send Send literal UTF-8 text or named keys to one exact pane.
  • tmux_snapshot Capture ordered metadata and evaluate Ruby criteria locally.
  • tmux_wait Wait for observed literal screen text or the initial pane process exit.

Browse every tool, with its arguments, result and protocol data.

Esc

Type to search.

Reference previewOpen reference