Files
unreal-engine-mcp-system-pl…/test/server.test.js
Bonchellon eba71c4ca8 Unified, portable Unreal Engine MCP system plugin
Merge of the two project copies into one self-contained plugin (the superset:
variable_op + variables.py, full pcg_op runtime/declarative/preset ops, the
CreateWidgetFloatAnimation widget tool, and full Voxel graph authoring).

Made fully project- and machine-agnostic — no hardcoded paths:
- New src/projectPaths.js auto-detects the host .uproject (walk-up), project
  name, Editor build target, log file, and engine install (EngineAssociation
  via launcher manifest/registry, else installed-engine scan). All overridable
  via UE_* env vars.
- Rewired buildOrchestrator/insights/launcher/insightsExporter/server.js and the
  Python workers (cpp_scaffold, live_coding, apply_graph, console) off the old
  C:/Github/ihy, IHY*, E:/UE_Versions and UE_5.7 literals onto the resolver.
- Voxel made optional: Build.cs auto-detects the Voxel plugin (env
  UE_MCP_WITH_VOXEL override) and the C++ compiles to stubs under WITH_MCP_VOXEL,
  so the module builds in projects without Voxel; .uplugin marks Voxel optional.
- De-branded the agent-gateway and docs; scrubbed a leaked API key; excluded
  node_modules/Binaries/Intermediate/__pycache__/secrets from the repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 01:07:24 +03:00

111 lines
3.6 KiB
JavaScript

// End-to-end smoke: spawn the MCP server as a subprocess, speak the wire
// protocol over stdio, verify tools/list and a dry_run apply_graph.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const SERVER = path.resolve(HERE, "..", "src", "server.js");
function startServer() {
const proc = spawn(process.execPath, [SERVER], { stdio: ["pipe", "pipe", "pipe"] });
let buf = "";
const pending = new Map();
let nextId = 1;
proc.stdout.on("data", chunk => {
buf += chunk.toString();
let idx;
while ((idx = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, idx); buf = buf.slice(idx + 1);
if (!line.trim()) continue;
try {
const msg = JSON.parse(line);
if (msg.id && pending.has(msg.id)) {
const { resolve } = pending.get(msg.id);
pending.delete(msg.id);
resolve(msg);
}
} catch { /* ignore non-JSON banner */ }
}
});
proc.stderr.on("data", () => { /* swallow */ });
function send(method, params) {
const id = nextId++;
const msg = { jsonrpc: "2.0", id, method, params };
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
proc.stdin.write(JSON.stringify(msg) + "\n");
setTimeout(() => {
if (pending.has(id)) { pending.delete(id); reject(new Error(`timeout: ${method}`)); }
}, 5000);
});
}
function close() { try { proc.kill(); } catch {} }
return { send, close };
}
async function initialize(send) {
return send("initialize", {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "test", version: "0" },
});
}
test("MCP server lists tools over stdio", async () => {
const { send, close } = startServer();
try {
await initialize(send);
const r = await send("tools/list", {});
const names = r.result.tools.map(t => t.name);
assert.ok(names.includes("apply_graph"));
assert.ok(names.includes("introspect"));
assert.ok(names.includes("compile"));
assert.ok(names.includes("read_graph"));
assert.ok(names.includes("ping_ue"));
assert.ok(names.includes("selection_op"));
assert.ok(names.includes("voxel_graph_op"));
assert.ok(names.includes("material_op"));
} finally { close(); }
});
test("apply_graph dry_run returns python without calling UE", async () => {
const { send, close } = startServer();
try {
await initialize(send);
const r = await send("tools/call", {
name: "apply_graph",
arguments: {
bp_path: "/Game/Test/BP_Foo",
nodes: [
{ id: "ev", kind: "event", target: "BeginPlay", position: [0, 0] },
{ id: "br", kind: "branch", position: [400, 0] },
],
edges: [{ from: ["ev", "Then"], to: ["br", "Exec"] }],
dry_run: true,
},
});
assert.equal(r.result.isError ?? false, false);
const payload = JSON.parse(r.result.content[0].text);
assert.equal(payload.dry_run, true);
assert.match(payload.python, /apply_graph\.entrypoint/);
assert.match(payload.python, /BeginPlay/);
} finally { close(); }
});
test("apply_graph rejects invalid graphs", async () => {
const { send, close } = startServer();
try {
await initialize(send);
const r = await send("tools/call", {
name: "apply_graph",
arguments: { bp_path: "not-a-game-path", nodes: [] },
});
assert.equal(r.result.isError, true);
assert.match(r.result.content[0].text, /bp_path/);
} finally { close(); }
});