Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,95 @@ Latest local verification from the Petrus machine, dogfooded against `v0.6.1`:

## Integrations

### MCP server (`src/mcp-server.mjs`)

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 tmux send-keys.

Tools exposed (stdio transport):

| 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 on the host 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). Configure via `mcp.sessions: ["...", ...]`. Falls back to `tmux.ide_session` + `tmux.default_session`. |
| `read_session` | `session`, `lines?` (default 50) | `tmux capture-pane` of the named session — see what the agent printed in response to a `wake_ide`. |
| `tmux_run` | `cmd`, `session?`, `cwd?`, `timeoutSec?` | Runs an allowlisted command in a tmux session. **Only registered when `tmux.allow` is non-empty or `mcp.allow_unrestricted: true` is set.** Otherwise omitted entirely from the tool list (fail-closed). Same allowlist as the CLI's `tmux run` subcommand. |

### MCP-specific config keys

Added to `ide-agent-kit.json` (or your own config path passed via `--config`):

```jsonc
{
"mcp": {
// Explicit list of sessions wake_all should target.
// If omitted, falls back to [tmux.ide_session, tmux.default_session].
"sessions": ["claudemb", "antigravity", "codex"],

// Set true to expose tmux_run with NO allowlist filter — any command runs.
// Default: false. Use only on a trusted host with a trusted MCP client.
"allow_unrestricted": false,

// User-confirmation flow (request_confirmation, list_intents,
// approve_intent, deny_intent tools). Tools are only registered if at
// least one channel below is configured.
"confirmations": {
"port": 8788, // HTTP port for /intent/:id/decision
"host": "127.0.0.1", // bind host (keep local unless tunneled)
"auth_token": "", // optional bearer for the HTTP endpoint
"callback_base": "http://...", // URL the watch / chat reach back on; defaults to http://host:port
"room": "thinkoff-development", // GroupMind room to post the prompt in (uses poller.api_key)
"codewatch_gate_url": "http://family@localhost:18791/intent",
"codewatch_gate_token": "" // bearer for CLAWWATCH_GATE
}
}
}
```

### Confirmation flow (request_confirmation tool)

When `mcp.confirmations` is configured, four extra tools appear:

| Tool | Args | Notes |
|-----------------------|---------------------------------------------------|-------|
| `request_confirmation`| `prompt`, `session?`, `channels?`, `timeoutSec?` | Posts an Approve / Deny prompt to GroupMind and/or Codewatch and BLOCKS until user decides or timeout. Returns `{decision: "approve"\|"deny"}` or `{status: "timeout", id}`. |
| `list_intents` | (none) | All intents — pending and recently decided. |
| `approve_intent` | `id` | Manually settle a pending intent (e.g. MCP override). |
| `deny_intent` | `id` | |

End-to-end:
1. MCP-aware agent calls `request_confirmation({prompt: "Drop production DB?"})`.
2. The IAK MCP server posts to GroupMind room (`/approve <id>` / `/deny <id>` quick replies) and to the CLAWWATCH_GATE (Android interactive notification with Approve / Deny buttons that vibrate the watch).
3. User taps Approve / Deny on the watch — Codewatch's notification action POSTs to `http://<callback_base>/intent/<id>/decision` with `{decision: "approve"}`.
4. The MCP tool's blocking `request_confirmation` call resolves with the decision.
5. The agent proceeds (or doesn't) based on the decision.

Run standalone:

```bash
node bin/iak-mcp.mjs # default config
node bin/iak-mcp.mjs --config /path/to/config.json
npm run mcp # via package.json script
```

Wire into Claude Desktop / Code:

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

After install: restart the MCP client. The four tools above appear in the tool
picker and can be called directly.

### GitHub Webhooks (`src/webhook-server.mjs`)

Receives GitHub webhook events, verifies HMAC signatures, normalizes them to a stable JSON schema, and appends to a local JSONL queue. Optionally nudges a tmux session when events arrive.
Expand Down
152 changes: 152 additions & 0 deletions bin/iak-mcp-daemon.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
#!/usr/bin/env node
// SPDX-License-Identifier: AGPL-3.0-only
//
// Long-running daemon flavor of the MCP confirmation flow. Keeps the HTTP
// listener up + watches the configured GroupMind room for `/approve <id>` and
// `/deny <id>` quick-reply messages and routes them to the local
// /intent/:id/decision endpoint.
//
// Used together with the MCP server: an MCP client triggers
// request_confirmation which posts to the room with an intent id; the user
// replies "/approve abc12345" from the watch / chat; this daemon catches the
// reply and POSTs it to the same HTTP listener that the MCP tool is waiting
// on; the MCP tool resolves with {decision: "approve"}.
//
// Run: node bin/iak-mcp-daemon.mjs [--config path/to/config.json]

import { loadConfig } from '../src/config.mjs';
import {
startConfirmationsServer,
decideIntent,
getIntent,
createIntent,
makeGroupmindAnnouncer,
makeCodewatchAnnouncer,
composeAnnouncers,
} from '../src/confirmations.mjs';

const argv = process.argv.slice(2);
let configPath;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--config' && argv[i + 1]) { configPath = argv[i + 1]; i++; }
}
const config = await loadConfig(configPath);
const cc = config?.mcp?.confirmations || {};
if (!cc.room && !cc.codewatch_gate_url) {
process.stderr.write('[iak-mcp-daemon] no mcp.confirmations channels configured — exiting\n');
process.exit(2);
}

// Build the announcer map up-front so the HTTP server can use it for
// externally-created intents (POST /intent).
const apiKey = config?.poller?.api_key;
const room = cc.room;

const serverAnnouncerMap = {};
if (cc.room && apiKey) {
serverAnnouncerMap.groupmind = makeGroupmindAnnouncer({
apiKey, room: cc.room, callbackBase: cc.callback_base || `http://127.0.0.1:${cc.port || 8788}`,
});
}
if (cc.codewatch_gate_url) {
serverAnnouncerMap.codewatch = makeCodewatchAnnouncer({
gateUrl: cc.codewatch_gate_url, gateToken: cc.codewatch_gate_token,
});
}
const serverAnnounce = composeAnnouncers(serverAnnouncerMap);

// Start the HTTP listener first so any decisions can settle.
// Wake script: defaults to scripts/claudemb-wake.sh in this repo.
// Override via mcp.confirmations.wake_script in config.
const wakeScript = cc.wake_script ||
new URL('../scripts/claudemb-wake.sh', import.meta.url).pathname;

startConfirmationsServer({
port: cc.port || 8788,
host: cc.host || '127.0.0.1',
authToken: cc.auth_token || '',
receiptsPath: config?.receipts?.path,
announce: serverAnnounce,
wakeScript,
});
console.log(`[iak-mcp-daemon] HTTP listener on http://${cc.host || '127.0.0.1'}:${cc.port || 8788} (POST /intent enabled: ${Object.keys(serverAnnouncerMap).join(',') || 'no announcers'})`);

// Chat-reply poller: watch the configured GroupMind room for "/approve <id>"
// and "/deny <id>" messages and route them to the local intent endpoint.
if (!apiKey) {
console.warn('[iak-mcp-daemon] poller.api_key missing — chat-reply poller disabled');
} else if (!room) {
console.warn('[iak-mcp-daemon] mcp.confirmations.room missing — chat-reply poller disabled');
} else {
startChatReplyPoller({ apiKey, room, intervalMs: 5000 });
console.log(`[iak-mcp-daemon] chat-reply poller watching room "${room}" every 5s`);
}

// Codewatch path: just announce when a message arrives at /push (not implemented
// here, would route through CodexMB's watch-gate.py — see PR #8).

// Manual ping: prove the round-trip by creating one demo intent on first run if
// --demo is passed.
if (argv.includes('--demo')) {
const announcerMap = {};
if (cc.room && apiKey) {
announcerMap.groupmind = makeGroupmindAnnouncer({
apiKey, room: cc.room, callbackBase: cc.callback_base || `http://127.0.0.1:${cc.port || 8788}`,
});
}
if (cc.codewatch_gate_url) {
announcerMap.codewatch = makeCodewatchAnnouncer({
gateUrl: cc.codewatch_gate_url, gateToken: cc.codewatch_gate_token,
});
}
const announce = composeAnnouncers(announcerMap);
const id = await createIntent({
prompt: 'iak-mcp-daemon demo: approve to confirm the round-trip works.',
session: 'demo',
channels: Object.keys(announcerMap),
announce,
receiptsPath: config?.receipts?.path,
});
console.log(`[iak-mcp-daemon] demo intent created: id=${id}`);
}

// Keep the process alive.
process.stdin.resume();

// --- helpers --------------------------------------------------------------

function startChatReplyPoller({ apiKey, room, intervalMs }) {
const seen = new Set();
let primed = false;
const poll = async () => {
try {
const url = `https://groupmind.one/api/v1/rooms/${encodeURIComponent(room)}/messages?limit=30`;
const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
if (!res.ok) return;
const body = await res.json();
const messages = body?.messages || [];
for (const m of messages) {
if (seen.has(m.id)) continue;
seen.add(m.id);
if (!primed) continue; // ignore historical messages on first pass
const text = (m.body || '').trim();
const match = text.match(/^\/(approve|deny)\s+([a-f0-9]+)$/i);
if (!match) continue;
const decision = match[1].toLowerCase();
const id = match[2];
const intent = getIntent(id);
if (!intent) {
console.log(`[iak-mcp-daemon] /${decision} ${id} from ${m.from}: unknown intent, ignoring`);
continue;
}
const r = decideIntent(id, decision);
console.log(`[iak-mcp-daemon] /${decision} ${id} from ${m.from}: ${r.ok ? 'settled' : r.error}`);
}
primed = true;
} catch (e) {
console.warn(`[iak-mcp-daemon] poll error: ${e.message}`);
}
};
poll();
setInterval(poll, intervalMs);
}
32 changes: 32 additions & 0 deletions bin/iak-mcp.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
// SPDX-License-Identifier: AGPL-3.0-only
//
// Entry point for the IAK MCP server. Designed to be invoked by an MCP
// client over stdio. Example Claude Desktop / Code config:
//
// {
// "mcpServers": {
// "ide-agent-kit": {
// "command": "node",
// "args": ["/path/to/ide-agent-kit/bin/iak-mcp.mjs"]
// }
// }
// }
//
// Or via npx if installed as a package: `npx ide-agent-kit-mcp`.

import { runMcpServer } from '../src/mcp-server.mjs';

const argv = process.argv.slice(2);
let configPath;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--config' && argv[i + 1]) {
configPath = argv[i + 1];
i++;
}
}

runMcpServer({ configPath }).catch((e) => {
process.stderr.write(`[iak-mcp] fatal: ${e.message}\n`);
process.exit(1);
});
Loading
Loading