iterm-mcp
README.md
# iterm-mcp
  
An [MCP](https://modelcontextprotocol.io/) server that exposes iTerm2 tab control as native tools — designed for the **conductor pattern** where one Claude/Codex session drives sibling tabs.
## Tools
| Tool | What it does |
|---|---|
| `iterm_ping` | Sanity check that the server is reachable |
| `tabs_list` | Enumerate iTerm tabs with runtime detection (claude / codex / ssh / shell) and resume-UUID extraction |
| `tabs_peek` | Read tab contents, optionally tail-limited to last N lines |
| `tabs_search` | Search recent tab contents for substring or regex matches with line numbers |
| `tabs_dispatch` | Write text into a tab with 3-tier submit escalation (CR+LF → keystroke → file-drop) |
| `tabs_focus` | Bring a tab to the foreground |
| `tabs_send_keystroke` | Send a raw `return` / `tab` / `escape` / `backspace` / `space` keystroke via Accessibility |
## Usage
After registering the server with Claude, use the tab tools from a conductor session:
```json
{ "tool": "tabs_list", "arguments": {} }
```
Peek at a specific tab:
```json
{ "tool": "tabs_peek", "arguments": { "window": 1, "tab": 2, "tailLines": 40 } }
```
Search recent output:
```json
{ "tool": "tabs_search", "arguments": { "window": 1, "tab": 2, "pattern": "BLOCKED|PR URL", "regex": true, "tailLines": 200 } }
```
Dispatch work to a sibling tab:
```json
{
"tool": "tabs_dispatch",
"arguments": {
"window": 1,
"tab": 3,
"text": "Please run npm test and report failures.",
"submit": true,
"escalation": "auto"
}
}
```
For recovery-only dispatches, force `escalation: "fallback"` to create a pending dispatch markdown file without sending keystrokes.
## Three-tier dispatch escalation
`tabs_dispatch` is the workhorse. Submitting text reliably across local-shell, claude, codex, and SSH-into-remote tabs needs different mechanisms, so the tool escalates:
1. **Tier 1 — `crlf`**: AppleScript `write text` + CR + LF. Fast and quiet; works for short text into local claude tabs.
2. **Tier 2 — `keystroke`**: Focus the tab, then have System Events deliver `keystroke return`. Required for codex tabs and remote ssh sessions where Tier 1 unreliably submits.
3. **Tier 3 — `fallback`**: Write the intended dispatch to `~/.claude/plans/pending-dispatches/<ts>-w<W>t<T>.md`. Operator-recoverable.
`escalation: "auto"` (default) tries Tier 1, falls through to Tier 2 on failure, drops to Tier 3 if both fail. You can also force a tier explicitly: `crlf`, `keystroke`, or `fallback`.
## Refuse-self
`tabs_dispatch` reads `~/.claude/plans/inter-agent-sync/conductor-active.txt` and refuses dispatch when:
- target window/tab matches the conductor's own tab, or
- target tab's resume UUID is in `also_refuse_self_for_resume_uuids`, or
- target tab's resume UUID equals the conductor's `session_id`.
This prevents the conductor from ever dispatching into itself (which would cause a feedback loop).
## Screenshot
> _(placeholder — drop a screenshot of `tabs_list` output in a claude session here)_
## Install
```bash
git clone https://github.com/CondorCommodore/iterm-mcp.git
cd iterm-mcp
npm install
npm run build
claude mcp add iterm-mcp -s user -- node $(pwd)/dist/server.js
```
On first dispatch, macOS will prompt to grant **Accessibility** permission to your terminal / Claude Code app so that System Events can deliver keystrokes. Grant it in System Settings → Privacy & Security → Accessibility.
## Smoke test
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' | node dist/server.js
```
You should see a JSON-RPC `initialize` result on stdout and `[iterm-mcp] server ready` on stderr.
## Test
```bash
npm test # vitest run tests/unit
npm run test:watch # vitest watch
```
Unit tests mock `osascript` / `ps` invocation and verify both the AppleScript shape we send and our result parsing.
## Troubleshooting
### `error TS2589: Type instantiation is excessively deep and possibly infinite`
The MCP SDK's `McpServer.registerTool` uses extremely deep generic inference over Zod schemas. With `zod ^3.25` and `@modelcontextprotocol/sdk ^1.29` together, TypeScript hits its instantiation-depth ceiling and refuses to compile.
We work around it in `src/server.ts` with a thin wrapper that erases the SDK's generics:
```ts
const reg: (name: string, config: any, cb: any) => void = (n, c, h) => {
(server as any).registerTool(n, c, h);
};
```
Runtime behavior is identical — only the compile-time type check is short-circuited. When the SDK ships a less-recursive overload, the wrapper can be retired.
### `osascript: command not found`
iterm-mcp only runs on macOS. AppleScript and System Events are macOS-only.
### Dispatch silently no-ops on a codex or ssh tab
That's Tier 1 (`crlf`) being unreliable on those runtimes. Either let `escalation: "auto"` fall through to Tier 2, or pin `escalation: "keystroke"` directly.
### "Not authorized to send keystrokes"
System Events needs Accessibility permission for the *parent* process that spawns `osascript` (your terminal, or Claude Code itself). Grant it under System Settings → Privacy & Security → Accessibility.
### Refuse-self firing on the wrong tab
Check that `~/.claude/plans/inter-agent-sync/conductor-active.txt` reflects the conductor's actual window/tab. The file is `key=value` lines: `conductor_window`, `conductor_tab`, `session_id`, `also_refuse_self_for_resume_uuids` (comma-separated).
## License
MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues