iterm-mcp
iterm-mcp
An MCP 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 |
| Sanity check that the server is reachable |
| Enumerate iTerm tabs with runtime detection (claude / codex / ssh / shell) and resume-UUID extraction |
| Read tab contents, optionally tail-limited to last N lines |
| Search recent tab contents for substring or regex matches with line numbers |
| Write text into a tab with 3-tier submit escalation (CR+LF → keystroke → file-drop) |
| Bring a tab to the foreground |
| Send a raw |
Usage
After registering the server with Claude, use the tab tools from a conductor session:
{ "tool": "tabs_list", "arguments": {} }Peek at a specific tab:
{ "tool": "tabs_peek", "arguments": { "window": 1, "tab": 2, "tailLines": 40 } }Search recent output:
{ "tool": "tabs_search", "arguments": { "window": 1, "tab": 2, "pattern": "BLOCKED|PR URL", "regex": true, "tailLines": 200 } }Dispatch work to a sibling tab:
{
"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:
Tier 1 —
crlf: AppleScriptwrite text+ CR + LF. Fast and quiet; works for short text into local claude tabs.Tier 2 —
keystroke: Focus the tab, then have System Events deliverkeystroke return. Required for codex tabs and remote ssh sessions where Tier 1 unreliably submits.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, ortarget 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_listoutput in a claude session here)
Install
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.jsOn 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
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' | node dist/server.jsYou should see a JSON-RPC initialize result on stdout and [iterm-mcp] server ready on stderr.
Test
npm test # vitest run tests/unit
npm run test:watch # vitest watchUnit 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:
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