Skip to main content
Glama

iterm-mcp

version node platform

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

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:

{ "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:

  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

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

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

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:

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