import { test } from "node:test"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { createInterface } from "node:readline"; import { SOURCES } from "../src/bundle.mjs"; import { typedTools } from "../src/tools.mjs"; import { fakeToolSocket, resolvedFor, scratch } from "./helpers.mjs"; // The server over stdio: send(message) resolves with the reply to its id. function serve(t, toolsFile, socket) { const child = spawn(process.execPath, [SOURCES.mcpServer, toolsFile, socket], { stdio: ["pipe", "pipe", "pipe"] }); t.after(() => child.kill()); const waiting = new Map(); const orphans = []; createInterface({ input: child.stdout }).on("line", (line) => { const m = JSON.parse(line); const w = waiting.get(m.id); if (w) { waiting.delete(m.id); w(m); } else orphans.push(m); }); let next = 1; return { orphans, send(method, params) { const id = next++; return new Promise((resolve) => { waiting.set(id, resolve); child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n"); }); }, raw: (text) => child.stdin.write(text), }; } async function setup(t) { const dir = scratch(t); const tools = typedTools({ resolved: resolvedFor(t, "pm"), tracker: true }); const toolsFile = join(dir, "tools.json"); writeFileSync(toolsFile, JSON.stringify(tools)); const sock = await fakeToolSocket(t, dir, (tool, args) => { if (tool === "launch") throw new Error("capacity-full"); return { tool, args }; }); return { dir, tools, sock, server: serve(t, toolsFile, sock.path) }; } test("initialize, ping and tools/list", async (t) => { const { tools, server } = await setup(t); const init = await server.send("initialize", { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "x", version: "0" } }); assert.deepEqual(init.result, { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "mosaic", version: "1" } }); assert.deepEqual((await server.send("ping")).result, {}); const list = (await server.send("tools/list")).result.tools; assert.deepEqual(list.map((x) => x.name), tools.map((x) => x.name)); const launch = list.find((x) => x.name === "launch"); assert.deepEqual(launch.inputSchema, tools.find((x) => x.name === "launch").parameters); }); test("tools/call goes through the tool socket; a refusal is an isError result", async (t) => { const { sock, server } = await setup(t); const ok = (await server.send("tools/call", { name: "list_agents", arguments: { scope: "all" } })).result; assert.equal(ok.isError, undefined); assert.deepEqual(JSON.parse(ok.content[0].text), { tool: "list_agents", args: { scope: "all" } }); const refused = (await server.send("tools/call", { name: "launch", arguments: { instance: "coder" } })).result; assert.equal(refused.isError, true); assert.equal(refused.content[0].text, "refused: capacity-full"); assert.deepEqual(sock.calls.map((c) => c.tool), ["list_agents", "launch"]); }); test("unknown tools and methods are JSON-RPC errors and never reach the socket", async (t) => { const { sock, server } = await setup(t); const unknown = await server.send("tools/call", { name: "resolve_decision", arguments: {} }); assert.equal(unknown.error.code, -32602); assert.equal((await server.send("resources/list")).error.code, -32601); server.raw("not json\n"); // A notification gets no reply; the next request still answers. server.raw(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"); assert.deepEqual((await server.send("ping")).result, {}); assert.deepEqual(server.orphans, [{ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }]); assert.equal(sock.calls.length, 0); }); test("a missing argument is a usage error", async (t) => { const child = spawn(process.execPath, [SOURCES.mcpServer], { stdio: "pipe" }); const code = await new Promise((r) => child.on("exit", r)); assert.equal(code, 2); });