An MCP server that exposes iTerm2 tab control as native tools — designed for the conductor pattern where one Claude/Codex session drives sibling tabs.
| 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 |
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.
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).
(placeholder — drop a screenshot of
tabs_listoutput in a claude session here)
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.
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.
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.
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.
iterm-mcp only runs on macOS. AppleScript and System Events are macOS-only.
That's Tier 1 (crlf) being unreliable on those runtimes. Either let escalation: "auto" fall through to Tier 2, or pin escalation: "keystroke" directly.
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.
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).
MIT