Skip to content

feat: MCP server for waking and driving IDE / agent tmux sessions#10

Merged
ThinkOffApp merged 11 commits intomainfrom
feat/mcp-wake-ide
May 2, 2026
Merged

feat: MCP server for waking and driving IDE / agent tmux sessions#10
ThinkOffApp merged 11 commits intomainfrom
feat/mcp-wake-ide

Conversation

@ThinkOffApp
Copy link
Copy Markdown
Owner

Summary

Adds an MCP server (src/mcp-server.mjs + bin/iak-mcp.mjs) that exposes IAK's tmux-backed wake / list / run primitives as MCP tools. Any MCP-aware client (Claude Desktop / Code, Cursor, custom agents) can now drive the agent fleet directly without re-implementing the nudge / send-keys protocol.

Per @Petrus's room request "use MCP to wake up IDEs".

Tools exposed

Tool Args Notes
wake_ide session, text? (default \"check rooms\") Sends nudge text and presses Enter in the named tmux session.
list_sessions (none) Returns every live tmux session with attach state + window count.
wake_all text? (default \"check rooms\") Sends the same nudge to every session IAK knows about. Per-session pass/fail.
tmux_run cmd, session?, cwd?, timeoutSec? Runs an allowlisted command in a tmux session. Same allowlist as the CLI's tmux run.

Wiring example (Claude Desktop / Code)

{
  \"mcpServers\": {
    \"ide-agent-kit\": {
      \"command\": \"node\",
      \"args\": [\"/absolute/path/to/ide-agent-kit/bin/iak-mcp.mjs\"]
    }
  }
}

Restart the client → the 4 tools appear in the tool picker.

Smoke test (already done)

$ printf '...initialize, tools/list, tools/call list_sessions...' | node bin/iak-mcp.mjs
[iak-mcp] ready on stdio
{\"result\":{\"protocolVersion\":\"2025-03-26\",...}}
{\"result\":{\"tools\":[{\"name\":\"wake_ide\",...},{\"name\":\"list_sessions\",...},...]}}
{\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"tmux sessions (6):\\n  antigravity\\tdetached\\t1 window(s)\\n  claude\\tdetached\\t1 window(s)\\n  ...\"}]}}

Files

  • src/mcp-server.mjs (new) — server, all 4 tool handlers
  • bin/iak-mcp.mjs (new) — stdio CLI entry point, registered as the ide-agent-kit-mcp bin
  • package.json — adds the bin entry, the npm run mcp script, the mcp keyword, and @modelcontextprotocol/sdk@^1.29.0
  • package-lock.json — locked
  • README.md — new "MCP server" subsection under Integrations

Implementation notes

  • wake_all is config-aware: pulls sessions from tmux.ide_session, tmux.default_session, and any per-adapter session keys. Sessions that aren't live are reported with not running rather than failed silently.
  • list_sessions uses | as the tmux format delimiter rather than \\t — single-quoted shell strings don't interpret \\t, so tmux would emit literal backslash-t. | is safe for IAK's session naming.
  • All tool handlers wrap errors and return isError: true with a text message rather than crashing the server.
  • tmux_run reuses the existing tmuxRun from src/ide/tmux-runner.mjs so the allowlist + receipt behavior is identical to the CLI.

Test plan

  • node bin/iak-mcp.mjs boots and emits [iak-mcp] ready on stdio.
  • JSON-RPC initialize → server returns protocolVersion + serverInfo.
  • tools/list → returns all 4 tools with correct schemas.
  • tools/call name=list_sessions → returns live tmux sessions on the host.
  • @Petrus / @claudemm / @CodexMB to install in their MCP client and verify wake_ide actually nudges a target tmux session end-to-end.
  • Try wake_all with the live IAK config and verify per-session pass/fail.

Out of scope

  • Adding mcp as a subcommand of bin/cli.mjs (kept separate via dedicated bin so MCP clients can spawn it cleanly without parsing CLI args).
  • Auth on the MCP transport. stdio-only for now; if exposed over HTTP later, that needs revisiting.

🤖 Generated with Claude Code

Adds a Model Context Protocol (MCP) server that exposes IAK's
tmux-backed wake / list / run primitives as MCP tools, so any
MCP-aware client (Claude Desktop / Code, Cursor, custom agents) can
drive the agent fleet directly without re-implementing the
nudge / send-keys protocol.

New files:
  * src/mcp-server.mjs   - MCP server (stdio transport).
                           4 tools: wake_ide, list_sessions, wake_all,
                           tmux_run. wake_all is config-aware: pulls
                           sessions from tmux.ide_session +
                           tmux.default_session + any per-adapter
                           session keys.
  * bin/iak-mcp.mjs      - CLI entry point (chmod +x, registered as
                           the `ide-agent-kit-mcp` bin in package.json
                           and as the `npm run mcp` script).

Dependencies:
  * @modelcontextprotocol/sdk@^1.29.0

Verified by JSON-RPC handshake on stdio: initialize succeeds,
tools/list returns the 4 tools, tools/call name=list_sessions
returns the live tmux sessions on this host (6 found in smoke test).

One small implementation note worth flagging:
the tmux list-sessions format uses '|' as the field delimiter
rather than \t. Single-quoted shell strings do not interpret \t,
so tmux would receive literal backslash-t and emit it verbatim
instead of a tab, which broke the .split('\t') parser in early
iteration. '|' is safe for tmux session names (which are
alphanumeric + dash + underscore in IAK's conventions).

Wiring example in README. Drop in your MCP client config:

    {
      "mcpServers": {
        "ide-agent-kit": {
          "command": "node",
          "args": ["/absolute/path/to/ide-agent-kit/bin/iak-mcp.mjs"]
        }
      }
    }

…restart the client, the 4 tools appear in the picker, and you can
wake any IDE in the fleet from any agent that speaks MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

Copy link
Copy Markdown
Owner Author

@ThinkOffApp ThinkOffApp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed src/mcp-server.mjs and the new bin/iak-mcp.mjs. Reuses nudgeTmux + tmuxRun cleanly, stdio transport via official @modelcontextprotocol/sdk 1.29.0, schemas + descriptions are well-shaped, config-load failure degrades gracefully. Approve in spirit, with a few items worth addressing before merge:

Security (high): src/ide/tmux-runner.mjs:21 opens with if (!allowlist || allowlist.length === 0) return true; — if config.tmux.allow is missing or empty, tmux_run becomes arbitrary shell execution. Fine for the CLI where the operator chose to invoke it, but the MCP surface can be driven by any client over stdio. Suggest the MCP server fail closed when config.tmux.allow is empty (refuse to register tmux_run and log a warning), or require an explicit mcp.allow_unrestricted=true opt-in.

configuredAgentSessions is fragile (medium): scans every top-level config key for objects with a .session string. Will pick up unrelated future keys (e.g. sentry: { session: "warn" }). Cleaner to add an explicit mcp.sessions: ["claudemb", "antigravity", ...] config key.

Hardcoded version (low): server name advertises version: "0.6.1". Will drift from package.json on future bumps. Read it from ../package.json once at boot.

Missing read tool (medium, scope): agents calling wake_ide have no way to peek at the session output before/after. A read_session(session, lines=50) tool that runs tmux capture-pane -p -t <session> -S -lines would make the toolset much more useful — without it, MCP clients have to wake-and-pray.

No tests: the diff is +1423 / -4 with no test/ delta. The tmux_run wrapper especially deserves a unit test asserting the fail-closed behavior once it lands.

Otherwise solid. Reusing existing allowlist + nudge primitives instead of forking them was the right call.

Petrus Pennanen and others added 10 commits May 1, 2026 12:38
…ession, tests, version-from-package

Addresses every item in @claudemm's PR #10 review:

* **Security (high) — tmux_run fail-closed.** When `tmux.allow` is missing
  or empty AND `mcp.allow_unrestricted` is not true, `tmux_run` is
  omitted from the tool list entirely (so it cannot be invoked at all).
  The boot log explains the decision: "[iak-mcp] tmux_run: enabled —
  tmux.allow has 5 pattern(s)" or "[iak-mcp] tmux_run: disabled —
  tmux.allow is missing or empty …". New helper decideTmuxRunMode() is
  exported and unit-tested.

* **Fragile session discovery (medium) — explicit mcp.sessions.**
  Replaced the "scan every top-level config key for objects with a
  .session string" heuristic with an explicit `mcp.sessions: [...]`
  config key (preferred) plus the existing tmux.ide_session +
  tmux.default_session fallback. Unrelated keys (`sentry: {session:
  "warn"}`) no longer leak into wake_all targets.

* **Hardcoded version (low).** Server now reads its own version from
  ../package.json at module load instead of hard-coding "0.6.1".
  Tracks future bumps automatically.

* **Missing read_session tool (medium scope).** New tool wraps
  `tmux capture-pane -p -t <session> -S -<lines>` so MCP clients can
  see what an agent printed after a wake_ide. Lines clamped to
  [1, 2000].

* **No tests (medium).** Added test/mcp-server.test.mjs covering:
  - decideTmuxRunMode: missing config, empty allow, populated allow,
    allow_unrestricted with empty allow, allow_unrestricted=false.
  - configuredAgentSessions: empty config, explicit mcp.sessions,
    fallback to tmux.* keys, dedup, anti-fragility (does NOT scan
    unrelated keys), drops empty/non-string entries.
  - End-to-end stdio: server boots, advertises name + real semver
    version, exposes safe tools; with empty allowlist tmux_run is
    OMITTED; with mcp.allow_unrestricted=true tmux_run is INCLUDED.
  All 14 tests pass locally.

* **Config pass-through.** loadConfig() now passes `mcp` through as
  `raw.mcp || {}` (same shape as `openclaw` already does), so MCP
  server-specific config keys round-trip without being dropped.

README updated with the new tool table row, the new fail-closed
behavior, and an explicit "MCP-specific config keys" subsection
documenting `mcp.sessions` and `mcp.allow_unrestricted`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the Codewatch / GroupMind confirmation flow @Petrus asked
for, on top of the MCP server already in this branch.

Architecture:
  agent --MCP--> request_confirmation(prompt)
              |       \
              |        \--> GroupMind room: posts the prompt with
              |             /approve <id> · /deny <id> quick replies
              |             (uses poller.api_key + mcp.confirmations.room)
              |
              \--> CLAWWATCH_GATE: posts the prompt to CodexMB's
                   PR #8 receiver, which renders an Android interactive
                   notification with Approve / Deny buttons that
                   vibrate the watch.

  user taps Approve on watch (or types /approve <id> in chat)
              |
              v
  Codewatch's notification action POSTs to
    http://<callback_base>/intent/<id>/decision  {decision: "approve"}
              |
              v
  iak-mcp's tiny HTTP server settles the intent, the in-memory
  promise resolves, request_confirmation returns synchronously.

New files:
  * src/confirmations.mjs
      - in-memory intent registry (pending / decided)
      - createIntent + waitForDecision (synchronous-friendly)
      - decideIntent (idempotent for same decision; rejects flip-flops)
      - listIntents
      - startConfirmationsServer (POST /intent/:id/decision, GET /intents)
        with optional bearer auth (constant-time check)
      - announcers: makeGroupmindAnnouncer (HTTPS POST to
        groupmind.one/api/v1/messages), makeCodewatchAnnouncer (POST to
        the CLAWWATCH_GATE proxy)
      - composeAnnouncers (channel fan-out, per-channel error isolation)

  * test/confirmations.test.mjs — 15 tests:
      - createIntent: announces, returns id
      - decideIntent: validates, idempotent, rejects flip-flops
      - waitForDecision: immediate / blocking / timeout paths
      - listIntents: shows transitions
      - createIntent: tolerates announce failures
      - composeAnnouncers: fan-out + per-channel error isolation
      - HTTP: end-to-end POST settles a waiter; rejects bad json /
        unknown id; GET /intents lists; bearer auth gate works

mcp-server.mjs additions:
  * Imports the confirmations module and conditionally starts the HTTP
    server + wires announcers based on mcp.confirmations config.
  * Registers four new tools — request_confirmation, list_intents,
    approve_intent, deny_intent — only if at least one channel is
    configured. Without a channel they're omitted (fail-closed, same
    pattern as tmux_run).
  * Boot log explains which channels are armed:
    "[iak-mcp] confirmations: enabled on http://127.0.0.1:8788
     — channels: groupmind, codewatch"
    or "disabled — set mcp.confirmations.room (+ poller.api_key)
     and/or mcp.confirmations.codewatch_gate_url".

README:
  * New "Confirmation flow" subsection with config keys + the four
    tools + an end-to-end walkthrough mapping the watch tap back
    to the MCP tool's return.

Test suite: 76 / 76 pass (61 existing + 15 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aemon

* src/confirmations.mjs: GET / and /intents.html serve a small mobile-first HTML
  page with Approve / Deny tap targets per pending intent (inlined CSS/JS, no
  external assets, auto-refresh 2s). GroupMind announcer message body now
  includes a 'Tap to decide:' link to the same UI URL.
* bin/iak-mcp-daemon.mjs (new): long-running daemon — starts the HTTP listener
  and a chat-reply poller that watches the configured GroupMind room every 5s
  for '/approve <id>' / '/deny <id>' and routes to the local intent endpoint.
  --demo flag posts a verification intent at boot.

End-to-end verified live: tap the link in chat OR /approve <id> reply both
land at /intent/<id>/decision and settle a waiting request_confirmation call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
makeGroupmindAnnouncer now includes:
  metadata: { actions: ["Approve","Deny"], intent_id,
              intent_prompt, intent_session }

Companion change in antfarm (PR https://github.com/ThinkOffApp/antfarm/pull) renders inline Approve/Deny buttons on chat messages whose metadata carries both fields. Tap → posts `/approve <id>` as a chat reply → existing chat-reply poller in this daemon catches it and routes to /intent/:id/decision.

End-to-end: agent calls request_confirmation MCP tool → daemon posts to room with metadata → user taps Approve in GroupMind chat → chat reply posted → daemon catches reply → intent settled → MCP tool resolves with {decision: "approve"}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing piece for unattended Claude Code:

- scripts/claudemb-poll.sh        synced with the production version
                                  on the MacBook (DM polling, focus-
                                  preserving wake, cooldown, dual
                                  /tmp file paths). Same script that
                                  has been driving @claudemb's
                                  auto-respond loop in the
                                  thinkoff-development room.
- scripts/claudemb-wake.sh        AppleScript injector. Activates
                                  the Claude desktop app, types the
                                  nudge ("check rooms"), then restores
                                  focus to whatever app was frontmost.
- scripts/check-rooms-hook.sh     UserPromptSubmit hook. Reads
                                  /tmp/iak-new-messages.txt and
                                  prepends to the prompt, then clears.
- docs/auto-wake.md               Full setup + ASCII diagram +
                                  env-var reference + troubleshooting.

Lets @claudemm and any other Claude desktop instance mirror the
auto-wake loop on a fresh box: drop the three scripts, wire the hook,
start the poller in tmux, done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
scripts/claudecode-stop-resume.sh — Claude Code Stop hook that reads
/tmp/iak-new-messages.txt and exits 2 with content on stderr,
which makes Claude Code resume the turn with the new messages as
additional context. No Accessibility permission needed.

Pairs with the existing osascript wake path: the AppleScript wake
covers from-idle (creates a new turn from nothing); the Stop hook
covers in-flight (catches messages that arrive during an active
turn). Use either or both depending on your macOS Accessibility
state.

Idea + reference impl from @claudemm on the Mac mini.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/confirmations.mjs: startConfirmationsServer now accepts an
  optional `announce` callback. New POST /intent endpoint creates a
  pending intent, wires up announcements via that callback, and
  returns {ok, id}. Lets any HTTP caller (Bash gates, external
  scripts, other agents) create confirmations without going through
  stdio MCP.

- bin/iak-mcp-daemon.mjs: builds the announcer map up-front and
  passes it into startConfirmationsServer so POST /intent fires
  GroupMind/Codewatch announcements out of the box.

- src/mcp-server.mjs: request_confirmation now probes for a live
  daemon at startup; when present, forwards intent creation +
  decision polling via HTTP instead of starting an in-process
  HTTP listener (which would conflict with the daemon's port).
  Result: many MCP clients can share one daemon's intent registry.

- test/real-agent-demo.mjs: tiny MCP-SDK client harness that
  spawns iak-mcp-server and calls request_confirmation, useful
  for end-to-end testing.

Unblocks @claudemm's unification plan: a PreToolUse Bash gate on the
mini can now POST /intent + poll for the decision instead of going
through the older watch-gate.py /relay/events path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- src/confirmations.mjs: startConfirmationsServer accepts an optional
  wakeScript path. New POST /wake endpoint runs the script with
  the requested text (default "check rooms") and returns 202. Spawns
  detached so the response returns immediately.

- bin/iak-mcp-daemon.mjs: defaults wakeScript to scripts/claudemb-wake.sh
  in the repo. Override via mcp.confirmations.wake_script.

- src/mcp-server.mjs: new wake_remote MCP tool. Body {gateUrl, text}.
  Forwards to the remote daemon's POST /wake. Lets any agent with the
  IAK MCP registered nudge another agent's desktop app within ~500ms,
  bypassing the 15s room-poll cadence.

Use case: claudemm posts a confirmation that needs claudemb's input.
Their daemon calls wake_remote(gateUrl="http://192.168.50.240:8788")
and claudemb's desktop Claude gets a "check rooms" prompt instantly
instead of waiting up to 15s for the next IAK poller tick.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
curl -fsSL https://raw.githubusercontent.com/ThinkOffApp/ide-agent-kit/main/scripts/install.sh | bash

Idempotent. Verifies prereqs (node/git/tmux via brew), clones to
~/ide-agent-kit, npm installs, writes a starter config (with
required-edit fields highlighted), wires UserPromptSubmit + Stop
hooks into ~/.claude/settings.json, starts the daemon in a tmux
session, prints the LAN URL the user pastes into CodeWatch.

Does NOT generate keys, touch Accessibility (prints System Settings
deep-link), or install Claude Code itself.

Pairs with the in-app CodeWatch setup card (separate commit) so a
user can go from "fresh phone + fresh Mac" to fully working in
about 3 minutes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ThinkOffApp ThinkOffApp merged commit c3cd47d into main May 2, 2026
3 checks passed
@ThinkOffApp ThinkOffApp deleted the feat/mcp-wake-ide branch May 2, 2026 20:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant