Back to the catalog

pty-mcp

Interactive PTY sessions over MCP: local shells, SSH, serial ports, and persistent remote sessions.

Open source Open in the app JSON README (API)

About

Interactive PTY sessions over MCP: local shells, SSH, serial ports, and persistent remote sessions.

Details

Kind
MCP servers
Topic
No topic detected
Publisher
raychao-oao
Origin
official
Category
ferramentas
Transport
local
Version
0.2.3
Stars
17
Forks
1
Last push
2026-08-17T04:48:37Z
Repository state
ativo
Language
Go
License
MIT
Added
2026-08-29 04:01:16
Updated
2026-08-29 04:01:16
Origin id
io.github.raychao-oao/pty-mcp

README

# pty-mcp

[![pty-mcp MCP server](https://glama.ai/mcp/servers/raychao-oao/pty-mcp/badges/score.svg)](https://glama.ai/mcp/servers/raychao-oao/pty-mcp)

An MCP (Model Context Protocol) server that gives AI agents interactive terminal sessions — local shells, SSH, serial ports, and persistent remote sessions that survive disconnects.

Built for **sysadmins and network engineers** who want AI to help with real server and device management, not just code generation.

![AI agent interacting with Telehack BBS via pty-mcp](docs/screenshots/telehack-cowsay.png)

## Why

AI agents run commands in non-interactive shells. They can't:
- SSH into a server and interact with running processes
- Connect to routers or switches via serial console
- Monitor logs and react when a specific event occurs
- Keep session state across multiple commands
- Wait for a server to reboot and detect when it's back up

pty-mcp solves all of these by providing real PTY sessions over MCP.

Without pty-mcp, AI agents resort to `sleep 30 && check_status` loops — burning CPU cycles and API calls waiting for things to happen. With `wait_for`, the agent blocks server-side until the event occurs. Less polling, less energy, better for polar bears. 🐻‍❄️

## Use Cases

**Server administration**
```
# Reboot a server and wait until it's back online
create_local_session("ping myserver")
read_output(wait_for: "bytes from", timeout: 300)
→ blocks until server responds after reboot (~80s, one tool call)
```

**Network device management**
```
# Connect to a router via serial console
create_serial_session(port: "/dev/ttyUSB0", baud: 9600)
send_input("show interfaces status")
read_output(wait_for: "\\$")
```

**Log monitoring and alerting**
```
# Watch logs and act when something happens
create_ssh_session(host: "prod", user: "admin")
send_input("tail -f /var/log/app.log")
read_output(wait_for: "ERROR|CRITICAL", timeout: 3600)
→ returns the error line + context when it appears
```

**Long-running tasks that survive disconnects**
```
create_ssh_session(host: "server", user: "admin", persistent: true)
send_input("apt upgrade -y")
detach_session()          → close Claude Code, task continues
# Reconnect later to check result
```

## Features

| Feature | Description |
|---------|-------------|
| **Local terminal** | Interactive bash/python/node sessions on local machine |
| **SSH sessions** | Connect to remote hosts with key/password auth, SSH config support |
| **Serial port** | Connect to devices via serial (IoT, embedded, network gear) |
| **Persistent sessions** | Sessions survive SSH disconnects via `ai-tmux` daemon |
| **Attach/Detach** | Detach from a running session, reconnect later |
| **Control keys** | Send ctrl+c, ctrl+d, arrow keys, tab, escape |
| **Settle detection** | Waits for output to settle before returning (smart timeout) |
| **Pattern matching** | `wait_for` blocks until a regex pattern appears in output (v0.2.0) |
| **Bounded memory** | Ring buffer prevents OOM on long-running sessions (v0.2.0) |
| **Resize terminal** | Resize the PTY window for local, SSH, serial, and persistent remote sessions (v0.10.0) |
| **Audit log** | Optional voluntary operation log — record `send_input` commands to a collector for review and traceability (v0.8.0) |
| **Audit redaction** | Credentials, auth headers, and PEM keys are automatically scrubbed before being written to the audit log (v0.10.0) |

## Architecture

```
┌─────────────────────────────────────────────────────┐
│ AI Agent (Claude Code, etc.)                        │
│                                                     │
│  MCP Tools: create_local_session, send_input,       │
│             send_control, read_output, close_session │
└──────────────────────┬──────────────────────────────┘
                       │ JSON-RPC stdio
┌──────────────────────┴──────────────────────────────┐
│ pty-mcp (MCP Server)                                │
│                                                     │
│  Session Manager                                    │
│  ├── LocalSession  (local PTY via creack/pty)       │
│  ├── SSHSession    (remote PTY via x/crypto/ssh)    │
│  ├── SerialSession (serial port via go.bug.st)      │
│  └── RemoteSession (persistent via ai-tmux)         │
└─────────────────────────────────────────────────────┘

Persistent mode (ai-tmux):

  pty-mcp ──SSH──▶ ai-tmux client ──Unix socket──▶ ai-tmux server (daemon)
                                                     ├── PTY: bash
                                                     ├── PTY: ssh admin@router
                                                     └── PTY: tail -f /var/log/syslog
```

## Quick Start

### Claude Code Plugin (recommended)

Installs the binary automatically and registers the MCP server:

```bash
claude plugin marketplace add raychao-oao/pty-mcp
claude plugin install pty-mcp@pty-mcp
```

Restart Claude Code — the binary downloads automatically on session start, then **restart once more** to activate it. No manual `claude mcp add` needed.

**Updating:**

```bash
claude plugin marketplace update pty-mcp
claude plugin update pty-mcp@pty-mcp
```

Restart Claude Code — the new binary downloads automatically on session start, then **restart once more** to apply the update.

### Manual install

**One-line install + register** (macOS / Linux / WSL2):

```bash
curl -fsSL https://raw.githubusercontent.com/raychao-oao/pty-mcp/main/install.sh | sh
claude mcp add pty-mcp -- /usr/local/bin/pty-mcp
```

Restart Claude Code and the tools are available.

<details>
<summary><b>Other install methods</b></summary>

**Download from GitHub Releases:**

Go to [Releases](https://github.com/raychao-oao/pty-mcp/releases), download the binary for your platform, and make it executable:

| Platform | Binary |
|----------|--------|
| macOS (Apple Silicon) | `pty-mcp-darwin-arm64` |
| macOS (Intel) | `pty-mcp-darwin-amd64` |
| Linux (x86_64) / WSL2 | `pty-mcp-linux-amd64` |
| Linux (ARM64) | `pty-mcp-linux-arm64` |

```bash
chmod +x pty-mcp-*
sudo mv pty-mcp-* /usr/local/bin/pty-mcp
claude mcp add pty-mcp -- /usr/local/bin/pty-mcp
```

**Build from source** (requires Go 1.25+):

```bash
go install github.com/raychao-oao/pty-mcp@latest
claude mcp add pty-mcp -- $(go env GOPATH)/bin/pty-mcp
```

</details>

### WSL2 Notes

pty-mcp works in WSL2 out of the box. Use the Linux binary:

```bash
# Inside WSL2
curl -fsSL https://raw.githubusercontent.com/raychao-oao/pty-mcp/main/install.sh | sh
claude mcp add pty-mcp -- /usr/local/bin/pty-mcp
```

### Optional: Install ai-tmux on remote servers

For persistent sessions that survive SSH disconnects, install `ai-tmux` on your remote server:

```bash
# Download for your server's architecture
curl -fsSL https://raw.githubusercontent.com/raychao-oao/pty-mcp/main/install.sh | sh
# Or just copy the binary:
scp /usr/local/bin/ai-tmux your-server:/usr/local/bin/ai-tmux
```

### Usage Examples

Once registered, the AI agent can use these MCP tools:

**Local interactive shell:**
```
create_local_session()                    → {session_id, type: "local"}
send_input(session_id, "cd /tmp && ls")   → {output: "...", is_complete: true}
send_input(session_id, "python3")         → start Python REPL
send_input(session_id, "print('hello')")  → {output: "hello\n>>>"}
send_control(session_id, "ctrl+d")        → exit Python
close_session(session_id)
```

**SSH to remote server:**
```
create_ssh_session(host: "myserver", user: "admin")
send_input(session_id, "top")
send_control(session_id, "ctrl+c")        → stop top
```

**Wait for pattern (v0.2.0):**
```
create_local_session("ping myserver")
read_output(session_id, wait_for: "bytes from", timeout: 300)
→ blocks until server responds or 5 min timeout

send_input(session_id, "docker-compose up")
read_output(session_id, wait_for: "ready|error", timeout: 60, context_lines: 3)
→ returns matched line + 3 lines of context
```

**Send secret / password (v0.3.0):**
```
# AI detects a password prompt, calls send_secret instead of handling the password itself
create_ssh_session(host: "router", user: "admin")
read_output(session_id, wait_for: "Password:")   → session is waiting for input

send_secret(session_id, prompt: "Router admin password:")
→ native GUI dialog appears on the operator's screen (macOS: system dialog,
   WSL2: Windows Get-Credential, Linux: zenity/kdialog)
→ operator types password — it is sent directly to the PTY session
→ AI only sees: {success: true, length: 12}
→ password never appears in AI context or logs
```

**Persistent session (survives SSH disconnect):**
```
create_ssh_session(host: "server", user: "admin", persistent: true)
send_input(session_id, "make build")      → start long build
detach_session(session_id)                → disconnect, build continues

# Later (even after restart):
list_remote_sessions(host: "server", user: "admin")  → see running sessions
create_ssh_session(host: "server", user: "admin", session_id: "abc123")  → reattach
send_input(session_id, "echo $?")         → check build result
```

## MCP Tools

| Tool | Description |
|------|-------------|
| `create_local_session` | Start a local interactive terminal (bash, python3, node, etc.) |
| `create_ssh_session` | SSH to a remote host (supports SSH config aliases) |
| `create_serial_session` | Connect to a serial port device |
| `send_input` | Send a command and wait for output to settle |
| `read_output` | Read output, optionally wait for a pattern (`wait_for`, `timeout`, `context_lines`, `tail_lines`) |
| `send_control` | Send control keys (ctrl+c, ctrl+d, arrows, tab, etc.) |
| `send_secret` | Prompt the human operator for a secret via GUI dialog; sends it to the PTY session without exposing it to AI context or logs ¹ |
| `list_sessions` | List all active sessions |
| `close_session` | Close a session (terminates remote PTY) |
| `detach_session` | Disconnect but keep remote PTY running |
| `resize_session` | Resize the terminal window (rows/cols) for any session type |
| `list_remote_sessions` | List persistent sessions on a remote host |

> ¹ **`send_secret` platform support**: macOS uses a native password dialog (osascript). WSL2 uses `powershell.exe Get-Credential` (Windows GUI dialog). Linux with a display server uses `zenity` or `kdialog`. Headless Linux falls back to `/dev/tty`. If the operator doesn't respond within 60 seconds, the dialog is dismissed and the call returns a timeout error — it does not fall through to another dialog or wait again. Tool calls now run concurrently, so an unanswered dialog no longer blocks other sessions either way; the 60s bound exists so a `send_secret` call itself doesn't sit open indefinitely, and cancelling it (e.g. pressing ESC in Claude Code) dismisses the dialog immediately instead of waiting out the timeout.

## Audit Log

pty-mcp includes an optional audit log feature that records every `send_input` command to a central collector. This lets teams review and trace what AI agents did during a session.

> **Important:** This is a **voluntary, self-reporting** operation log. It relies on operators choosing to enable it and run the collector. Because pty-mcp runs on the operator's own machine, there is no technical mechanism to enforce logging — a non-compliant operator could simply run pty-mcp without audit enabled. This feature provides traceability for teams that *want* it, but **it is not a substitute for system-level audit tools** (e.g., auditd, syslog forwarding, SSH session recording) in environments where audit compliance is required.

### What it records

- Timestamp, operator identity, session ID, session type (local/ssh/serial), target host
- The exact input sent via `send_input` (including `raw=true` inputs like menu selections)
- Output snippet (first 2 KB) after each command
- A `cmd_id` linking the command to its output

`send_secret` is **never logged** — secrets entered via the GUI dialog do not appear in the audit log.

Commands and output snippets are **automatically redacted** before being written. The following patterns are replaced with `[REDACTED]` or `[PRIVATE KEY REDACTED]`:
- Key-value credentials: `password=`, `passwd:`, `token=`, `api_key=`, `access_key=`, `auth_token=`, `secret=`
- HTTP Authorization headers: `Authorization: Bearer …`, `Authorization: Basic …`, `Authorization: Token …`
- PEM private key blocks: `-----BEGIN RSA PRIVATE KEY-----` / `-----BEGIN OPENSSH PRIVATE KEY-----`

### Setup

**Each operator runs once** to create their config and generate a token:

```bash
pty-mcp audit init
```

This creates `~/.config/pty-mcp/config` (chmod 600) with a randomly generated token and prints the token to share with the collector admin.

**The collector admin** starts the server (using the token from init output):

```bash
PTY_MCP_AUDIT_TOKEN=<token-from-init> \
  pty-mcp audit serve --port 9099 --log /var/log/pty-mcp-audit.jsonl
```

**Enable audit** after setting the collector URL in the config:

```bash
# Edit config and set: audit-url=http://your-collector:9099
pty-mcp audit enable
# Restart Claude Code to apply
```

To temporarily stop logging without losing your config:

```bash
pty-mcp audit disable
```

Operators without a config file are unaffected — audit is off by default.

### Audit modes

| Mode | Behaviour |
|------|-----------|
| `best-effort` (default) | Commands execute regardless of whether the log was written; entries are queued and retried in the background |
| `strict` | `send_input` is rejected if the audit entry cannot be delivered; use when logging is a team policy requirement |

### Reviewing logs

Logs are stored as JSONL (one JSON object per line), readable with standard tools:

```bash
# All commands by operator ray
grep '"user":"ray"' /var/log/pty-mcp-audit.jsonl | jq .

# Commands sent to a specific host
jq 'select(.target == "root@prod01")' /var/log/pty-mcp-audit.jsonl
```

## ai-tmux: Persistent Terminal Daemon

`ai-tmux` is a lightweight daemon that runs on remote servers, keeping PTY sessions alive across SSH disconnects. Think of it as tmux designed for AI agents.

### Install on remote server

```bash
# Cross-compile for Linux
GOOS=linux GOARCH=amd64 go build -o ai-tmux-linux ./cmd/ai-tmux/

# Copy to server
scp ai-tmux-linux server:~/ai-tmux
ssh server "chmod +x ~/ai-tmux && sudo mv ~/ai-tmux /usr/local/bin/ai-tmux"
```

### How it works

- `ai-tmux server` — daemon mode, listens on Unix socket, manages PTY sessions
- `ai-tmux client` — bridge mode, forwards JSON protocol over stdin/stdout (used by pty-mcp over SSH)
- `ai-tmux list` — list active sessions

The daemon auto-starts when pty-mcp connects with `persistent: true`. Sessions are reaped after 30 minutes of inactivity.

pty-mcp checks `ai-tmux --version` on the remote host before opening a session and returns a clear error if the binary is missing or below the minimum required version — no cryptic connection errors.

## SSH Config Support

pty-mcp reads `~/.ssh/config` to resolve host aliases:

```
# ~/.ssh/config
Host myserver
    HostName 192.168.1.100
    User admin
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
```

```
create_ssh_session(host: "myserver", user: "admin")
# Automatically resolves hostname, port, and identity file
```

## Requirements

- Go 1.25+
- For serial: appropriate device permissions
- For persistent sessions: `ai-tmux` binary on remote server

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for version history.

## License

MIT

More