Terminal Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Terminal Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<!-- mcp-name: io.github.mkpvishnu/terminal-mcp -->
<p align="center"> <img src="assets/banner.svg" width="800" alt="terminal-mcp banner"/> </p>
<p align="center"> <strong>MCP server for interactive terminal sessions — SSH, REPLs, database CLIs, and TUI apps.</strong> </p>
<p align="center"> <a href="https://pypi.org/project/terminal-mcp/"><img src="https://img.shields.io/pypi/v/terminal-mcp.svg" alt="PyPI"/></a> <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+"/></a> <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License: MIT"/></a> <a href="https://github.com/mkpvishnu/terminal-mcp/actions/workflows/ci.yml"><img src="https://github.com/mkpvishnu/terminal-mcp/actions/workflows/ci.yml/badge.svg" alt="CI"/></a> <a href="https://github.com/mkpvishnu/terminal-mcp/actions/workflows/codeql.yml"><img src="https://github.com/mkpvishnu/terminal-mcp/actions/workflows/codeql.yml/badge.svg" alt="CodeQL"/></a> </p>
<p align="center"> <a href="https://insiders.vscode.dev/redirect/mcp/install?name=terminal-mcp&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22terminal-mcp%22%5D%7D"><img src="https://img.shields.io/badge/VS_Code-Install-007ACC?logo=visual-studio-code&logoColor=white" alt="Install in VS Code"/></a> <a href="https://insiders.vscode.dev/redirect/mcp/install?name=terminal-mcp&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22terminal-mcp%22%5D%7D"><img src="https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?logo=visual-studio-code&logoColor=white" alt="Install in VS Code Insiders"/></a> <a href="cursor://anysphere.cursor-mcp/install?name=terminal-mcp&config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJ0ZXJtaW5hbC1tY3AiXX0="><img src="https://img.shields.io/badge/Cursor-Install-F37626?logo=cursor&logoColor=white" alt="Install in Cursor"/></a> <a href="#register-with-claude-desktop"><img src="https://img.shields.io/badge/Claude_Desktop-Install-cc785c?logo=claude&logoColor=white" alt="Install in Claude Desktop"/></a> </p>
<p align="center"> <img src="assets/demo.gif" alt="terminal-mcp demo" width="700"/> </p>
If you've hit any of these limitations with Claude Code, terminal-mcp solves them:
terminal-mcp fills this gap by exposing MCP tools that create and manage real PTY sessions. Each session runs as a persistent child process; you send input, special keys, and control characters and read output across multiple tool calls for as long as the session lives.
session_interact combines send and read, halving LLM round tripssession_wait_for blocks until a pattern matches in output — no more guessing timeoutsrm -rf, DROP TABLE, curl|sh, etc.) and requires confirmationtail (default), head_tail (preserves beginning and end), tail_only (for build logs), nonetail, head_tail, tail_only, none) to prevent context overflow while preserving the most useful outputTERMINAL_MCP_* environment variablespip install terminal-mcp| Client | Status | Install |
|---|---|---|
| Claude Code (CLI) | ✅ Supported | ~/.claude.json or .mcp.json |
| Claude Desktop | ✅ Supported | One-click install |
| VS Code (Copilot Chat) | ✅ Supported | One-click install or .vscode/mcp.json |
| Cursor | ✅ Supported | One-click install or Settings → MCP |
| Windsurf | ✅ Supported | ~/.codeium/windsurf/mcp_config.json |
Recommended — no install needed:
uvx terminal-mcpOr install via pip:
pip install terminal-mcpOr from source:
git clone https://github.com/mkpvishnu/terminal-mcp.git
cd terminal-mcp
pip install -e ".[dev]"Add to ~/.claude.json (or project .mcp.json):
{
"mcpServers": {
"terminal": {
"command": "uvx",
"args": ["terminal-mcp"]
}
}
}Add to your claude_desktop_config.json:
{
"mcpServers": {
"terminal": {
"command": "uvx",
"args": ["terminal-mcp"]
}
}
}Click the one-click install badge above, or add to .vscode/mcp.json:
{
"servers": {
"terminal-mcp": {
"command": "uvx",
"args": ["terminal-mcp"]
}
}
}session_exec exec="echo hello from terminal-mcp"<details> <summary><strong>SSH session to a remote server</strong></summary>
session_create command="ssh [email protected]" label="prod-ssh"
session_read session_id="a1b2c3d4" timeout=5.0
session_send session_id="a1b2c3d4" password="mypassword"
session_send session_id="a1b2c3d4" input="df -h"
session_read session_id="a1b2c3d4"
session_close session_id="a1b2c3d4"</details>
<details> <summary><strong>Python REPL</strong></summary>
session_create command="python3" label="repl"
session_read session_id="e5f6g7h8"
session_send session_id="e5f6g7h8" input="import math"
session_send session_id="e5f6g7h8" input="print(math.sqrt(144))"
session_read session_id="e5f6g7h8"
session_close session_id="e5f6g7h8"</details>
<details> <summary><strong>TUI navigation with special keys</strong></summary>
session_create command="python3 -m openclaw configure" label="openclaw"
session_read session_id="x1y2z3w4" timeout=3.0
session_send session_id="x1y2z3w4" key="down"
session_send session_id="x1y2z3w4" key="down"
session_send session_id="x1y2z3w4" key="enter"
session_read session_id="x1y2z3w4"
session_send session_id="x1y2z3w4" key="tab"
session_read session_id="x1y2z3w4"
session_close session_id="x1y2z3w4"</details>
<details> <summary><strong>Auto TUI detection and diff mode</strong></summary>
session_create command="htop" label="monitor"
session_read session_id="a1b2c3d4"
→ auto-detects TUI, returns snapshot with mode_used="snapshot", tui_active=true
session_read session_id="a1b2c3d4" mode="diff"
→ returns only changed lines since last read
session_read session_id="a1b2c3d4" mode="diff"
→ returns only lines that changed, minimizing tokens
session_close session_id="a1b2c3d4"</details>
<details> <summary><strong>One-shot command execution</strong></summary>
session_exec exec="ls -la /tmp"
session_exec exec="python3 -c 'print(42)'" command="bash" timeout=10.0</details>
<details> <summary><strong>Send + read in one call (session_interact)</strong></summary>
session_create command="bash" label="demo"
session_interact session_id="a1b2c3d4" input="ls -la" wait_for="\\$\\s*$" timeout=5.0
session_interact session_id="a1b2c3d4" input="whoami" wait_for="\\$"
session_close session_id="a1b2c3d4"</details>
<details> <summary><strong>Wait for specific output pattern</strong></summary>
session_create command="bash" label="build"
session_send session_id="a1b2c3d4" input="npm run build"
session_wait_for session_id="a1b2c3d4" pattern="Build complete|ERROR" timeout=60.0
session_close session_id="a1b2c3d4"</details>
<details> <summary><strong>Dangerous command confirmation</strong></summary>
session_send session_id="a1b2c3d4" input="rm -rf /tmp/old"
→ returns: requires_confirmation=true, reason="Matched dangerous pattern: ..."
session_send session_id="a1b2c3d4" input="rm -rf /tmp/old" confirmed=true
→ executes the command</details>
<details> <summary><strong>Sending Ctrl-C to interrupt</strong></summary>
session_send session_id="a1b2c3d4" control_char="c"
session_read session_id="a1b2c3d4"</details>
Spawn a persistent PTY terminal session.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
command | string | Yes | — | Shell command to run (e.g. bash, python3, ssh user@host) |
label | string | No | command name | Human-readable label |
rows | integer | No | 24 | Terminal height |
cols | integer | No | 80 | Terminal width |
idle_timeout | integer | No | 1800 | Seconds before auto-close |
enable_snapshot | boolean | No | true | Deprecated: snapshot is now always enabled |
scrollback_lines | integer | No | 1000 | Scrollback history lines |
Returns: session_id, label, pid, created_at, snapshot_available
Send input text, a control character, or a special key to an active session. Only one of input, control_char, key, or password may be provided per call.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | — | Session ID from session_create |
input | string | No | — | Text to send |
press_enter | boolean | No | true | Append carriage return after input |
control_char | string | No | — | Control character: c d z l ] |
key | string | No | — | Special key (see table below) |
password | string | No | — | Password or secret (not logged) |
confirmed | boolean | No | false | Bypass dangerous command gate |
Returns: bytes_sent — or requires_confirmation, reason if the command matches a dangerous pattern
<details> <summary>Supported special keys</summary>
| Key | Description | Key | Description |
|---|---|---|---|
up | Arrow up | f1–f12 | Function keys |
down | Arrow down | home | Home |
left | Arrow left | end | End |
right | Arrow right | page-up | Page Up |
tab | Tab | page-down | Page Down |
shift-tab | Shift+Tab | insert | Insert |
escape | Escape | delete | Delete |
enter | Enter | backspace | Backspace |
</details>
<details> <summary>Supported control characters</summary>
| Char | Signal | Description |
|---|---|---|
c | SIGINT | Interrupt (Ctrl-C) |
d | EOF | End of file / logout (Ctrl-D) |
z | SIGTSTP | Suspend (Ctrl-Z) |
l | — | Clear screen (Ctrl-L) |
] | — | Telnet escape |
</details>
Resize the terminal window of an active session.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | — | Session ID |
rows | integer | Yes | — | New terminal height |
cols | integer | Yes | — | New terminal width |
Returns: rows, cols
Read output from a session.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | — | Session ID |
mode | string | No | auto | auto, stream, snapshot, or diff |
timeout | number | No | 2.0 | Settle timeout in seconds (stream/auto mode) |
strip_ansi | boolean | No | true | Strip ANSI escape sequences |
scrollback | integer | No | — | Lines of scrollback history (snapshot mode) |
truncation | string | No | config default | Truncation mode: tail, head_tail, tail_only, none |
Returns: output, bytes_read, prompt_detected, is_alive, truncated, tui_active, snapshot_available, mode_used, changed_lines (diff mode), is_first_read (diff mode), total_lines (scrollback), osc133, command_state, exit_code, command_complete (shell integration)
Terminate a session gracefully (EOF → SIGHUP → SIGKILL).
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID to close |
Returns: exit_status — or already_closed: true if the session was already terminated (idempotent)
Execute a command in a temporary session and return output. The session is automatically cleaned up.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
exec | string | Yes | — | Command to execute |
command | string | No | bash | Shell to use |
timeout | number | No | 5.0 | Seconds to wait for output |
rows | integer | No | 24 | Terminal height |
cols | integer | No | 80 | Terminal width |
truncation | string | No | config default | Truncation mode: tail, head_tail, tail_only, none |
Returns: output, bytes_read, session_id, truncated
Send input and read output in a single call. Combines session_send + session_read to halve round trips. Optionally waits for a regex pattern in the output.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | — | Session ID |
input | string | No | — | Text to send |
press_enter | boolean | No | true | Append carriage return after input |
control_char | string | No | — | Control character: c d z l ] |
key | string | No | — | Special key (see session_send) |
password | string | No | — | Password or secret (not logged) |
wait_for | string | No | — | Regex pattern to wait for in output |
timeout | number | No | 5.0 | Seconds to wait for output |
strip_ansi | boolean | No | true | Strip ANSI escape sequences |
confirmed | boolean | No | false | Bypass dangerous command gate |
read_mode | string | No | stream | Read mode: auto, stream, snapshot, diff |
truncation | string | No | config default | Truncation mode: tail, head_tail, tail_only, none |
Returns: output, bytes_read, bytes_sent, matched (when wait_for used), prompt_detected, is_alive, truncated, tui_active, mode_used, snapshot_available
Read output from a session until a regex pattern matches or timeout expires. Use this instead of session_read when you know what output to expect.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
session_id | string | Yes | — | Session ID |
pattern | string | Yes | — | Regex pattern to wait for in output |
timeout | number | No | 30.0 | Max seconds to wait |
strip_ansi | boolean | No | true | Strip ANSI escape sequences |
truncation | string | No | config default | Truncation mode: tail, head_tail, tail_only, none |
Returns: output, bytes_read, matched, prompt_detected, is_alive, truncated
List all active sessions with their status and idle time.
Returns: sessions (array with tui_active, snapshot_available per session), count
flowchart LR
Client[AI Client] -->|MCP JSON-RPC| Server[terminal-mcp]
Server --> SM[Session Manager]
SM --> S1[PTY 1: bash]
SM --> S2[PTY 2: python3]
SM --> S3[PTY 3: ssh user@host]
S1 & S2 & S3 -.->|PTY output| Reader[Reader Thread]
Reader -.->|buffer| ServerstateDiagram-v2
[*] --> Active : session_create
state Active {
Idle --> Sending : session_send
Sending --> Idle
Idle --> Reading : session_read
Reading --> Idle
Idle --> Resizing : session_resize
Resizing --> Idle
}
Active --> [*] : session_close
Active --> [*] : idle_timeoutEach session is backed by a real PTY allocated via pexpect.spawn. The design has four main parts:
Background reader thread. A daemon thread continuously reads from the PTY file descriptor in 4096-byte chunks and appends bytes to an in-memory buffer. The thread is lock-protected and dies automatically when the child process exits.
Output settling (stream mode). session_read in stream mode polls the buffer until no new bytes have arrived for timeout seconds (default 2s), then returns everything written since the last read call. A hard ceiling of timeout + 10s prevents infinite blocking.
Snapshot mode (always on). All PTY output is fed into a pyte virtual screen buffer. In auto mode (the default), session_read automatically detects TUI applications via alternate screen buffer sequences and returns a rendered snapshot. The diff mode returns only changed lines since the last read, minimizing tokens for TUI monitoring.
Idle cleanup. SessionManager runs a background cleanup loop (every 60s by default) that closes sessions idle longer than their idle_timeout. The default timeout is 30 minutes. Concurrent sessions are capped at 10 by default.
All settings can be overridden via environment variables prefixed with TERMINAL_MCP_:
| Setting | Env Var | Default | Description |
|---|---|---|---|
max_sessions | TERMINAL_MCP_MAX_SESSIONS | 10 | Maximum concurrent sessions |
idle_timeout | TERMINAL_MCP_IDLE_TIMEOUT | 1800 | Seconds before auto-close |
default_rows | TERMINAL_MCP_DEFAULT_ROWS | 24 | Default terminal height |
default_cols | TERMINAL_MCP_DEFAULT_COLS | 80 | Default terminal width |
read_settle_timeout | TERMINAL_MCP_READ_SETTLE_TIMEOUT | 2.0 | Output settle timeout |
max_output_bytes | TERMINAL_MCP_MAX_OUTPUT_BYTES | 100000 | Max bytes per read |
cleanup_interval | TERMINAL_MCP_CLEANUP_INTERVAL | 60 | Seconds between cleanup |
max_buffer_bytes | TERMINAL_MCP_MAX_BUFFER_BYTES | 1000000 | Per-session PTY buffer cap in bytes |
safety_gate | TERMINAL_MCP_SAFETY_GATE | on | Dangerous command gate (off to disable) |
dangerous_patterns | TERMINAL_MCP_DANGEROUS_PATTERNS | built-in | Extra patterns (semicolon-separated regexes) |
truncation_mode | TERMINAL_MCP_TRUNCATION_MODE | tail | Default truncation strategy (tail, head_tail, tail_only, none) |
Per-session parameters: rows, cols, idle_timeout, and scrollback_lines can be set per session via session_create. There is no global env var for scrollback_lines — it defaults to 1000 lines per session. Set scrollback_lines=0 to disable scrollback history.
Since terminal-mcp runs as a subprocess of your MCP client, environment variables must be configured in the client's MCP server configuration — not in your shell profile (.bashrc, .zshrc, etc.).
Claude Code (~/.claude.json or project .mcp.json):
{
"mcpServers": {
"terminal": {
"command": "uvx",
"args": ["terminal-mcp"],
"env": {
"TERMINAL_MCP_MAX_SESSIONS": "20",
"TERMINAL_MCP_IDLE_TIMEOUT": "3600",
"TERMINAL_MCP_SAFETY_GATE": "off",
"TERMINAL_MCP_TRUNCATION_MODE": "head_tail"
}
}
}
}Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"terminal": {
"command": "uvx",
"args": ["terminal-mcp"],
"env": {
"TERMINAL_MCP_MAX_SESSIONS": "20",
"TERMINAL_MCP_IDLE_TIMEOUT": "3600"
}
}
}
}VS Code / Cursor (.vscode/mcp.json):
{
"servers": {
"terminal-mcp": {
"command": "uvx",
"args": ["terminal-mcp"],
"env": {
"TERMINAL_MCP_MAX_SESSIONS": "20",
"TERMINAL_MCP_IDLE_TIMEOUT": "3600"
}
}
}
}Windsurf (~/.codeium/windsurf/mcp_config.json):
{
"mcpServers": {
"terminal": {
"command": "uvx",
"args": ["terminal-mcp"],
"env": {
"TERMINAL_MCP_MAX_SESSIONS": "20"
}
}
}
}You can also set env vars directly in your shell when running the server manually for testing:
TERMINAL_MCP_MAX_SESSIONS=20 TERMINAL_MCP_SAFETY_GATE=off uvx terminal-mcpsession_interact with wait_for no longer matches the echoed input text. Pattern matching now starts from a buffer position anchored before the send, so only genuinely new output is matched (fixes #6)session_interact with wait_for no longer matches pre-existing unread buffer content (startup banners, previous command output). Uses a monotonic absolute byte counter that survives buffer trims (fixes #7)success: true, already_closed: true instead of a not_found error. Safe for finally-style cleanup (fixes #8)strip_ansi now handles Kitty keyboard protocol sequences, application keypad mode (ESC=/ESC>), DCS sequences, secondary DA responses, and tilde-terminated CSI sequences that leaked through during TUI exit transitions (fixes #9)is_alive property now uses exception-safe _is_alive() internally, preventing PtyProcessError when child processes exit before waitpid can reap them. Fixes flaky CI failures on Linux runnersESC[?1049h, ESC[?47h, ESC[?1047h) and switches session_read to snapshot mode. New mode="auto" is now the defaultsession_read with mode="diff" returns only changed screen lines since last read, with 1-indexed line numbers and is_first_read flagtruncate_output_smart() with four strategies: tail (keep beginning), head_tail (keep first 30% + last 70% with line-count marker), tail_only (keep end, ideal for build logs), none (disable truncation). Configurable via truncation parameter on all read tools or TERMINAL_MCP_TRUNCATION_MODE env varenable_snapshot=true). enable_snapshot parameter deprecatedauto, stream, snapshot, or diffread_snapshot() and read_diff() now acquire buffer lock before reading pyte screentui_active, snapshot_available, mode_used added to read responses; snapshot_available added to create and list responseswait_for regex patternrm -rf, DROP TABLE, curl|sh, chmod 777, etc.) and returns requires_confirmation instead of executing. Resend with confirmed=true to proceed. 17 built-in patterns, extensible via TERMINAL_MCP_DANGEROUS_PATTERNS env var, disable with TERMINAL_MCP_SAFETY_GATE=offcommand_state, exit_code, and command_complete fields for precise command completion trackingTERMINAL_MCP_MAX_BUFFER_BYTES), prevents unbounded memory growth on long-running sessionsasyncio.to_thread(), unblocking the event loop for concurrent MCP requestsstrip_ansi parameter now correctly applied in snapshot and scrollback read modessession_exec now applies max_output_bytes truncation to prevent context overflowkill, systemd)close() now handles pexpect exceptions when child process is already reapedimport atexit from server.pymcp-name marker and server.json for official MCP registrymax_output_bytes (100KB default)TERMINAL_MCP_* env varspassword parameter on session_send for credentials (redacted from logs)pyte.HistoryScreen with configurable history depth; scrollback param on session_readpip install terminal-mcp via trusted publishing workflowkey parameter on session_sendinput, control_char, and key are now validated as mutually exclusivepip install -e ".[dev]"
pytest tests/ -vContributions are welcome! Please open an issue first to discuss what you'd like to change.
pytest tests/ -v~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.