74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { formatFailure, startCraftos, startServer, waitFor, type Server } from "./harness.js";
|
|
|
|
// Drives the MCP tools end to end: a real reactive mcp-computer server (libcloud +
|
|
// 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(server: Server, name: string, args: Record<string, unknown> = {}): Promise<string> {
|
|
const res = await server.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 server = await startServer();
|
|
const craftos = startCraftos("mcp-server-client.lua", {
|
|
computerId: 7,
|
|
computerLabel: "base",
|
|
shellArgs: [server.baseUrl, "-"],
|
|
});
|
|
try {
|
|
await waitFor(() => server.registry.has("global", "7:base"), 12_000, "computer registered");
|
|
|
|
assert.equal(await callTool(server, "probe-computers"), "ok from 7:base");
|
|
|
|
const exec = await callTool(server, "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(server, "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(server, "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(server, "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(server, "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 server.close();
|
|
}
|
|
});
|