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+
tmux3.x when usingbackend="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_spawnpty_resumepty_recoverpty_attachpty_detachpty_handoffpty_takeoverpty_readpty_read_stdoutpty_read_stderrpty_read_atpty_read_untilpty_read_until_anypty_read_quiescentpty_promptconfirm_dangerous_commandpty_writepty_resizepty_closepty_statuspty_waitpty_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:
owneris required by default forpty_spawn,pty_list, and follow-up session-bound calls- dangerous commands require
confirm_dangerous_commandfirst, thendangerous_confirm_tokeninpty_spawn pty_signaland 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 sameclient_id
Attachment is coordination state, not an authorization override:
ownerandscopechecks still run firstpty_attach/pty_detachdo 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=0restores owner-optional behaviorPTY_MCP_DANGEROUS_LEGACY_MODE=1restores legacy dangerous-command confirmation viadangerous_confirmed/dangerous_justification
New Parameters
pty_spawn
cwd— working directoryseparate_streams— split stdout/stderr into separate bufferstag,owner— session labels for filteringname— stable session name for later lookup/recoveryscope—read-onlyorread-writemax_output_bytes— ring buffer capspawn_timeout_sec— session runtime caprate_limit_bps— output rate limitinput_rate_bps— input rate limitread_timeout_ms— default read timeout per sessionenv— per-session environment overridesbackend— execution backend (default: subprocess)backendcurrently supportssubprocessand a minimaltmuxbackend.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 forbackend="tmux"; sessions with the sameowner+tmux_workspacereuse one internal tmux session while each MCP session keeps its own panedangerous_confirm_token— token returned byconfirm_dangerous_commandfor dangerous command executiondangerous_confirmed— legacy dangerous-command confirmation flag (compatibility mode only)dangerous_justification— legacy justification text (compatibility mode only)
backend="tmux" caveats
separate_streams=trueis not supported for tmux sessions.pty_signalis 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_workspaceis a high-level grouping hint, not a public tmux control surface. Each MCP session still owns exactly one pane and is still addressed bysession_id.pty_resizeon shared tmux workspaces is best-effort and may affect sibling pane layout.pty_resumestill resolves only in-memory live sessions tracked bypty-mcp; it does not rediscover orphaned tmux sessions after a server restart.pty_recoveris the explicit/manual restart-recovery path for persisted tmux-backed named sessions; it is separate frompty_resume.pty_attach/pty_detachare 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_tokenandexpires_atfor dangerous commands - supports binding confirmation to
ownerandscope - the returned token is single-use and TTL-bound
pty_resume
- resolves an existing named session and returns its current status
- accepts
nameand optionalowner - under the default policy, use the same
ownervalue 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
nameandowner - only supports tmux sessions that were spawned with a non-empty
nameandowner pty_resumestays live-only;pty_recoveris the manual recovery path- shared
tmux_workspacerecovery is all-or-nothing for the whole persisted group - recovered sessions come back detached (
attached_client_idis 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 sessionpty_detach(session_id, owner, client_id)releases that lease without closing the process, pane, or tmux workspacepty_handoff(session_id, owner, client_id, target_client_id, reason)explicitly transfers a current lease to another clientpty_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
subprocessandtmuxsessions - attaching is idempotent for the same
client_id - attaching from a different
client_idfails while the session is still attached - for shared
backend="tmux"sessions with the sameowner+tmux_workspace, all attached live sessions must share oneclient_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_idonly when a session is attached:pty_readpty_read_stdoutpty_read_stderrpty_read_untilpty_read_until_anypty_read_quiescentpty_promptpty_writepty_resizepty_closepty_signal
- observational tools stay usable without attachment:
pty_read_atpty_statuspty_waitpty_listpty_resume
- this phase does not add per-client read cursors; use
pty_read_atfor observation without consuming shared buffers pty_statusandpty_listexpose workspace-level attachment state for shared tmux workspaces so callers can distinguish a session lease from a workspace leasepty_status,pty_list, and spawn metadata also expose the latest lease audit fields (last_lease_*) for explicit handoff/takeover tracingpty_statusandpty_listalso exposerecovered/recovered_atfor explicitly recovered sessions
Session Logging
Set PTY_MCP_SESSION_LOG_DIR to enable per-session logs:
transcript.log— raw output streamevents.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—jsonortext(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=truemakes 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"withtmux_workspacereuses 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_detachare in-memory logical lease state only; they are not persisted across server restarts.pty_resumestays name-based; phase-4 does not add attach-by-workspace or attach-by-tmux-target flows.pty_recoveris 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
commandis an absolute path, cwd is inferred from its directory. pty_signaland admin/global tools are hidden by default; enable them explicitly withPTY_MCP_ENABLE_CONTROL_TOOLS=1andPTY_MCP_ENABLE_ADMIN_TOOLS=1when needed.- Session names are checked within the same
ownerscope; if you run withoutowner, same-name sessions can become ambiguous or conflict globally.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c515d809fc1ed7c73b0164721a5a9054d1bebf6fa944d8f35967f021a341fbd
|
|
| MD5 |
8690e52bf05e701297610d3ee9dbe593
|
|
| BLAKE2b-256 |
a57033579b7f3693f360f1df0296be000dd7c3dadbb338690a34be647ee5a425
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b4bd309611a073a7727bdf78cc72893c9bff3b98481b2628e5c18b430a75a80
|
|
| MD5 |
ac2de46c3178b04dda7304d73a6c5962
|
|
| BLAKE2b-256 |
727790f0d62b3f493b3cb930e7c3f8f4459d45dfa5687b2ea38d4e53eaeaf166
|