npm package: @iflow-mcp/amol21p-interactive-terminal
Description: MCP server that gives AI agents (Claude Code, Cursor, Windsurf) real interactive terminal sessions — run REPLs, SSH, databases, Docker, and any interactive CLI with clean output and smart completion detection
Latest version: 1.0.9

# mcp-interactive-terminal

[![npm version](https://img.shields.io/npm/v/mcp-interactive-terminal.svg)](https://www.npmjs.com/package/mcp-interactive-terminal)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org/)

MCP server that gives AI agents (Claude Code, Cursor, Windsurf, etc.) real interactive terminal sessions. Run REPLs, SSH, database clients, and any interactive CLI — with clean text output, smart completion detection, and 7-layer security.

## Why This Exists

AI coding agents can't handle interactive commands. There's no PTY, no stdin streaming. You can't run `rails console`, `python`, `psql`, `ssh`, or any REPL through them. This MCP server fixes that.

```
AI Agent (Claude Code, Cursor, etc.)
    ↕  MCP (JSON-RPC over stdio)
mcp-interactive-terminal
    ↕  node-pty + xterm-headless
Interactive Process (rails console, python, psql, ssh, bash...)
    ↕
Clean text output (exactly what a human would see)
```

## Install

### Claude Code

```bash
claude mcp add terminal -- npx -y mcp-interactive-terminal
```

That's it. The server is now available. Ask Claude to "open a python REPL and calculate 2**100".

### Cursor

Go to **Settings > MCP Servers**, click **Add Server**, and enter:

```json
{
  "mcpServers": {
    "terminal": {
      "command": "npx",
      "args": ["-y", "mcp-interactive-terminal"]
    }
  }
}
```

### Windsurf

Add to your MCP configuration:

```json
{
  "mcpServers": {
    "terminal": {
      "command": "npx",
      "args": ["-y", "mcp-interactive-terminal"]
    }
  }
}
```

### VS Code (GitHub Copilot)

Add to your `.vscode/mcp.json`:

```json
{
  "servers": {
    "terminal": {
      "command": "npx",
      "args": ["-y", "mcp-interactive-terminal"]
    }
  }
}
```

### Any MCP Client

The server communicates over stdio using the [Model Context Protocol](https://modelcontextprotocol.io/). Any MCP-compatible client can use it with the same `npx -y mcp-interactive-terminal` command.

## Real-World Examples

### Rails Console

```
You: "Open rails console for staging and check the user count"

Agent creates session → bash
Agent sends: cd /path/to/app && rails console -e staging
Agent sends: User.count
Agent returns: 1,847,293
```

### Python REPL

```
You: "Open python and test my sorting algorithm"

Agent creates session → python3
Agent sends: def quicksort(arr): ...
Agent sends: quicksort([3, 1, 4, 1, 5, 9])
Agent returns: [1, 1, 3, 4, 5, 9]
```

### Database Client

```
You: "Connect to postgres and show me the largest tables"

Agent creates session → psql -U myuser mydb
Agent sends: SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::text)) ...
Agent returns: formatted table of results
```

### SSH

```
You: "SSH into the staging server and check disk usage"

Agent creates session → ssh user@staging.example.com
Agent sends: df -h
Agent returns: disk usage table
```

### Docker

```
You: "Open a shell in my running container and check the logs"

Agent creates session → docker exec -it my-container bash
Agent sends: tail -100 /var/log/app.log
Agent returns: last 100 log lines
```

### Node.js REPL

```
You: "Open node and test the date parsing logic"

Agent creates session → node
Agent sends: new Date('2024-02-29').toISOString()
Agent returns: 2024-02-29T00:00:00.000Z
```

## Tools

The server exposes 7 MCP tools:

### `create_session` — Spawn an interactive process

```json
{ "command": "python3", "name": "my-python", "cwd": "/project" }
→ { "session_id": "a1b2c3d4", "name": "my-python", "pid": 12345 }
```

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `command` | Yes | — | Command to run (bash, python3, psql, ssh, etc.) |
| `args` | No | `[]` | Command arguments |
| `name` | No | auto | Human-readable session name |
| `cwd` | No | server cwd | Working directory |
| `env` | No | `{}` | Additional environment variables |
| `cols` | No | `120` | Terminal columns |
| `rows` | No | `40` | Terminal rows |

### `send_command` — Send input and get output

```json
{ "session_id": "a1b2c3d4", "input": "1 + 1" }
→ { "output": "2", "is_complete": true, "is_alive": true }
```

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| `session_id` | Yes | — | Target session |
| `input` | Yes | — | Command/input to send (newline appended automatically) |
| `timeout_ms` | No | `5000` | Max wait time for output |
| `max_output_chars` | No | `20000` | Truncate output beyond this |

Dangerous commands (`rm -rf`, `DROP TABLE`, `curl|bash`, etc.) are blocked — the agent must use `confirm_dangerous_command` first.

### `read_output` — Read terminal screen (read-only)

```json
{ "session_id": "a1b2c3d4" }
→ { "output": ">>> ", "is_alive": true }
```

Safe to auto-approve — this only reads, never sends input.

### `list_sessions` — List active sessions (read-only)

```json
→ [{ "session_id": "a1b2c3d4", "name": "my-python", "command": "python3", "pid": 12345, "is_alive": true }]
```

Safe to auto-approve.

### `close_session` — Kill a session

```json
{ "session_id": "a1b2c3d4" }
→ { "success": true }
```

### `send_control` — Send control characters

```json
{ "session_id": "a1b2c3d4", "control": "ctrl+c" }
→ { "output": "^C\n>>>" }
```

Supported: `ctrl+c`, `ctrl+d`, `ctrl+z`, `ctrl+l`, `ctrl+r`, `tab`, `escape`, `up`, `down`, `left`, `right`, `enter`, `backspace`, `delete`, `home`, `end`, and more.

### `confirm_dangerous_command` — Two-step safety confirmation

```json
{ "session_id": "a1b2c3d4", "input": "rm -rf /tmp/old", "justification": "Cleaning up stale temp files from failed build" }
→ { "output": "...", "is_complete": true, "is_alive": true }
```

Required when `send_command` detects a dangerous pattern. The agent must explain why the command is necessary. This is a **separate tool** — even if `send_command` is auto-approved, this requires its own permission.

## How It Works

### Two Terminal Modes

**PTY mode** (default) — uses `node-pty` + `@xterm/headless` (the same terminal emulator as VS Code):

- Clean output — the AI sees exactly what a human would see on screen
- Cursor positioning, progress bars, `\r` overwrites all render correctly
- Full keyboard: arrow keys, tab completion, ctrl+c/d/z, home/end
- Terminal resize, TUI apps (vim, htop, top), 256-color, 1000-line scrollback

**Pipe mode** (automatic fallback) — activates when node-pty can't load (e.g., in sandboxed environments):

- Interactive sessions still work via `child_process.spawn` with auto-injected flags (`python -u -i`, `bash -i`, etc.)
- ANSI codes stripped, control keys still work
- No terminal emulation, but covers the basics

The mode is selected automatically — PTY is tried first, pipe mode kicks in if it fails.

### What the AI sees: PTY vs Pipe

| Scenario | PTY mode | Pipe mode |
|----------|----------|-----------|
| `printf "\rProgress: 3/3"` | `Progress: 3/3` | `Progress: 1/3Progress: 2/3Progress: 3/3` |
| ANSI colors | Stripped cleanly | Stripped via regex |
| vim, htop, top | Readable screen | Garbled |
| Arrow keys, tab completion | Works | Works |
| Terminal resize | Works | No-op |

### Smart "Command Done" Detection

Instead of blindly waiting a fixed time, the server uses a layered strategy:

1. **Process exit** — if the process died, command is done
2. **Prompt detection** — auto-detects the session's prompt at startup (bash `$`, python `>>>`, psql `#`, etc.), watches for it to reappear
3. **Output settling** — no new output for 300ms = probably done
4. **Timeout** — always returns after `timeout_ms` with `is_complete: false`

## Security

Seven-layer defense-in-depth:

| Layer | What It Does | Default |
|-------|-------------|---------|
| MCP Tool Annotations | `readOnlyHint`/`destructiveHint` on each tool | Always on |
| Confirmation Flow | Dangerous patterns require `confirm_dangerous_command` | Always on |
| Input Pattern Detection | Detect rm -rf, DROP TABLE, curl\|bash, etc. | Always on |
| Command Blocklist/Allowlist | Block/allow specific commands | Configurable |
| OS-Level Sandbox | Kernel-level process sandboxing via `@anthropic-ai/sandbox-runtime` | Off (opt-in) |
| Secret Redaction | Redact AWS keys, tokens, private keys in output | Off (opt-in) |
| Resource Limits | Max sessions, output cap, idle timeout, audit logging | Always on |

### Recommended Permissions

Only auto-approve the read-only tools:

```json
{
  "permissions": {
    "allow": [
      "mcp__terminal__list_sessions",
      "mcp__terminal__read_output"
    ]
  }
}
```

This way `send_command`, `create_session`, and especially `confirm_dangerous_command` always require human approval.

## Configuration

All settings via environment variables. Pass them in your MCP config:

```json
{
  "mcpServers": {
    "terminal": {
      "command": "npx",
      "args": ["-y", "mcp-interactive-terminal"],
      "env": {
        "MCP_TERMINAL_ALLOWED_COMMANDS": "bash,python3,node,psql",
        "MCP_TERMINAL_REDACT_SECRETS": "true",
        "MCP_TERMINAL_IDLE_TIMEOUT": "300000"
      }
    }
  }
}
```

| Variable | Default | Description |
|----------|---------|-------------|
| `MCP_TERMINAL_MAX_SESSIONS` | `10` | Max concurrent sessions |
| `MCP_TERMINAL_MAX_OUTPUT` | `20000` | Max output chars per read |
| `MCP_TERMINAL_DEFAULT_TIMEOUT` | `5000` | Default wait timeout (ms) |
| `MCP_TERMINAL_BLOCKED_COMMANDS` | — | Comma-separated blocklist |
| `MCP_TERMINAL_ALLOWED_COMMANDS` | — | Comma-separated allowlist (if set, only these are allowed) |
| `MCP_TERMINAL_ALLOWED_PATHS` | — | Comma-separated paths sessions can access |
| `MCP_TERMINAL_REDACT_SECRETS` | `false` | Redact AWS keys, tokens, private keys in output |
| `MCP_TERMINAL_LOG_INPUTS` | `false` | Log all inputs to stderr (for debugging) |
| `MCP_TERMINAL_IDLE_TIMEOUT` | `1800000` | Auto-close idle sessions (ms, default 30min, 0 = disabled) |
| `MCP_TERMINAL_DANGER_DETECTION` | `true` | Enable dangerous command confirmation flow |
| `MCP_TERMINAL_AUDIT_LOG` | — | Path to JSON audit log file |
| `MCP_TERMINAL_SANDBOX` | `false` | Enable OS-level kernel sandboxing |
| `MCP_TERMINAL_SANDBOX_ALLOW_WRITE` | `/tmp` | Writable paths in sandbox mode |
| `MCP_TERMINAL_SANDBOX_ALLOW_NETWORK` | `*` | Allowed network domains in sandbox |

## Troubleshooting

### "Tools not showing up" / Server fails silently

MCP servers that fail to start often show no error in the client. Check:

```bash
# Test the server directly:
npx -y mcp-interactive-terminal

# You should see "[mcp-terminal] Starting MCP Interactive Terminal Server" on stderr.
# If you see an error, that's what's failing.
```

### Node.js version too old

The server requires Node.js >= 18. If you see errors about unsupported syntax or missing APIs:

```bash
node --version  # Must be >= 18

# If using nvm:
nvm install 18 && nvm use 18

# If using volta:
volta install node@18
```

**For nvm/volta/fnm users**: `npx` may use a different Node version than your shell. Use an absolute path:

```json
{
  "mcpServers": {
    "terminal": {
      "command": "/Users/you/.nvm/versions/node/v22.0.0/bin/npx",
      "args": ["-y", "mcp-interactive-terminal"]
    }
  }
}
```

Find your path with: `which npx`

### node-pty compilation errors

`node-pty` is a native module that requires build tools. If it fails to compile, the server automatically falls back to **pipe mode** — interactive sessions still work, just without terminal emulation.

If you want full PTY support:

```bash
# macOS:
xcode-select --install

# Ubuntu/Debian:
sudo apt-get install -y make python3 build-essential

# RHEL/Fedora:
sudo yum install -y make python3 gcc gcc-c++
```

### Session dies immediately

Some commands need to be run inside a shell rather than directly:

```
# Instead of:  create_session({ command: "rails console -e staging" })
# Do this:     create_session({ command: "bash" })
#              send_command({ input: "rails console -e staging" })
```

This is because `create_session` runs the command directly (like `exec`), not through a shell. Spawning `bash` first gives you a full shell environment.

### Output looks garbled

If output contains escape codes or looks wrong, you're likely in **pipe mode** (node-pty failed to load). Check the server logs for `"falling back to pipe mode"`. Install build tools (see above) to enable PTY mode.

### Timeout too short for long-running commands

Increase the timeout per-command:

```json
{ "session_id": "...", "input": "bundle install", "timeout_ms": 60000 }
```

Or globally via environment variable:

```json
{ "env": { "MCP_TERMINAL_DEFAULT_TIMEOUT": "30000" } }
```

## Comparison with Alternatives

| Feature | mcp-interactive-terminal | App-specific terminal servers | Generic shell MCP servers |
|---------|------------------------|-------------------------------|--------------------------|
| Cross-platform | Yes | Often single-app only | Varies |
| Clean output (xterm-headless) | Yes | No (screen scrape) | No (raw PTY dump) |
| Smart completion detection | 4-layer algorithm | No | Basic timeout |
| Security layers | 7 (confirmation flow, sandbox, redaction, etc.) | None | Basic |
| Dangerous command confirmation | Yes (separate tool) | No | No |
| MCP tool annotations | Yes | No | No |
| Background sessions | Yes | No (uses active tab) | Yes |
| Focused API | 7 tools | 2-3 tools | 15-20+ tools (scope creep) |
| Install | `npx -y` (zero-config) | Requires specific app | Varies |

## Development

```bash
git clone https://github.com/amol21p/mcp-interactive-terminal.git
cd mcp-interactive-terminal
npm install
npm run build
npm test
```

Test with MCP Inspector:

```bash
npx @modelcontextprotocol/inspector dist/index.js
```

## License

MIT