mcp-interactive-terminal
# mcp-interactive-terminal
[](https://github.com/hugo94z/mcp-interactive-terminal/actions/workflows/ci.yml)
[](LICENSE)
[](pyproject.toml)
A [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI agents a **real interactive terminal** on Windows: launch PowerShell, PowerShell 7 or CMD, type into it, read what happens, answer prompts, interrupt processes and resize the window — across multiple sessions that stay alive between tool calls.
Most "terminal" tools for LLMs run a command and return its output. That is not a terminal. Anything that asks a question, shows a menu, draws a progress bar or waits for a keypress is out of reach. This server runs the shell inside a **ConPTY** (Windows pseudo-console), so the agent talks to a genuine terminal session, exactly like a human.
```
agent ── MCP (stdio) ──> session manager ── ConPTY ──> powershell.exe / pwsh.exe / cmd.exe
│
├─ raw output ring buffer (cursor based reads)
└─ VT emulator (rendered screen, like a human sees it)
```
## Features
- **Real PTY sessions** — ConPTY under the hood, not pipes. Prompts, TUIs, colors and interactive programs work.
- **Live interaction** — send text and named keys (`enter`, `tab`, `ctrl-c`, `up`, `f1`…), read incrementally.
- **Rendered screen** — an internal VT100 emulator turns cursor movements and repaints into clean, readable text.
- **Blocking waits** — wait for a regex, for output to go quiet, for the process to exit, or for a timeout. No polling.
- **Multiple sessions** — independent shells running in parallel, each with its own buffer, screen and size.
- **One-shot shortcut** — `terminal_exec` for commands that only need output and an exit code.
- **UTF-8 everywhere** — accents and non-ASCII output are handled for PowerShell and CMD.
- **Token friendly** — incremental reads, `grep`/`tail` filters, head+tail truncation with an explicit omission marker.
## Requirements
- Windows 10 (1809+) or Windows 11 — ConPTY is required.
- Python 3.10+.
- An MCP client (Claude Desktop, Cursor, opencode, Cline, any SDK-based agent).
## Install
```powershell
pip install git+https://github.com/hugo94z/mcp-interactive-terminal
```
Or from a clone:
```powershell
git clone https://github.com/hugo94z/mcp-interactive-terminal
cd mcp-interactive-terminal
uv venv .venv
uv pip install --python .venv\Scripts\python.exe -e .
```
The `mcp-interactive-terminal` command starts the server on stdio.
## Configure your MCP client
### opencode (`~/.config/opencode/opencode.jsonc`)
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"interactive-terminal": {
"type": "local",
"command": ["mcp-interactive-terminal"]
}
}
}
}
```
### Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"interactive-terminal": {
"command": "mcp-interactive-terminal"
}
}
}
```
### Cursor (`.cursor/mcp.json`)
```json
{
"mcpServers": {
"interactive-terminal": {
"command": "mcp-interactive-terminal"
}
}
}
```
If the command is not on your `PATH`, use the absolute path to the entry point, for example:
```json
{
"command": "C:\\Users\\you\\project\\.venv\\Scripts\\mcp-interactive-terminal.exe"
}
```
## Tools
| Tool | Description |
|---|---|
| `terminal_start` | Start a session (`powershell`, `pwsh`, `cmd`, or any executable), with `cwd`, `env`, `cols`, `rows`, `name`, `replace`, `args`. Returns the id, pid and initial screen. |
| `terminal_send` | Send `text`, named `keys`, `enter=true`, `repeat`. |
| `terminal_read` | Incremental raw output since the last read (or an explicit `since` cursor). Optional `grep`, `tail`, `strip_ansi`. |
| `terminal_wait` | Block until `until` regex matches, output is quiet for `quiet_ms`, the session exits, or `timeout_ms`. Provide `until` and/or `quiet_ms`. |
| `terminal_screen` | Rendered VT screen with cursor position — the authoritative view. |
| `terminal_interrupt` | Ctrl+C, then terminate the child process tree if the command is still running. The shell survives. |
| `terminal_resize` | Resize the pseudo-terminal (columns × rows). |
| `terminal_list` | Active sessions, state, pid, cwd, size, idle time. |
| `terminal_stop` | Close a session (`force` to kill the tree). |
| `terminal_exec` | One-shot command: output, `exit_code`, `timed_out`, duration. |
### Keys accepted by `terminal_send`
`enter` `tab` `backtab` `esc` `space` `backspace` `up` `down` `left` `right` `home` `end`
`pageup` `pagedown` `insert` `delete` `f1`…`f12` `ctrl-a`…`ctrl-z` `ctrl-space` `ctrl-break`,
plus `text:<literal>` for arbitrary text.
## Example
```text
terminal_start(shell="powershell", cwd="C:/work/api")
-> session_id: "t1", screen: "PS C:\\work\\api> "
terminal_send(session_id="t1", text="python -m http.server 8080", enter=True)
terminal_wait(session_id="t1", until="Serving HTTP on", timeout_ms=15000)
-> reason: "matched"
terminal_exec(command="curl.exe -s -o NUL -w '%{http_code}' http://localhost:8080")
-> output: "200", exit_code: 0
terminal_interrupt(session_id="t1")
-> sent: true, running: true # back to the prompt
terminal_send(session_id="t1", text="exit", enter=True)
terminal_stop(session_id="t1")
```
## Why `terminal_screen` and `terminal_read` both exist
PowerShell uses PSReadLine, which redraws the current line with absolute cursor movements every time a character is typed. Raw output therefore contains fragments such as `\x1b[1;58He`, `\x1b[1;58Hec`, `\x1b[1;58Hech`… Stripping ANSI codes gives unreadable text.
`terminal_screen` feeds that stream into a VT100 emulator (via [pyte](https://github.com/selectel/pyte)) and returns what a human would actually see. `terminal_read` is still useful for exact incremental output (logs, byte streams, detecting events) and never returns the same bytes twice.
`terminal_wait` matches against the rendered screen **and** the raw output, and ignores the echo of the command you just typed by default (`ignore_echo`).
## Design notes
- One reader thread per session pushes output into a bounded ring buffer and the VT screen under a single lock.
- Reads are cursor-based (`buffer end offset`), so nothing is duplicated and nothing is lost silently.
- `terminal_wait` sleeps on a condition variable — no busy loop, negligible CPU.
- Sessions are closed on exit (`atexit`, EOF on stdio, or explicit `terminal_stop`); forced stops use `taskkill /T /F` on the process tree.
- PowerShell sessions are started with `-NoExit -Command` to force UTF-8 console encoding before the first prompt.
- CMD sessions run `chcp 65001` at startup.
## Notes on Windows behaviour
- **Ctrl+C in a pseudo-console** is not delivered as a console control event to native
child processes: the control character reaches the console input buffer, but the child
process group does not receive `CTRL_C_EVENT`. `terminal_interrupt` therefore sends
Ctrl+C and then terminates the remaining child process tree while keeping the shell
alive. Processes that trap Ctrl+C gracefully get the chance to run their handler first.
- **Resizing** the pseudo-console can make some shells drop the next write. See
[Known limitations](#known-limitations).
## Known limitations
- Windows only. ConPTY does not exist on Linux or macOS, so there is no POSIX backend yet.
- `terminal_resize` works, but some shells need a moment to redraw after a size change; give
the next command a short delay if you resize in the middle of a session.
- The rendered screen shows the visible window only. Use `terminal_read` for full scrollback
(bounded by the 500k-character ring buffer).
- Interactive programs that require raw mouse events or bracketed paste are not emulated.
## Security
This server executes arbitrary commands **with the privileges of the user running it**. There is no sandbox, no allowlist and no confirmation prompt. Only connect it to MCP clients you trust, and treat its tools with the same care as a shell running on your machine.
## Development
```powershell
uv venv .venv
uv pip install --python .venv\Scripts\python.exe -e ".[dev]"
.venv\Scripts\python.exe -m pytest -q
```
The test suite covers the ring buffer, VT rendering, key mapping, truncation, real PowerShell/CMD
sessions (interactive prompt, Ctrl+C, resize, exit codes) and a full MCP client handshake over stdio.
## Roadmap
Known limitations, root causes and the prioritised backlog live in [ROADMAP.md](ROADMAP.md).
It includes file-level pointers for each improvement, a guide for adding a tool or a shell, and
a debugging checklist.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 10 tools
Each tool has a clearly distinct role: lifecycle (start/stop/list/resize), input (send/interrupt), and output (read/screen/wait/exec). The potentially overlapping read vs. screen and wait vs. read pairs are explicitly differentiated in their descriptions (raw incremental vs. rendered VT view; blocking vs. immediate).
Every tool follows the same terminal_<verb> snake_case pattern with no exceptions. The verb choices are predictable and consistent across the set.
Ten tools is well-scoped for interactive terminal control, with each one covering a necessary operation (lifecycle, input, output, waiting, rendering). No redundant or filler tools.
The surface covers the full interactive lifecycle: start/stop, send/interrupt, read/screen/wait, resize, plus a one-shot exec for non-interactive commands. Minor gaps exist, such as no session rename or explicit scrollback/buffer clearing, but core workflows are fully covered.