PTY MCP
pty-mcp is an MCP server for managing local PTY sessions and SSH-backed remote workflows over stdio. It is meant for MCP clients (Claude Code, Codex, OpenCode, etc.) that need persistent terminal and SSH state instead of one-shot shell execution.
With pty-mcp, a client can:
- start a terminal session once and keep using it across multiple calls
- read buffered output incrementally instead of losing process state between commands
- send follow-up input into the same local or remote shell
- manage SSH connections, remote sessions, remote files, and remote directories through one MCP surface
- mount a remote project locally and combine local editing with remote execution
- expose mount-setup resources so an agent can guide the user through local FUSE/
sshfsinstallation when mount support is missing
This makes workflows like dev servers, watch tasks, remote debugging, and near-local remote development much easier to drive.
Installation
Using Cargo:
Using Homebrew:
From source:
The binary will be available at target/release/pty-mcp.
Usage
Add the MCP server to your Codex config:
[]
= "pty-mcp"
If you want to run a locally built binary instead:
[]
= "/absolute/path/to/pty-mcp/target/release/pty-mcp"
The server communicates over stdio and reads configuration from environment variables.
Typical Workflows
These flows are best understood from the MCP client's point of view: an agent receives a task, decides whether it needs persistent terminal state, remote access, or a mounted workspace, and then keeps interacting with the same session instead of starting over each turn.
Scenario: keep a local dev process alive across turns
Use this when the agent needs to start something like pnpm dev, cargo watch, a test watcher, or a REPL, then come back later to inspect logs or send more input.
flowchart LR
A["Agent receives a local task<br/>Run a dev server / watcher / REPL"] --> B{"Does the task need process state<br/>to survive across turns?"}
B -- "Yes" --> C["pty_spawn<br/>Start one persistent local shell/process"]
C --> D["pty_read<br/>Inspect startup logs and current state"]
D --> E{"Need to interact,<br/>retry, or provide more input?"}
E -- "Yes" --> F["pty_write<br/>Send command, keystrokes, or stdin"]
F --> D
E -- "No, just observe" --> G{"Still running?"}
G -- "Yes" --> D
G -- "Finished / should stop" --> H["pty_wait or pty_kill"]
Scenario: investigate or operate on a remote machine interactively
Use this when the agent needs a real remote shell that it can return to, instead of one-shot SSH execution with no retained terminal state.
flowchart LR
A["Agent receives a remote task<br/>Check logs / run deployment / debug service"] --> B["ssh_connect<br/>Establish or reuse SSH connection"]
B --> C{"Need an interactive remote shell<br/>with persistent terminal state?"}
C -- "Yes" --> D["ssh_session_spawn<br/>Create remote PTY session"]
D --> E["pty_read<br/>Read remote output incrementally"]
E --> F{"Need follow-up commands,<br/>answers, or shell input?"}
F -- "Yes" --> G["pty_write<br/>Continue in the same remote session"]
G --> E
F -- "No" --> H{"Task complete?"}
H -- "Not yet" --> E
H -- "Yes" --> I["pty_kill if needed<br/>ssh_disconnect when done"]
Scenario: edit locally, execute remotely
Use this when the agent wants local-quality file access for search and editing, while still running build, test, or runtime commands on the remote host.
flowchart LR
A["Agent receives a remote code task"] --> B["ssh_connect"]
B --> C{"Would local editing/search be easier<br/>if the remote project were mounted?"}
C -- "Yes" --> D["ssh_mount<br/>Expose remote project as local files"]
D --> E["Agent reads, searches, and edits mounted files locally"]
E --> F["ssh_session_spawn or ssh_exec<br/>Run commands on the remote host"]
F --> G["pty_read<br/>Inspect build/test/runtime output"]
G --> H{"Need another edit or rerun?"}
H -- "Yes" --> E
H -- "No" --> I["ssh_unmount and ssh_disconnect"]
Tool Surface
PTY tools
pty_spawn: start a local PTY processpty_write: send input to a running PTY sessionpty_read: page through retained output, optionally filtering by regex patternpty_list: list known PTY sessionspty_kill: stop a PTY session withsigint,sigterm, orsigkillpty_wait: wait for a PTY session to exit
pty_read and initial output capture support three views:
plain: ANSI stripped textansi: ANSI-preserving textraw: raw buffer view
SSH tools
ssh_connect: create or reuse an SSH connection handlessh_list: list SSH connections and mountsssh_session_spawn: start a remote PTY session over an existing SSH connectionssh_exec: run a remote script over an existing SSH connectionssh_read_file: read a UTF-8 text file from the remote hostssh_write_file: write a UTF-8 text file to the remote hostssh_list_dir: list one remote directory levelssh_mkdir: create a remote directoryssh_mount: mount a remote path locally throughsshfsssh_unmount: unmount a mounted remote pathssh_disconnect: disconnect and optionally clean up related resources
SSH mount requirements
ssh_mount depends on the local machine being able to mount a remote filesystem.
To use it locally, you need:
- a FUSE implementation installed and available
sshfsinstalled and available inPATH, or configured viaPTY_MCP_SSHFS_BIN_PATH
In practice:
- macOS:
macFUSEandsshfs - Linux:
fuseorfuse3, plussshfs
Without local FUSE support and sshfs, SSH connections and remote command execution can still work, but ssh_mount will not.
Agents can read the built-in mount setup resources and walk the user through the correct FUSE/sshfs installation steps for the current platform.
MCP Resources
The server also exposes structured resources:
pty://sessionspty://sessions/{id}pty://sessions/{id}/bufferpty://sessions/{id}/tailssh://connectionsssh://connections/{id}ssh://mountsssh://mounts/{id}ssh://docs/mount-setupssh://docs/mount-setup/{platform}
These are useful when the client wants a snapshot without invoking a tool.
The SSH mount setup guides are meant for agents: when sshfs/FUSE support is missing, the
agent can read these resources and then guide the user through the right local installation flow
for the current platform instead of guessing.
Runtime Requirements
PTY
Local PTY support is built in. Commands are subject to policy checks for:
- allowed working-directory roots
- allowed/denied commands
- allowed/denied environment variables
SSH
SSH features depend on host binaries:
sshis required for SSH connections and remote executionsshfsis required forssh_mountumountis used for unmountingdiskutilis additionally probed on macOS
On macOS, the server also probes macFUSE / osxfuse availability as part of SSH mount capability detection.
Configuration
All configuration is read from environment variables at startup.
Core settings
PTY_MCP_SESSION_LIMIT: max number of tracked PTY sessions, default32PTY_MCP_DEFAULT_READ_LIMIT: default line count for reads, default200PTY_MCP_MAX_BUFFER_LINES: retained lines per session buffer, default50000PTY_MCP_ALLOWED_CWD_ROOTS: colon-separated allowed working-directory roots, default current directoryPTY_MCP_ALLOWED_COMMANDS: comma-separated allowlist of command namesPTY_MCP_DENIED_COMMANDS: comma-separated denylist of command namesPTY_MCP_ALLOWED_ENV_VARS: comma-separated allowlist of env var namesPTY_MCP_DENIED_ENV_VARS: comma-separated denylist of env var names
By default, the following env vars are denied:
LD_PRELOADLD_LIBRARY_PATHDYLD_INSERT_LIBRARIESDYLD_LIBRARY_PATH
SSH settings
PTY_MCP_SSH_BIN_PATH: explicit path tosshPTY_MCP_SSHFS_BIN_PATH: explicit path tosshfsPTY_MCP_UMOUNT_BIN_PATH: explicit path toumountPTY_MCP_DISKUTIL_BIN_PATH: explicit path todiskutilPTY_MCP_SSH_MANAGED_MOUNT_ROOT: managed local root for SSH mountsPTY_MCP_SSH_ALLOWED_HOSTS: comma-separated host allowlist, supports*and*.example.comPTY_MCP_SSH_DENIED_HOSTS: comma-separated host denylistPTY_MCP_SSH_ALLOWED_USERS: comma-separated SSH user allowlistPTY_MCP_SSH_ALLOWED_AUTH_KINDS: comma-separated auth allowlist, values:host_alias,ssh_agent,identity_pathPTY_MCP_SSH_ALLOW_EXPLICIT_MOUNT_PATHS: whether arbitrary local mount paths are allowed, defaulttruePTY_MCP_SSH_ALLOWED_MOUNT_ROOTS: colon-separated allowed local mount rootsPTY_MCP_SSH_PORT_MIN: minimum allowed SSH port, default1PTY_MCP_SSH_PORT_MAX: maximum allowed SSH port, default65535
When PTY_MCP_SSH_MANAGED_MOUNT_ROOT is set, it is automatically added to the allowed cwd roots and mount roots.
Example
Example with a tighter policy:
[]
= "pty-mcp"
[]
= "/Users/alice/work:/tmp/pty-mcp"
= "bash,sh,python,node,cargo"
= "*.example.com,github.com"
= "alice"
= "/tmp/pty-mcp-mounts"
Development
License
MIT