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>
111 lines
3.6 KiB
JavaScript
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(); }
|
|
});
|