cc-libs/tools/trap-sandbox/test-integration/mcp.test.ts

74 lines
3.2 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { formatFailure, startCraftos, startSandbox, waitFor, type Sandbox } from "./harness.js";
// Drives the MCP tools end to end: a real reactive mcp-computer server (libsandbox +
// libmcpcomputer) on a real CraftOS-PC client, reached through the real gateway. The
// MCP app is driven in-process via inject() (no extra listener).
async function callTool(sandbox: Sandbox, name: string, args: Record<string, unknown> = {}): Promise<string> {
const res = await sandbox.mcpApp.inject({
method: "POST",
url: "/",
payload: { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } },
});
const body = res.json<{ result?: { content?: { text?: string }[] }; error?: { message?: string } }>();
if (body.error) {
throw new Error(`tool ${name} returned a JSON-RPC error: ${body.error.message}`);
}
return body.result?.content?.[0]?.text ?? "";
}
test("MCP exec-lua / run-file / write-file / probe round-trip over the gateway", async () => {
const sandbox = await startSandbox();
const craftos = startCraftos("mcp-server-client.lua", {
computerId: 7,
computerLabel: "base",
shellArgs: [sandbox.baseUrl, "-"],
});
try {
await waitFor(() => sandbox.registry.has("local", "7:base"), 12_000, "computer registered");
assert.equal(await callTool(sandbox, "probe-computers"), "ok from 7:base");
const exec = await callTool(sandbox, "exec-lua", { traposId: "7:base", code: "print('hi'); return 1 + 1" });
assert.match(exec, /^computer: 7:base\nok: true/);
assert.match(exec, /"value":2/); // return value serialized
assert.match(exec, /\nhi$/); // captured print output
const program = "local a, b = ...; print(a .. ':' .. b); return arg[1], arg[2], shell.getRunningProgram()";
const write = await callTool(sandbox, "write-file", {
traposId: "7:base",
path: "/mcp-it.lua",
content: program,
});
assert.match(write, /ok: true/);
assert.match(write, new RegExp(`bytes: ${program.length}`));
const run = await callTool(sandbox, "run-file", { traposId: "7:base", path: "/mcp-it.lua", args: ["install", "pkg"] });
assert.match(run, /^computer: 7:base\nok: true/);
assert.match(run, /"value":"install"/);
assert.match(run, /"value":"pkg"/);
assert.match(run, /"value":"\/mcp-it\.lua"/);
assert.match(run, /\ninstall:pkg$/);
// Read it back through exec-lua to prove the write actually landed.
const verify = await callTool(sandbox, "exec-lua", {
traposId: "7:base",
code: "local h = fs.open('/mcp-it.lua', 'r'); local c = h.readAll(); h.close(); return c",
});
assert.match(verify, /shell\.getRunningProgram/);
// Unknown traposId is a transport (not_connected) failure, not a crash.
const ghost = await callTool(sandbox, "exec-lua", { traposId: "9:ghost", code: "return 1" });
assert.match(ghost, /^computer: 9:ghost\nok: false\nerror: not_connected:/);
} catch (error) {
throw new Error(formatFailure(error instanceof Error ? error.message : String(error), craftos.snapshot()), {
cause: error,
});
} finally {
craftos.abort();
await craftos.done.catch(() => undefined);
await sandbox.close();
}
});