From 8db0dd2d8d6971f721c28e583fbe51221e38771d Mon Sep 17 00:00:00 2001 From: prompt.ac/@jeffrey Date: Sun, 02 Aug 2026 01:33:17 +0000 Subject: [PATCH] Keep Codex startup alive when Iris is offline --- toolchain/mcp/iris-mcp.mjs | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ toolchain/mcp/iris-mcp.test.mjs | 40 ++++++++++++++++++++++++++++++++++++++++ 2 file(s) changed, 112 insertion(s)(+), 0 deletion(s)(-) diff --git a/toolchain/mcp/iris-mcp.mjs b/toolchain/mcp/iris-mcp.mjs new file mode 100644 --- /dev/null +++ b/toolchain/mcp/iris-mcp.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node +// Keep the Iris MCP available when panda is asleep or off the network. + +import { spawn, spawnSync } from "node:child_process"; +import * as readline from "node:readline"; +import { pathToFileURL } from "node:url"; + +const ssh = process.env.IRIS_MCP_SSH || "ssh"; +const host = process.env.IRIS_MCP_HOST || "panda"; +const connectTimeout = process.env.IRIS_MCP_CONNECT_TIMEOUT || "3"; +const remoteCommand = process.env.IRIS_MCP_REMOTE_COMMAND + || "/opt/homebrew/bin/node /Users/fusermacminipanda/Developer/fuser/tools/iris/iris-mcp.mjs"; +const remoteProbe = process.env.IRIS_MCP_REMOTE_PROBE + || "test -x /opt/homebrew/bin/node && test -f /Users/fusermacminipanda/Developer/fuser/tools/iris/iris-mcp.mjs"; + +export function offlineResponse(message) { + if (message.id === undefined || message.id === null) return null; + const result = { + initialize: { + protocolVersion: message.params?.protocolVersion || "2025-06-18", + capabilities: { tools: { listChanged: false } }, + serverInfo: { name: "iris-offline", version: "1.0.0" }, + }, + "tools/list": { tools: [] }, + "resources/list": { resources: [] }, + "prompts/list": { prompts: [] }, + ping: {}, + }[message.method]; + if (result) return { jsonrpc: "2.0", id: message.id, result }; + return { + jsonrpc: "2.0", + id: message.id, + error: { code: -32001, message: "Iris is offline or unreachable" }, + }; +} + +function serveOffline(reason) { + console.error(`iris-mcp: Iris unavailable (${reason}); serving an empty MCP`); + const lines = readline.createInterface({ input: process.stdin, terminal: false }); + lines.on("line", (line) => { + if (!line.trim()) return; + try { + const response = offlineResponse(JSON.parse(line)); + if (response) process.stdout.write(`${JSON.stringify(response)}\n`); + } catch (error) { + process.stderr.write(`iris-mcp: ignored invalid JSON: ${error.message}\n`); + } + }); +} + +export function main() { + const common = ["-o", "BatchMode=yes", "-o", `ConnectTimeout=${connectTimeout}`, host]; + const probe = spawnSync(ssh, [...common, remoteProbe], { + encoding: "utf8", + timeout: (Number(connectTimeout) + 1) * 1000, + }); + if (probe.status !== 0) { + serveOffline(probe.error?.message || probe.stderr.trim() || `ssh exited ${probe.status}`); + return; + } + + const child = spawn(ssh, [...common, remoteCommand], { stdio: ["pipe", "pipe", "inherit"] }); + process.stdin.pipe(child.stdin); + child.stdout.pipe(process.stdout); + child.on("error", (error) => { + console.error(`iris-mcp: connection failed after probe: ${error.message}`); + process.exitCode = 0; + }); + child.on("exit", (code) => { process.exitCode = code || 0; }); +} + +if (import.meta.url === pathToFileURL(process.argv[1]).href) main(); diff --git a/toolchain/mcp/iris-mcp.test.mjs b/toolchain/mcp/iris-mcp.test.mjs new file mode 100644 --- /dev/null +++ b/toolchain/mcp/iris-mcp.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { createInterface } from "node:readline"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { offlineResponse } from "./iris-mcp.mjs"; + +test("offline server advertises no Iris tools", () => { + assert.deepEqual(offlineResponse({ jsonrpc: "2.0", id: 1, method: "tools/list" }), { + jsonrpc: "2.0", id: 1, result: { tools: [] }, + }); +}); + +test("unreachable Iris still completes the MCP handshake", async () => { + const child = spawn(process.execPath, [fileURLToPath(new URL("./iris-mcp.mjs", import.meta.url))], { + env: { ...process.env, IRIS_MCP_SSH: "/usr/bin/false" }, + stdio: ["pipe", "pipe", "pipe"], + }); + child.stdin.write(`${JSON.stringify({ + jsonrpc: "2.0", + id: 7, + method: "initialize", + params: { protocolVersion: "test-version" }, + })}\n`); + const line = await new Promise((resolve, reject) => { + const lines = createInterface({ input: child.stdout }); + lines.once("line", resolve); + child.once("error", reject); + child.once("exit", (code) => reject(new Error(`offline MCP exited before replying (${code})`))); + }); + assert.deepEqual(JSON.parse(line), { + jsonrpc: "2.0", id: 7, + result: { + protocolVersion: "test-version", + capabilities: { tools: { listChanged: false } }, + serverInfo: { name: "iris-offline", version: "1.0.0" }, + }, + }); + child.kill(); +}); -- tangled.sh