Skip to main content

A PTY runner for MCP

Project description

pty-mcp

A lightweight PTY runner for MCP (Model Context Protocol).

pty-mcp provides terminal session tools over MCP so clients can spawn commands in a pseudo-terminal, read interactive output, send input, resize terminal windows, and manage process lifecycle safely.

Features

  • Spawn shell commands in a real PTY
  • Read incremental output from a ring buffer
  • Wait for pattern matching (read_until)
  • Wait for any pattern (read_until_any)
  • Send interactive input (write)
  • Send input and wait for prompt (prompt)
  • Resize terminal (cols / rows)
  • Process control (status, wait, signal, close)
  • Stream split reads (read_stdout, read_stderr)
  • Read with offset (read_at)
  • Session tags (tag, owner)
  • Dynamic defaults (set_default_cwd)
  • Runtime limits (set_limits)
  • Metrics and health (metrics, health)

Requirements

  • Python 3.10+
  • tmux 3.x when using backend="tmux"

Installation

pip install pty-mcp

CLI Entrypoint

After installation, the package exposes:

pty-mcp

This starts the MCP server over stdio.

MCP Tools

By default, the server exposes the core session tools:

  • pty_spawn
  • pty_resume
  • pty_recover
  • pty_attach
  • pty_detach
  • pty_handoff
  • pty_takeover
  • pty_read
  • pty_read_stdout
  • pty_read_stderr
  • pty_read_at
  • pty_read_until
  • pty_read_until_any
  • pty_read_quiescent
  • pty_prompt
  • confirm_dangerous_command
  • pty_write
  • pty_resize
  • pty_close
  • pty_status
  • pty_wait
  • pty_list

Optional tools are available when explicitly enabled by runtime policy:

  • control tool: pty_signal (PTY_MCP_ENABLE_CONTROL_TOOLS=1)
  • admin/global tools: pty_close_all, set_default_cwd, get_default_cwd, set_limits, get_limits, pty_metrics, pty_health (PTY_MCP_ENABLE_ADMIN_TOOLS=1)

Default Security Policy

Current package defaults are secure-by-default:

  • owner is required by default for pty_spawn, pty_list, and follow-up session-bound calls
  • dangerous commands require confirm_dangerous_command first, then dangerous_confirm_token in pty_spawn
  • pty_signal and admin/global tools are hidden by default unless enabled explicitly via runtime environment variables
  • when a session is attached to a logical client_id, destructive interactive tools require the same client_id

Attachment is coordination state, not an authorization override:

  • owner and scope checks still run first
  • pty_attach / pty_detach do not grant access across owners
  • unattached sessions preserve the legacy single-client behavior

Compatibility escape hatches still exist, but they are opt-in:

  • PTY_MCP_REQUIRE_OWNER=0 restores owner-optional behavior
  • PTY_MCP_DANGEROUS_LEGACY_MODE=1 restores legacy dangerous-command confirmation via dangerous_confirmed / dangerous_justification

New Parameters

pty_spawn

  • cwd — working directory
  • separate_streams — split stdout/stderr into separate buffers
  • tag, owner — session labels for filtering
  • name — stable session name for later lookup/recovery
  • scoperead-only or read-write
  • max_output_bytes — ring buffer cap
  • spawn_timeout_sec — session runtime cap
  • rate_limit_bps — output rate limit
  • input_rate_bps — input rate limit
  • read_timeout_ms — default read timeout per session
  • env — per-session environment overrides
  • backend — execution backend (default: subprocess)
  • backend currently supports subprocess and a minimal tmux backend.
  • backend="tmux" maps one MCP session to one tmux session/pane; it does not expose tmux window or pane management as MCP tools.
  • tmux_workspace — optional logical workspace name for backend="tmux"; sessions with the same owner + tmux_workspace reuse one internal tmux session while each MCP session keeps its own pane
  • dangerous_confirm_token — token returned by confirm_dangerous_command for dangerous command execution
  • dangerous_confirmed — legacy dangerous-command confirmation flag (compatibility mode only)
  • dangerous_justification — legacy justification text (compatibility mode only)

backend="tmux" caveats

  • separate_streams=true is not supported for tmux sessions.
  • pty_signal is not supported for tmux sessions in this phase.
  • pty_read* reads pane output captured from the bound tmux pane; this is designed for shell / REPL / installer style workloads rather than exact full-screen terminal replay.
  • tmux_workspace is a high-level grouping hint, not a public tmux control surface. Each MCP session still owns exactly one pane and is still addressed by session_id.
  • pty_resize on shared tmux workspaces is best-effort and may affect sibling pane layout.
  • pty_resume still resolves only in-memory live sessions tracked by pty-mcp; it does not rediscover orphaned tmux sessions after a server restart.
  • pty_recover is the explicit/manual restart-recovery path for persisted tmux-backed named sessions; it is separate from pty_resume.
  • pty_attach / pty_detach are logical MCP session lease operations; they do not map to native tmux attach/detach and do not expose pane/window/session management.

confirm_dangerous_command

  • returns confirm_token and expires_at for dangerous commands
  • supports binding confirmation to owner and scope
  • the returned token is single-use and TTL-bound

pty_resume

  • resolves an existing named session and returns its current status
  • accepts name and optional owner
  • under the default policy, use the same owner value that was used when spawning the session
  • resolves in-memory live sessions only; it does not restore sessions across server restarts
  • reconnect flow for attached sessions is: pty_resume -> pty_attach

pty_recover

  • explicitly rebuilds a persisted tmux-backed named session after manager restart
  • requires both name and owner
  • only supports tmux sessions that were spawned with a non-empty name and owner
  • pty_resume stays live-only; pty_recover is the manual recovery path
  • shared tmux_workspace recovery is all-or-nothing for the whole persisted group
  • recovered sessions come back detached (attached_client_id is not restored as an active lease)
  • recovery preserves last_lease_* as audit history only; it does not restore unread buffers, destructive read cursors, dangerous confirmation tokens, or seamless output continuity

pty_attach / pty_detach / pty_handoff / pty_takeover

  • pty_attach(session_id, owner, client_id) claims a logical client lease for an existing live session
  • pty_detach(session_id, owner, client_id) releases that lease without closing the process, pane, or tmux workspace
  • pty_handoff(session_id, owner, client_id, target_client_id, reason) explicitly transfers a current lease to another client
  • pty_takeover(session_id, owner, client_id, from_client_id, reason) explicitly takes a current lease from another client
  • attach state is backend-neutral and applies to both subprocess and tmux sessions
  • attaching is idempotent for the same client_id
  • attaching from a different client_id fails while the session is still attached
  • for shared backend="tmux" sessions with the same owner + tmux_workspace, all attached live sessions must share one client_id
  • detaching never destroys the underlying session; normal teardown still happens through pty_close
  • handoff and takeover are explicit lease-transfer operations only; they do not attach to arbitrary tmux targets or restore sessions across restarts
  • for shared tmux workspaces, handoff/takeover transfer all currently attached live sessions in that workspace together so workspace arbitration remains coherent

Client lease semantics

  • destructive interactive tools require the matching client_id only when a session is attached:
    • pty_read
    • pty_read_stdout
    • pty_read_stderr
    • pty_read_until
    • pty_read_until_any
    • pty_read_quiescent
    • pty_prompt
    • pty_write
    • pty_resize
    • pty_close
    • pty_signal
  • observational tools stay usable without attachment:
    • pty_read_at
    • pty_status
    • pty_wait
    • pty_list
    • pty_resume
  • this phase does not add per-client read cursors; use pty_read_at for observation without consuming shared buffers
  • pty_status and pty_list expose workspace-level attachment state for shared tmux workspaces so callers can distinguish a session lease from a workspace lease
  • pty_status, pty_list, and spawn metadata also expose the latest lease audit fields (last_lease_*) for explicit handoff/takeover tracing
  • pty_status and pty_list also expose recovered / recovered_at for explicitly recovered sessions

Session Logging

Set PTY_MCP_SESSION_LOG_DIR to enable per-session logs:

  • transcript.log — raw output stream
  • events.jsonl — JSON lines for spawn, write, attach, detach, handoff, takeover, and recover events

Dangerous Command Rules

Set PTY_MCP_DANGEROUS_PATTERNS (semicolon-separated regex) to customize detection.

pty_read_quiescent

  • quiescence_ms — silence window before return

pty_read / pty_read_stdout / pty_read_stderr

  • formatjson or text (default)

Examples

Set default cwd

{"name":"set_default_cwd","arguments":{"cwd":"/root/aa"}}

Spawn with stream split

{"name":"pty_spawn","arguments":{"command":"ls","name":"demo-shell","owner":"demo-client","scope":"read-write","separate_streams":true}}

Spawn with tmux backend

{"name":"pty_spawn","arguments":{"command":"printf tmux-ok","name":"tmux-demo","owner":"demo-client","scope":"read-write","backend":"tmux"}}

Spawn two panes in one tmux workspace

{"name":"pty_spawn","arguments":{"command":"read line; printf \"pane-a:%s\" \"$line\"","name":"pane-a","owner":"demo-client","scope":"read-write","backend":"tmux","tmux_workspace":"demo-workspace"}}
{"name":"pty_spawn","arguments":{"command":"read line; printf \"pane-b:%s\" \"$line\"","name":"pane-b","owner":"demo-client","scope":"read-write","backend":"tmux","tmux_workspace":"demo-workspace"}}

Resume a named session

{"name":"pty_resume","arguments":{"name":"demo-shell","owner":"demo-client"}}

Recover a tmux-backed named session after restart

{"name":"pty_recover","arguments":{"name":"tmux-demo","owner":"demo-client"}}

Attach a client lease

{"name":"pty_attach","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-a"}}

Interactive read/write with a client lease

{"name":"pty_write","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-a","data":"hello\n"}}
{"name":"pty_read_quiescent","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-a","quiescence_ms":300}}

Detach a client lease

{"name":"pty_detach","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-a"}}

Handoff a client lease

{"name":"pty_handoff","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-a","target_client_id":"controller-b","reason":"operator handoff"}}

Take over a client lease

{"name":"pty_takeover","arguments":{"session_id":"...","owner":"demo-client","client_id":"controller-b","from_client_id":"controller-a","reason":"controller-a disconnected"}}

Read stdout only

{"name":"pty_read_stdout","arguments":{"session_id":"...","owner":"demo-client"}}

Wait for any prompt

{"name":"pty_read_until_any","arguments":{"session_id":"...","owner":"demo-client","patterns":["OK","ERROR"]}}

Read until quiet

{"name":"pty_read_quiescent","arguments":{"session_id":"...","owner":"demo-client","quiescence_ms":300}}

Prompt helper

{"name":"pty_prompt","arguments":{"session_id":"...","owner":"demo-client","data":"y\n","patterns":["done","error"]}}

Confirm dangerous command

{"name":"confirm_dangerous_command","arguments":{"command":"rm -rf /tmp/demo","justification":"cleanup temp data","owner":"release-bot","scope":"admin"}}

Spawn a dangerous command with token

{"name":"pty_spawn","arguments":{"command":"rm -rf /tmp/demo","owner":"release-bot","scope":"admin","dangerous_confirm_token":"dc_xxx"}}

Notes

  • PTY sessions are in-memory and have idle timeout cleanup.
  • The server is intended for local trusted environments.
  • separate_streams=true makes stdout/stderr non-tty (some commands may change output).
  • backend="tmux" keeps the same MCP lifecycle (spawn/read/write/status/wait/close) but uses a dedicated tmux session under the hood.
  • backend="tmux" with tmux_workspace reuses one internal tmux session for multiple MCP sessions, but it still does not expose tmux pane/window management as public MCP tools.
  • pty_attach / pty_detach are in-memory logical lease state only; they are not persisted across server restarts.
  • pty_resume stays name-based; phase-4 does not add attach-by-workspace or attach-by-tmux-target flows.
  • pty_recover is explicit/manual recovery only. It is limited to tmux-backed named sessions with an owner and refuses ambiguous or partially invalid live tmux state.
  • If command is an absolute path, cwd is inferred from its directory.
  • pty_signal and admin/global tools are hidden by default; enable them explicitly with PTY_MCP_ENABLE_CONTROL_TOOLS=1 and PTY_MCP_ENABLE_ADMIN_TOOLS=1 when needed.
  • Session names are checked within the same owner scope; if you run without owner, same-name sessions can become ambiguous or conflict globally.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pty_mcp-0.2.0.tar.gz (76.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pty_mcp-0.2.0-py3-none-any.whl (30.8 kB view details)

Uploaded Python 3

File details

Details for the file pty_mcp-0.2.0.tar.gz.

File metadata

  • Download URL: pty_mcp-0.2.0.tar.gz
  • Upload date:
  • Size: 76.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pty_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 5c515d809fc1ed7c73b0164721a5a9054d1bebf6fa944d8f35967f021a341fbd
MD5 8690e52bf05e701297610d3ee9dbe593
BLAKE2b-256 a57033579b7f3693f360f1df0296be000dd7c3dadbb338690a34be647ee5a425

See more details on using hashes here.

File details

Details for the file pty_mcp-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pty_mcp-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 30.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pty_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b4bd309611a073a7727bdf78cc72893c9bff3b98481b2628e5c18b430a75a80
MD5 ac2de46c3178b04dda7304d73a6c5962
BLAKE2b-256 727790f0d62b3f493b3cb930e7c3f8f4459d45dfa5687b2ea38d4e53eaeaf166

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page