Drive any interactive CLI tool (newt init, npm init, gt submit, ssh, docker login, etc.) from a Cursor agent using a PTY-based MCP server.
Cursor agents can't handle interactive CLI prompts natively. When a tool like newt init asks "What type of component?", the agent has no way to respond. This setup solves that.
mkdir -p ~/.local/terminalcp && cd ~/.local/terminalcp
npm init -y
npm install @mariozechner/terminalcpIf you hit native module issues with node-pty on macOS:
cd ~/.local/terminalcp/node_modules/node-pty
npm run buildAdd this to ~/.cursor/mcp.json (create the file if it doesn't exist):
{
"mcpServers": {
"terminal": {
"command": "node",
"args": ["<HOME>/.local/terminalcp/node_modules/@mariozechner/terminalcp/dist/index.js", "--mcp"],
"env": {
"PATH": "<your PATH including node, homebrew, nflx bins>"
},
"alwaysAllow": ["terminalcp"]
}
}
}Replace <HOME> with your actual home directory path (e.g. /Users/yourname) and set PATH to include the directories your CLI tools live in.
Tip: Run echo $PATH in your terminal and use that value, or at minimum include:
/opt/homebrew/bin:/usr/local/bin:/opt/nflx/bin:~/.local/bin:/usr/bin:/bin
Save this to ~/.cursor/rules/interactive-cli.mdc:
---
description: Use the terminal MCP server (terminalcp) to drive interactive CLI tools that prompt for input (newt init, gt submit, npm init, ssh, docker, etc.)
globs:
alwaysApply: true
---
# Interactive CLI Usage
Use the `terminal` MCP server (terminalcp) when a command requires interaction (prompts, REPLs, TUIs, auth flows).
## When to use
- CLI prompts for input (y/n, passwords, selections)
- Long-running commands with staged output
- REPLs or shells (node, python, db clients)
- Tools like git, ssh, docker, kubectl that may prompt
## Core workflow (strict loop)
1. **Start**: `action: "start"`, `command: "<cli>"`, `cwd: "<dir>"` -- returns an `id`
2. **Wait for startup**: pause 3-5 seconds for the process to initialize
3. **Read output**: `action: "stdout"` to see what the CLI printed
4. **LOOP**:
- `action: "stream"`, `since_last: true`, `strip_ansi: false` -- get raw output
- **Check for DSR queries** (see below)
- Analyze:
- Is there a prompt waiting for input?
- Is the command still running?
- Did it complete or error?
- If **DSR query detected** (`ESC[6n` in raw output):
- Send response: `action: "stdin"`, `data: "\u001b[40;80R"`
- Send one response per `[6n` found
- Then re-read output before sending any answer
- If **prompt detected**:
- Decide response
- If destructive or unclear: **ASK USER** before proceeding
- `action: "stdin"`, `data: "<answer>\r"`
- If **still running** without prompt:
- Wait 2-3 seconds, then read again
- If **complete** (process exited):
- Break loop
5. **Stop**: `action: "stop"`, `id: "<id>"`
## DSR (Device Status Report) workaround -- CRITICAL
Many Node.js CLIs (Inquirer.js, prompts, ora) send `ESC[6n` to query terminal dimensions. PTY-based MCP servers do NOT respond to these automatically. If you don't handle them, the CLI hangs forever.
**How to detect**: Use `action: "stream"` with `strip_ansi: false` and look for `[6n` in the raw output.
**How to fix**: Send `\u001b[40;80R` (ESC [ rows ; cols R) back via stdin for each `[6n` found. This tells the CLI the terminal is 40 rows by 80 columns.
**Pattern**: After every `stdin` send to an Inquirer-based CLI:
1. Wait 1-2 seconds
2. Read raw stream (`strip_ansi: false`, `since_last: true`)
3. Count occurrences of `[6n`
4. Send that many `\u001b[40;80R` responses
5. Wait 1-2 seconds
6. Read `stdout` to see the rendered prompt
**Which CLIs need this**: newt init, npm init (newer versions), yeoman generators, create-react-app, and anything using @inquirer/prompts or the ora spinner library.
## API reference
| Action | Purpose | Key args |
|--------|---------|----------|
| `start` | Spawn process | `command`, `cwd`, `name` |
| `stdin` | Send keystrokes | `id`, `data` (use `\r` for Enter, `\u0003` for Ctrl+C) |
| `stdout` | Read rendered screen | `id`, `lines` (optional) |
| `stream` | Read raw output | `id`, `since_last`, `strip_ansi` |
| `stop` | Kill process | `id` (omit to stop all) |
| `list` | Show active sessions | none |
## Prompt detection
- Look for: `y/n`, `yes/no`, `(Y/n)`, `(y/N)`, `Enter value:`, `?`, `>`, `[default]`, menus, numbered selections
- Do NOT assume defaults unless safe and obvious
## Escape sequences for stdin
- Enter: `\r`
- Ctrl+C: `\u0003`
- Ctrl+D: `\u0004`
- Arrow keys: Up=`\u001b[A` Down=`\u001b[B` Right=`\u001b[C` Left=`\u001b[D`
- Tab: `\t`
- Escape: `\u001b`
## Safety rules
- ALWAYS confirm with user before: creating/modifying infrastructure, deleting data, running migrations, auth/credential entry, accepting SSH fingerprints
## Output handling
- Use `stream` with `since_last: true` and `strip_ansi: false` for prompt detection (see raw escape codes)
- Use `stdout` for clean rendered view to show the user
- Prefer frequent reads over one long blocking call
- Summarize progress between steps
## Failure handling
- If command errors: analyze, retry if safe, otherwise ask user
- If stuck: `stdin` with `\u0003` (Ctrl+C) to interrupt
- If session dies: `stop`, start a new one, retry
- If prompt loops (same question repeated): stop, report to user
## Cleanup
- ALWAYS `stop` when done, even on errors
The setup gives Cursor agents a PTY (pseudo-terminal) they can read from and write to, just like a human at a terminal. The rule teaches the agent a strict read-respond loop so it doesn't get confused by interactive prompts.
The DSR workaround is the key insight: many Node.js CLI tools (Inquirer.js, ora) query terminal dimensions via ESC[6n]. Without a response, they hang forever. The rule teaches the agent to detect these queries and respond with fake dimensions.
newt init/newt add(Jetpack, NodeQuark, etc.)gt submit/gt create(Graphite stacked PRs)npm initsshsessionsdocker login- Any y/n or menu-based CLI
- Full-screen TUIs (vim, htop, lazygit) are not reliably automatable
- Some CLIs with aggressive screen redraws can confuse the agent
- Password prompts with no echo require the agent to send input blind