Skip to main content

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
  • scope — read-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

  • format — json 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

Metadata

Release files for pty-mcp 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pty-mcp 0.2.0
File Size Uploaded Provenance
pty_mcp-0.2.0.tar.gz 76.8 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pty-mcp 0.2.0
File Interpreter ABI Platform
pty_mcp-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 107.6 kB

Release files / pty_mcp-0.2.0.tar.gz

Download URL pty_mcp-0.2.0.tar.gz
Size 76.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5c515d809fc1ed7c73b0164721a5a9054d1bebf6fa944d8f35967f021a341fbd
BLAKE2b-256 checksum
How to use checksums
a57033579b7f3693f360f1df0296be000dd7c3dadbb338690a34be647ee5a425
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / pty_mcp-0.2.0-py3-none-any.whl

Download URL pty_mcp-0.2.0-py3-none-any.whl
Size 30.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1b4bd309611a073a7727bdf78cc72893c9bff3b98481b2628e5c18b430a75a80
BLAKE2b-256 checksum
How to use checksums
727790f0d62b3f493b3cb930e7c3f8f4459d45dfa5687b2ea38d4e53eaeaf166
Upload date
Uploaded using Trusted Publishing?
What is 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}

Security for pty-mcp 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page