Give AI Agents Interactive Terminal Capabilitiesnow has been moved to [termcp](https://github.com/open-mcp-ai/termcp)
中文 | English
interactive-process-mcp is an MCP (Model Context Protocol) server that enables AI Agents (like Claude Code) to start, control, and manage long-running interactive processes.
AI Agents can natively only execute one-shot commands — they run and immediately return results. But many real-world scenarios require multi-turn interaction:
- SSH into a remote server, enter a password first, then run commands
- Debug code line by line in a Python REPL
- Answer
[Y/n]prompts in interactive installers - Use terminal-dependent commands like
top,htop - Run security tools (e.g., impacket) for multi-step operations
In these scenarios, the process keeps running, and the AI Agent needs to repeatedly read and write the process's I/O across multiple conversation turns. interactive-process-mcp is the bridge designed precisely for this purpose.
| Feature | Description |
|---|---|
| Multi-agent session sharing | Multiple AI agents read from the same session simultaneously, each with an independent cursor — no output stealing |
| PTY and Pipe dual mode | PTY mode emulates a real terminal; Pipe mode for simple stdin/stdout interaction |
| Remote deployment | SSE over HTTP transport — Agent and Server can run on different machines |
| Multi-session management | Manage multiple independent processes simultaneously without interference |
| Message persistence | Session records and I/O messages persisted to local JSON files |
| ANSI escape code stripping | Optional automatic removal of terminal control sequences for clean text output |
| Blocking reads with timeout | Agents wait for new output up to a configurable timeout; returns promptly via sync.Cond |
| Atomic send-and-read | send_and_read combines sending + reading in one step |
| Graceful termination | SIGTERM first, then SIGKILL after a configurable grace period |
| PTY resize | Dynamically adjust terminal rows and columns at runtime |
| Session cleanup | Delete exited sessions to prevent resource accumulation |
┌──────┐ SSE/HTTP ┌──────────────┐ Internal SSH ┌──────────┐
│Agent │ ──────────> │ Go Server │ ──────────────> │ PTY/ │
│(MCP) │ │ - MCP API │ (localhost) │ Process │
└──────┘ │ - SSH Server │ └──────────┘
└──────────────┘
│
▼
┌──────────────┐
│ JSON Storage │
│ - sessions │
│ - messages │
└──────────────┘
.
├── cmd/server/main.go # Entry point
├── internal/
│ ├── config/config.go # Configuration with validation
│ ├── mcp/
│ │ ├── server.go # MCP SSE server & tool registration
│ │ └── handlers.go # 13 tool handlers
│ ├── sshserver/server.go # Internal SSH server (gliderlabs/ssh)
│ ├── sshclient/client.go # Internal SSH client (crypto/ssh)
│ ├── session/
│ │ ├── session.go # Session lifecycle (goroutine-safe)
│ │ └── manager.go # Thread-safe session registry
│ ├── buffer/buffer.go # Multi-reader ring buffer (1MB per reader)
│ ├── storage/store.go # Atomic JSON file persistence
│ ├── message/message.go # Message management (per-session mutex)
│ └── ansi/strip.go # ANSI escape code removal
├── pkg/api/types.go # Public types (Session, Message, SessionMode)
├── go.mod
└── go.sum
-
Multi-Reader Ring Buffer: Each agent registers as an independent reader with its own
ringbuffer.RingBufferinstance. Writes broadcast to all readers. Slow readers lose oldest data (overwrite mode) rather than blocking the writer. -
Internal SSH Architecture: The server starts a gliderlabs/SSH server on localhost. Each
start_processcreates an SSH session via crypto/ssh client, leveraging SSH's mature PTY allocation, window resize, signal forwarding, and environment variable passing. -
SSE over HTTP Transport: Unlike traditional stdio-based MCP servers, this server exposes an HTTP endpoint supporting MCP SSE transport. Agents connect remotely, enabling cross-machine deployment.
-
Atomic JSON Persistence: Session metadata and I/O messages are stored via temp-file + fsync + rename, preventing half-written files on crash:
data/sessions.json— Session listdata/messages/{session_id}/index.json— Message indexdata/messages/{session_id}/messages/{msg_id}.json— Message content
-
Session Lifecycle Safety: Exit goroutine is the single authority for
Status/ExitCode(viasync.Once). Terminate is idempotent. Stdin writes are serialized via a dedicated mutex.
AI Agent Flow Process Output
───────────────── ────────────────
start_process(
command="ssh",
args=["deploy@192.168.1.100"],
mode="pty"
)
← "deploy@192.168.1.100's password: "
send_and_read(
text="my_secret_pass",
press_enter=true
)
← "Welcome to Ubuntu 22.04 LTS
deploy@web-server:~$ "
send_and_read(
text="df -h",
press_enter=true
)
← "Filesystem Size Used Avail Use% Mounted on
/dev/sda1 100G 45G 55G 45% /
deploy@web-server:~$ "
terminate_process(session_id="abc123")
start_process(command="python3", mode="pty")
← "Python 3.10.12\n>>> "
send_and_read(text="data = [1, 2, 3, 4, 5]", press_enter=true)
← ">>> "
send_and_read(text="sum(data)", press_enter=true)
← "15\n>>> "
# Agent A starts a monitoring process
start_process(command="top", mode="pty")
→ session_id: "sess-001"
# Agent B joins the same session without stealing output
register_reader(session_id="sess-001")
→ reader_id: 2
# Agent A reads its own cursor
read_output(session_id="sess-001", reader_id=1)
→ "PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND..."
# Agent B reads from the beginning independently
read_output(session_id="sess-001", reader_id=2)
→ "top - 14:32:10 up 3 days, 2:15, 1 user, load average: 0.52, 0.58, 0.59..."
# Agent B is done
unregister_reader(session_id="sess-001", reader_id=2)
# Agent A terminates the session
terminate_process(session_id="sess-001")
delete_session(session_id="sess-001")
start_process(command="ping", args=["-c", "5", "google.com"], name="ping-test")
→ session_id: "a1b2c3"
start_process(command="python3", args=["-m", "http.server", "8080"], name="web-server")
→ session_id: "d4e5f6"
list_sessions()
→ [{id: "a1b2c3", status: "running"}, {id: "d4e5f6", status: "running"}]
read_output(session_id="a1b2c3") → ping statistics
terminate_process(session_id="a1b2c3")
terminate_process(session_id="d4e5f6")
Start an interactive process.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
command |
string | Yes | — | Command to execute |
args |
string[] | No | [] |
Command arguments |
mode |
"pty" | "pipe" | No | "pty" |
I/O mode |
name |
string | No | Auto-generated | Session name |
rows |
integer | No | 24 |
PTY row count (1–1000) |
cols |
integer | No | 80 |
PTY column count (1–1000) |
Returns: { session_id, pid, initial_output }
Send text to a process.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
text |
string | Yes | — | Text to send |
press_enter |
boolean | No | false |
Whether to append a newline |
Read new output since the last read for the given reader.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
reader_id |
integer | No | 0 |
Reader ID (0 = default) |
strip_ansi |
boolean | No | true |
Strip ANSI escape codes |
timeout |
number | No | 5 |
Wait time in seconds (0.1–60) |
max_lines |
integer | No | 0 |
Max lines (0 = unlimited) |
Returns: { output, has_more, lines_returned, bytes_returned }
Atomic operation: send input + wait + read output. Parameters are the union of send_input and read_output.
List all sessions. Returns: { sessions: [...] }
Get session details. Returns: { id, name, command, args, mode, status, exit_code, pid, created_at }
Terminate a process.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
force |
boolean | No | false |
Use SIGKILL directly |
grace_period |
number | No | 5 |
Seconds to wait after SIGTERM (0–60) |
Remove an exited session from the registry.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
Resize PTY dimensions (PTY mode only).
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
rows |
integer | No | 24 |
Row count |
cols |
integer | No | 80 |
Column count |
Register a new independent reader for a session.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
Returns: { reader_id }
Unregister a reader to free resources.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
reader_id |
integer | Yes | — | Reader ID |
List the message index for a session.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
Returns: { messages: [{id, type, created_at, byte_size}, ...] }
Get the content of one or more messages.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id |
string | Yes | — | Session ID |
message_ids |
string[] | No | — | Message IDs to retrieve |
Returns: { messages: [{id, session_id, type, content, created_at, byte_size}, ...] }
go build -o server ./cmd/serverRequirements: Go >= 1.21 / macOS or Linux
./server --host 127.0.0.1 --port 8080 --data-dir ./dataOptions:
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
HTTP server host |
--port |
8080 |
HTTP server port |
--data-dir |
./data |
JSON storage directory |
--ssh-host |
127.0.0.1 |
Internal SSH server host |
--ssh-port |
0 (random) |
Internal SSH server port |
In .claude/settings.json or .mcp.json:
{
"mcpServers": {
"interactive-process": {
"type": "sse",
"url": "http://your-server:8080/sse"
}
}
}Or via CLI:
claude mcp add --transport sse interactive-process http://localhost:8080/sseAny MCP client that supports SSE transport can connect to http://<host>:<port>/sse.
- linux.do — Chinese tech community
MIT