Files
stack/packages/harness/src/mcp-server.mjs
T
jason.woltjeandClaude Opus 5.5 ce104b27cc bus, harness: PM task reads that fit a session (#1583)
Row 89, round 1. The broker's `tasks` read takes optional filter and
page arguments and returns a short form. The harness adds `list_tasks`
arguments (open tasks by default), a `get_task` tool, and compact JSON
replies capped below the 50000-character threshold where Claude Code
2.1.296 saves a tool result to a file outside the workspace, which was
the PM's fault in Jason's test. The bundle's tools line names the
harness's own tools.

Candidate manifest b8c5bb3a (agents/dewey/work/queue-89/), base
a3f6e9e6, no drift on the 12 paths. Builder Dewey. Approved by
Darkwing (comment 27940). Sage's gate on f04185fc plus the patch
2026-10-12T22:00:55Z-22:03:38Z: bus 108, harness 65, webui 86, cli
115, control-board 124, every scripts/test-*.sh green.

Live at the next bus restart (broker) and role session launch
(harness), both Mos's call (decision 109); Mos had the notice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 17:04:15 -05:00

61 lines
2.3 KiB
JavaScript

// Minimal stdio MCP server that gives a Claude Code session its typed
// tools: `node mcp-server.mjs <tools.json> <tool socket>`. Newline-delimited
// JSON-RPC 2.0 with initialize, tools/list, tools/call and ping. Each call
// goes to the runner's tool socket; the server holds no capability. A
// reply over REPLY_CAP is a tool error (tools.mjs, replyText).
import { readFileSync } from "node:fs";
import { createInterface } from "node:readline";
import { callTool, replyText } from "./tools.mjs";
const [toolsFile, socket] = process.argv.slice(2);
if (!toolsFile || !socket) {
process.stderr.write("usage: mcp-server.mjs <tools.json> <tool socket>\n");
process.exit(2);
}
const tools = JSON.parse(readFileSync(toolsFile, "utf8"));
const byName = new Map(tools.map((t) => [t.name, t]));
const send = (message) => process.stdout.write(JSON.stringify({ jsonrpc: "2.0", ...message }) + "\n");
const reply = (id, result) => send({ id, result });
const error = (id, code, message) => send({ id, error: { code, message } });
async function handle(m) {
const id = m.id;
switch (m.method) {
case "initialize":
return reply(id, {
protocolVersion: m.params?.protocolVersion ?? "2025-06-18",
capabilities: { tools: {} },
serverInfo: { name: "mosaic", version: "1" },
});
case "ping":
return reply(id, {});
case "tools/list":
return reply(id, { tools: tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.parameters })) });
case "tools/call": {
const name = m.params?.name;
if (!byName.has(name)) return error(id, -32602, `unknown tool: ${name}`);
try {
const result = await callTool(socket, name, m.params?.arguments ?? {});
return reply(id, { content: [{ type: "text", text: replyText(result) }] });
} catch (e) {
return reply(id, { content: [{ type: "text", text: e.message }], isError: true });
}
}
default:
if (id !== undefined) return error(id, -32601, `method not found: ${m.method}`);
}
}
createInterface({ input: process.stdin }).on("line", (line) => {
if (!line.trim()) return;
let m;
try {
m = JSON.parse(line);
} catch {
return error(null, -32700, "parse error");
}
handle(m).catch((e) => m.id !== undefined && error(m.id, -32603, e.message));
});