Skip to main content

MCP server for spawning and controlling background processes with virtual terminals (Python port)

Project description

terminalcp

Python port of terminalcp — let AI agents control interactive command-line tools like a human would.

What it does

terminalcp enables AI agents to spawn and interact with any CLI tool in real-time — from debuggers like LLDB and GDB to other AI coding assistants like Claude Code, Gemini CLI, and Codex. Think of it as Playwright for the terminal: your agent can start processes, send keystrokes, read output, and maintain full interactive sessions with tools that normally require human input.

Key capabilities:

  • Debug code step-by-step using command-line debuggers (LLDB, GDB, pdb)
  • Collaborate with other AI tools by running them as subprocesses
  • Interact with REPLs (Python, Node, Ruby), database shells, and system monitors
  • Control any interactive CLI that expects human input
  • Run multiple processes simultaneously without blocking the agent
  • Users can attach to AI-spawned processes from their own terminal, similar to screen/tmux

Two output modes for different use cases:

  • Terminal mode (stdout): Returns the rendered screen with full scrollback — perfect for TUIs like vim, htop, or interactive debuggers where visual layout matters
  • Stream mode: Returns raw output with optional ANSI stripping and incremental reading — ideal for build processes, server logs, and high-volume output

Each process runs in a proper pseudo-TTY with full terminal emulation (via pyte), preserving colors, cursor movement, and special key sequences. Processes run in the background, so your agent stays responsive while managing long-running tools.

Requirements

  • Python 3.10 or newer
  • An MCP client (VS Code, Cursor, Windsurf, Claude Desktop, Claude Code, etc.)

Getting Started

Installation

pip install terminalcp

Or use directly with uvx (no installation needed):

uvx terminalcp --mcp

MCP Client Configuration

Standard config works in most tools:

{
  "mcpServers": {
    "terminalcp": {
      "command": "uvx",
      "args": ["terminalcp", "--mcp"]
    }
  }
}
Claude Code

Use the Claude Code CLI to add the terminalcp server:

claude mcp add -s user terminalcp uvx terminalcp --mcp
Claude Desktop

Follow the MCP install guide, use the standard config above.

Cursor

Go to Cursor Settings -> MCP -> Add new MCP Server. Name it "terminalcp", use command type with the command uvx terminalcp --mcp.

VS Code

Follow the MCP install guide, use the standard config above.

Installed via pip

If you installed globally with pip install terminalcp:

{
  "mcpServers": {
    "terminalcp": {
      "command": "terminalcp",
      "args": ["--mcp"]
    }
  }
}

MCP Usage Examples

The following examples show the JSON arguments passed to the single terminalcp tool exposed by the MCP server. The MCP server returns plain text responses to minimize token usage.

Starting and Managing Processes

// Start with auto-generated ID
{"action": "start", "command": "python3 -i"}
// Returns: "proc-3465b9b687af"

// Start with custom name (becomes the ID)
{"action": "start", "command": "npm run dev", "name": "dev-server"}
// Returns: "dev-server"

// Start in specific directory
{"action": "start", "command": "python3 script.py", "cwd": "/path/to/project", "name": "analyzer"}
// Returns: "analyzer"

Interacting with Running Sessions

// Send text with Enter key (\r)
{"action": "stdin", "id": "dev-server", "data": "npm test\r"}
// Returns: ""

// Send arrow keys (\u001b[D = Left arrow)
{"action": "stdin", "id": "editor", "data": "echo hello\u001b[D\u001b[D\u001b[D\u001b[Dhi \r"}

// Send control sequences
{"action": "stdin", "id": "process", "data": "\u0003"}  // Ctrl+C
{"action": "stdin", "id": "shell", "data": "\u0004"}     // Ctrl+D (EOF)

// Get terminal output (rendered screen)
{"action": "stdout", "id": "dev-server"}
// Returns: Full terminal screen with colors and formatting

// Get last N lines only
{"action": "stdout", "id": "dev-server", "lines": 50}

Monitoring Long-Running Processes

// Get all output as raw stream (ANSI codes stripped)
{"action": "stream", "id": "dev-server"}

// Get only new output since last check
{"action": "stream", "id": "dev-server", "since_last": true}

// Keep ANSI color codes
{"action": "stream", "id": "dev-server", "since_last": true, "strip_ansi": false}

Process Management

// List all sessions
{"action": "list"}
// Returns: "dev-server running /Users/you/project npm run dev\nanalyzer stopped /path python3 script.py"

// Stop specific process
{"action": "stop", "id": "dev-server"}
// Returns: "stopped dev-server"

// Stop ALL processes
{"action": "stop"}
// Returns: "stopped 3 processes"

// Kill the terminal server
{"action": "kill-server"}
// Returns: "shutting down"

Interactive AI Agents Example

// Start Claude with a name
{"action": "start", "command": "/path/to/claude --dangerously-skip-permissions", "name": "claude"}

// Send a prompt
{"action": "stdin", "id": "claude", "data": "Write a test for main.py\r"}

// Get the response
{"action": "stdout", "id": "claude"}

// Clean up
{"action": "stop", "id": "claude"}

Debugging with LLDB

{"action": "start", "command": "lldb ./myapp", "name": "debugger"}
{"action": "stdin", "id": "debugger", "data": "break main\r"}
{"action": "stdin", "id": "debugger", "data": "run\r"}
{"action": "stdout", "id": "debugger"}
{"action": "stdin", "id": "debugger", "data": "bt\r"}
{"action": "stdout", "id": "debugger"}

Build Process Monitoring

{"action": "start", "command": "npm run build", "name": "build"}
// Monitor progress
{"action": "stream", "id": "build", "since_last": true}
// ... wait ...
{"action": "stream", "id": "build", "since_last": true}  // Only new output

CLI Usage

terminalcp can also be used as a standalone CLI tool:

# List all active sessions
terminalcp ls

# Start a new session with a custom name
terminalcp start my-app "npm run dev"

# Attach to a session interactively (Ctrl+B to detach)
terminalcp attach my-app

# Get output from a session
terminalcp stdout my-app
terminalcp stdout my-app 50  # Last 50 lines

# Send input to a session (use :: prefix for special keys)
terminalcp stdin my-app "echo hello" ::Enter
terminalcp stdin my-app "echo test" ::Left ::Left ::Left "hi " ::Enter
terminalcp stdin my-app ::C-c  # Send Ctrl+C

# Monitor logs
terminalcp stream my-app --since-last
terminalcp stream my-app --with-ansi  # Keep ANSI codes

# Resize terminal
terminalcp resize my-app 120 40

# Get terminal size
terminalcp term-size my-app

# Stop sessions
terminalcp stop my-app
terminalcp stop  # Stop all

# Maintenance
terminalcp version
terminalcp kill-server

Zsh Completion

Auto-install (recommended):

terminalcp completion

Manual install:

mkdir -p ~/.zsh/completions
cp /path/to/terminalcp/terminalcp/completion/scripts/_terminalcp.zsh ~/.zsh/completions/_terminalcp

Enable completions in ~/.zshrc:

fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit
compdef _terminalcp terminalcp

Attaching to Sessions

You can attach to any session from your terminal to watch or interact with AI-spawned processes:

  1. AI spawns a process with a name:
{"action": "start", "command": "python3 -i", "name": "python-debug"}
  1. Attach from your terminal:
terminalcp attach python-debug
  1. Interact directly:
  • Type commands as normal
  • Terminal resizing is automatically synchronized
  • Press Ctrl+B to detach (session continues running)
  • Multiple users can attach to the same session simultaneously

Important Usage Notes

  • MCP Escape Sequences: Send special keys using escape sequences: \r (Enter), \u001b[A (Up), \u0003 (Ctrl+C)
  • CLI Special Keys: Use :: prefix: ::Enter, ::Left, ::C-c, ::M-x, ::F1-::F12
  • Aliases don't work: Commands run via bash -c, so use absolute paths or commands in PATH
  • Process persistence: Sessions persist across MCP server restarts — manually stop them when done
  • Named sessions: Use the name parameter when starting to create human-readable session IDs

Common Escape Sequences (for MCP)

// Basic keys
Enter: "\r"          Tab: "\t"         Escape: "\u001b"      Backspace: "\u007f"

// Control keys
Ctrl+C: "\u0003"     Ctrl+D: "\u0004"  Ctrl+Z: "\u001a"      Ctrl+L: "\u000c"

// Arrow keys
Up: "\u001b[A"       Down: "\u001b[B"   Right: "\u001b[C"     Left: "\u001b[D"

// Navigation
Home: "\u001b[H"     End: "\u001b[F"    PageUp: "\u001b[5~"   PageDown: "\u001b[6~"

// Function keys
F1: "\u001bOP"       F2: "\u001bOQ"     F3: "\u001bOR"        F4: "\u001bOS"

// Meta/Alt (ESC + character)
Alt+x: "\u001bx"     Alt+b: "\u001bb"   Alt+f: "\u001bf"

Programmatic Usage with TerminalManager

The TerminalManager class provides a programmatic API for driving TUI applications, useful for automation, testing, or building higher-level abstractions.

Basic Usage

import asyncio
from terminalcp import TerminalManager, build_input

async def main():
    manager = TerminalManager()

    # Start a process
    session_id = await manager.start('python3 -i', {'name': 'python-repl'})

    # Send input
    await manager.send_input(session_id, '2 + 2\r')

    # Wait for output to settle
    await asyncio.sleep(0.5)

    # Get rendered terminal screen
    output = await manager.get_output(session_id)
    print(output)

    # Get raw stream output
    stream = await manager.get_stream(session_id, since_last=True)
    print(stream)

    # Send special keys using build_input helper
    await manager.send_input(session_id, build_input('Up', 'Enter'))

    # Clean up
    await manager.stop(session_id)

asyncio.run(main())

Advanced TUI Interaction

import asyncio
from terminalcp import TerminalManager, build_input

async def drive_debugger():
    manager = TerminalManager()

    # Start LLDB
    debug_id = await manager.start('lldb ./myapp')
    await manager.send_input(debug_id, 'break main\r')
    await manager.send_input(debug_id, 'run\r')

    await asyncio.sleep(1)

    # Get debugger output
    output = await manager.get_output(debug_id)
    print(output)

    # Navigate with special keys
    await manager.send_input(debug_id, build_input('Up', 'Up', 'Enter'))

    # Monitor streaming output
    logs = await manager.get_stream(debug_id, since_last=True)
    print(logs)

    await manager.stop(debug_id)

asyncio.run(drive_debugger())

Testing TUI Applications

import asyncio
import pytest
from terminalcp import TerminalManager

async def wait_for_output(manager, session_id, pattern, timeout=5.0):
    """Wait until terminal output contains the expected pattern."""
    elapsed = 0.0
    while elapsed < timeout:
        output = await manager.get_output(session_id)
        if pattern in output:
            return output
        await asyncio.sleep(0.1)
        elapsed += 0.1
    raise TimeoutError(f"Timeout waiting for: {pattern}")

@pytest.mark.asyncio
async def test_python_repl():
    manager = TerminalManager()
    session_id = await manager.start('python3 -i')

    try:
        await wait_for_output(manager, session_id, '>>>')
        await manager.send_input(session_id, '2 + 2\r')
        output = await wait_for_output(manager, session_id, '4')
        assert '4' in output
    finally:
        await manager.stop(session_id)

@pytest.mark.asyncio
async def test_vim_navigation():
    manager = TerminalManager()
    session_id = await manager.start('vim')

    try:
        await manager.send_input(session_id, 'iHello\x1b')  # Insert + ESC
        await asyncio.sleep(0.3)
        output = await manager.get_output(session_id)
        assert 'Hello' in output
    finally:
        await manager.stop(session_id)

How it works

terminalcp uses a layered architecture for flexibility and persistence:

Architecture Layers

  1. TerminalManager — Core library that manages PTY sessions

    • Creates pseudo-TTY processes via Python's pty module
    • Maintains virtual terminals using pyte
    • Handles input/output, ANSI sequences, and terminal emulation
    • Provides the programmatic API for all terminal operations
  2. TerminalServer — Persistent background daemon

    • Auto-spawns when needed by CLI or MCP
    • Listens on Unix domain socket at ~/.terminalcp/server.sock
    • Manages all active terminal sessions across clients
    • Sessions persist even when clients disconnect
  3. TerminalClient — Communication layer

    • Used by both CLI and MCP to talk to TerminalServer
    • Sends commands over Unix socket
    • Handles connection management, retries, and version checking
  4. User Interfaces

    • MCP Server: Exposes terminalcp tool via FastMCP, uses TerminalClient to communicate with TerminalServer
    • CLI: Command-line interface, uses TerminalClient for communication
    • Both interfaces provide the same functionality

MCP Tool: terminalcp

The MCP server exposes a single tool called terminalcp that accepts JSON commands with different action types:

Action Parameters Returns
start command, cwd?, name? Session ID string
stop id? (omit to stop all) Confirmation message
stdout id, lines? Rendered terminal screen
stream id, since_last?, strip_ansi? Raw output text
stdin id, data Empty string
list Newline-separated session list
term-size id "rows cols scrollback_lines"
kill-server "shutting down"

Development

# Clone and install in editable mode
git clone <repo>
cd terminalcp
pip install -e .

# Run as MCP server
terminalcp --mcp

# Run as terminal server daemon
terminalcp --server

# Local development with uvx
uvx --from . terminalcp --mcp

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

terminalcp-1.0.3.tar.gz (27.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

terminalcp-1.0.3-py3-none-any.whl (29.0 kB view details)

Uploaded Python 3

File details

Details for the file terminalcp-1.0.3.tar.gz.

File metadata

  • Download URL: terminalcp-1.0.3.tar.gz
  • Upload date:
  • Size: 27.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for terminalcp-1.0.3.tar.gz
Algorithm Hash digest
SHA256 a4bf83a60742c10586342d850f1d9b6ac996adcdbdd63f07f894c78dc2cb897a
MD5 b52881dfc89090fc07f076c3176ecafe
BLAKE2b-256 c18fa6b4585f1f5429c28c288e1777aed57af73f1e670ebaf23d05ce1c2816d8

See more details on using hashes here.

Provenance

The following attestation bundles were made for terminalcp-1.0.3.tar.gz:

Publisher: publish.yml on code-wangshuyi/terminalcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file terminalcp-1.0.3-py3-none-any.whl.

File metadata

  • Download URL: terminalcp-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 29.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for terminalcp-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 9a6c46797d29ec1c514611c60d175ebe1e3146793ca56a90774054ac7f3a4d4c
MD5 c29115d6e3cc304f7df78dd759c6184f
BLAKE2b-256 163fa7c2c00349d6a53e6a443af6ad12ebeb651be308c66e226e7ecd3674c2b6

See more details on using hashes here.

Provenance

The following attestation bundles were made for terminalcp-1.0.3-py3-none-any.whl:

Publisher: publish.yml on code-wangshuyi/terminalcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page