diff --git a/.gitignore b/.gitignore index b9c6f7a..9b87c72 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,6 @@ agent-gateway/.generated-audio/ # real API keys live in a local, untracked copy integrations.local.json +# Local setup-wizard overrides (machine-specific — never commit) +mcp.config.json + diff --git a/README.md b/README.md index d82782e..907d760 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,18 @@ It has two halves that ship together: 4. In the editor, enable **Remote Control** and start its web server (`WebControl.StartServer`, or set `WebControl.EnableServerOnStartup=true`). -5. Install the MCP server deps: +5. **Run the setup wizard** — it installs deps, confirms the auto-detected + project/engine, checks the editor side, and prints the exact client-config + snippet to paste: ```bash cd Plugins/UEBlueprintMCP - npm install + npm run setup ``` + (Prefer to do it by hand? `npm install`, then point your client at + `src/server.js` as below.) + 6. Point your MCP client at `src/server.js`, e.g.: ```json @@ -62,6 +67,20 @@ It has two halves that ship together: Call `ping_ue` first to confirm the editor is reachable. +### First-run check & `doctor` + +The first time the server starts it prints a one-time environment check to its +stderr (visible in your client's MCP logs) and points you at `npm run setup` if +anything is off. You can re-run the checks any time: + +- `npm run doctor` — print the diagnostics report in a terminal. +- the **`doctor`** MCP tool — same checks from inside a session, so the assistant + can diagnose and guide you. + +The wizard pins any overrides (project dir, engine dir, Remote Control URL, ...) +to a git-ignored `mcp.config.json`. Precedence everywhere is **env var > config > +auto-detect**. + --- ## Zero-config path resolution diff --git a/package.json b/package.json index 84e75cc..7152044 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ }, "scripts": { "start": "node src/server.js", + "setup": "node src/setup.js", + "doctor": "node src/doctor.js", "test": "node --test \"test/*.test.js\"" }, "dependencies": { diff --git a/src/doctor.js b/src/doctor.js new file mode 100644 index 0000000..cbddd39 --- /dev/null +++ b/src/doctor.js @@ -0,0 +1,170 @@ +// doctor.js — environment diagnostics for the UE MCP server. +// +// Used three ways: +// 1. `node src/doctor.js` — print a one-shot report (npm run doctor) +// 2. by the setup wizard — drives the interactive fixes +// 3. by server.js on first run — prints a summary so the user knows what to fix +// +// Intentionally imports ONLY zero-dependency modules at the top level so it runs +// before `npm install` (checking for the deps is one of its jobs). Heavy/optional +// deps are probed with dynamic import inside try/catch. + +import { ping, executePythonInUE } from "./ueBridge.js"; +import * as PP from "./projectPaths.js"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +export const STATUS = { OK: "ok", WARN: "warn", FAIL: "fail", SKIP: "skip" }; + +// Run every check. `deep` also probes the live editor (Remote Control + C++ module). +export async function runDiagnostics({ deep = true } = {}) { + const checks = []; + + // --- project --- + const proj = PP.projectDir(); + const uproj = PP.uprojectPath(); + checks.push(uproj + ? { id: "project", label: "Project", status: STATUS.OK, + detail: `${PP.projectName()} (${uproj})` } + : { id: "project", label: "Project", status: STATUS.FAIL, + detail: `no .uproject found above ${proj}`, + fix: "Place the plugin under /Plugins/, or set UE_PROJECT_DIR / run the setup wizard." }); + + // --- engine --- + const eng = PP.engineDir(); + checks.push(eng + ? { id: "engine", label: "Engine", status: STATUS.OK, detail: eng } + : { id: "engine", label: "Engine", status: STATUS.FAIL, + detail: "could not locate an Unreal Engine install", + fix: "Set UE_ENGINE_DIR to the folder containing /Engine, or run the setup wizard." }); + + // --- node deps --- + const deps = await checkNodeDeps(); + checks.push(deps); + + // --- remote control --- + let rc = null; + if (deep) { + const url = PP.remoteControlUrl(); + const p = await ping(); + rc = p.ok + ? { id: "remote_control", label: "Remote Control", status: STATUS.OK, detail: url } + : { id: "remote_control", label: "Remote Control", status: STATUS.FAIL, + detail: `unreachable at ${url} (${p.error})`, + fix: "In UE: enable the 'Remote Control API' plugin, then run console command `WebControl.StartServer` " + + "(or set WebControl.EnableServerOnStartup=true). Make sure the editor is open." }; + checks.push(rc); + } + + // --- python plugin + C++ module (only if RC is up) --- + if (deep) { + if (rc && rc.status === STATUS.OK) { + const probe = await probeEditor(); + if (!probe.ok) { + checks.push({ id: "python", label: "Python plugin", status: STATUS.FAIL, + detail: probe.error || "ExecutePythonCommand failed", + fix: "Enable the 'Python Editor Script Plugin' in UE and restart the editor." }); + } else { + checks.push({ id: "python", label: "Python plugin", status: STATUS.OK, + detail: probe.engine ? `UE ${probe.engine}` : "ExecutePythonCommand works" }); + checks.push(probe.has_mcp + ? { id: "cpp", label: "C++ module", status: STATUS.OK, detail: "UMCPGraphLibrary is loaded" } + : { id: "cpp", label: "C++ module", status: STATUS.FAIL, + detail: "unreal.MCPGraphLibrary not found — UEBlueprintMCPEditor isn't built", + fix: "Generate project files and build the editor target (the plugin has a C++ module)." }); + if (probe.has_voxel !== undefined) { + checks.push({ id: "voxel", label: "Voxel (optional)", status: probe.has_voxel ? STATUS.OK : STATUS.SKIP, + detail: probe.has_voxel ? "voxel_graph_op available" : "Voxel plugin not installed — voxel_graph_op disabled" }); + } + } + } else { + checks.push({ id: "python", label: "Python plugin", status: STATUS.SKIP, detail: "skipped (editor not reachable)" }); + checks.push({ id: "cpp", label: "C++ module", status: STATUS.SKIP, detail: "skipped (editor not reachable)" }); + } + } + + const summary = summarize(checks); + return { checks, summary, ok: summary.fail === 0 }; +} + +async function checkNodeDeps() { + const missing = []; + for (const m of ["@modelcontextprotocol/sdk/server/index.js", "better-sqlite3"]) { + try { await import(m); } catch { missing.push(m.split("/")[0].replace(/^@/, "@")); } + } + if (!missing.length) { + return { id: "node_deps", label: "Node deps", status: STATUS.OK, detail: "installed" }; + } + const sqliteOnly = missing.length === 1 && missing[0] === "better-sqlite3"; + return { + id: "node_deps", + label: "Node deps", + status: sqliteOnly ? STATUS.WARN : STATUS.FAIL, + detail: `missing: ${missing.join(", ")}${sqliteOnly ? " (only the insights_* tools need it)" : ""}`, + fix: "Run `npm install` in the plugin folder.", + }; +} + +// Ask the live editor to write a probe file we can read back (RC doesn't echo +// Python stdout, so we tee to /Saved/MCP/doctor.json). +async function probeEditor() { + const outFile = path.join(PP.projectDir(), "Saved", "MCP", "doctor.json").replace(/\\/g, "/"); + try { fs.rmSync(outFile, { force: true }); } catch { /* ignore */ } + const py = [ + "import unreal, json, os", + "d = {'ok': True}", + "try: d['has_mcp'] = hasattr(unreal, 'MCPGraphLibrary')", + "except Exception as e: d['has_mcp'] = False", + "try: d['has_voxel'] = hasattr(unreal, 'MCPVoxelGraphLibrary') and unreal.MCPVoxelGraphLibrary.is_voxel_graph_asset is not None", + "except Exception: d['has_voxel'] = False", + "try: d['engine'] = unreal.SystemLibrary.get_engine_version()", + "except Exception: pass", + "p = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_saved_dir()) + 'MCP/'", + "os.makedirs(p, exist_ok=True)", + "open(p + 'doctor.json', 'w', encoding='utf-8').write(json.dumps(d))", + ].join("\n"); + const r = await executePythonInUE(py, { timeoutMs: 8000 }); + if (!r.ok) return { ok: false, error: r.error }; + // Give the editor a tick to flush the file. + for (let i = 0; i < 20; i++) { + try { + const txt = fs.readFileSync(outFile, "utf8"); + return { ok: true, ...JSON.parse(txt) }; + } catch { await sleep(100); } + } + return { ok: false, error: "probe file was not written (Python ran but produced no output)" }; +} + +function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } + +function summarize(checks) { + const s = { ok: 0, warn: 0, fail: 0, skip: 0 }; + for (const c of checks) s[c.status] = (s[c.status] || 0) + 1; + return s; +} + +const ICON = { ok: "✓", warn: "!", fail: "✗", skip: "·" }; + +export function formatReport({ checks, summary }) { + const lines = []; + for (const c of checks) { + lines.push(` ${ICON[c.status] || "?"} ${c.label.padEnd(18)} ${c.detail || ""}`); + if (c.fix && (c.status === STATUS.FAIL || c.status === STATUS.WARN)) { + lines.push(` → ${c.fix}`); + } + } + lines.push(""); + lines.push(` ${summary.ok} ok · ${summary.warn} warn · ${summary.fail} fail · ${summary.skip} skipped`); + return lines.join("\n"); +} + +// Standalone: `node src/doctor.js` +const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; +if (isMain) { + const res = await runDiagnostics({ deep: true }); + console.log("\nUnreal Engine MCP — diagnostics\n"); + console.log(formatReport(res)); + console.log(res.ok ? "\nAll good. ✓\n" : "\nSome checks failed — run `npm run setup` to fix interactively.\n"); + process.exit(res.ok ? 0 : 1); +} diff --git a/src/projectPaths.js b/src/projectPaths.js index 7b87512..3dc5c76 100644 --- a/src/projectPaths.js +++ b/src/projectPaths.js @@ -24,6 +24,42 @@ const HERE = path.dirname(fileURLToPath(import.meta.url)); // /src export const PLUGIN_DIR = path.resolve(HERE, ".."); // const norm = (p) => (p ? p.replace(/\\/g, "/") : p); +// --------------------------------------------------------------------------- +// Persisted config (written by the setup wizard) — lets a user pin overrides +// when auto-detection guesses wrong. Precedence everywhere is: env > config > +// auto-detect. Lives next to the plugin so it needs no project dir to find, and +// is git-ignored so it never gets published. +// --------------------------------------------------------------------------- + +export function configPath() { + return norm(process.env.UE_MCP_CONFIG || path.join(PLUGIN_DIR, "mcp.config.json")); +} + +let _cfg; +export function loadConfig() { + if (_cfg !== undefined) return _cfg; + try { + _cfg = JSON.parse(fs.readFileSync(configPath(), "utf8")); + if (!_cfg || typeof _cfg !== "object") _cfg = {}; + } catch { _cfg = {}; } + return _cfg; +} + +export function saveConfig(obj) { + const merged = { ...loadConfig(), ...obj }; + // Drop empty values so we don't pin blanks. + for (const k of Object.keys(merged)) { + if (merged[k] === "" || merged[k] == null) delete merged[k]; + } + fs.writeFileSync(configPath(), JSON.stringify(merged, null, 2) + "\n", "utf8"); + _cfg = merged; + // Invalidate caches that depend on config. + _project = null; _engine = null; + return merged; +} + +const cfg = (k) => loadConfig()[k]; + // --------------------------------------------------------------------------- // Project discovery // --------------------------------------------------------------------------- @@ -33,8 +69,8 @@ let _project = null; function discoverProject() { if (_project) return _project; - // 1. Explicit override. - const envDir = process.env.UE_PROJECT_DIR; + // 1. Explicit override (env, then persisted config). + const envDir = process.env.UE_PROJECT_DIR || cfg("projectDir"); if (envDir && fs.existsSync(envDir)) { _project = fromDir(envDir) || { projectDir: norm(envDir), uproject: null, projectName: null }; return _project; @@ -62,7 +98,7 @@ function fromDir(dir) { try { entries = fs.readdirSync(dir); } catch { return null; } const up = entries.find((f) => f.toLowerCase().endsWith(".uproject")); if (!up) return null; - const override = process.env.UE_UPROJECT_NAME; + const override = process.env.UE_UPROJECT_NAME || cfg("uprojectName"); const uprojectName = override && entries.includes(override) ? override : up; return { projectDir: norm(dir), @@ -75,15 +111,17 @@ export function projectDir() { return discoverProject().projectDir; } export function uprojectPath() { return discoverProject().uproject; } export function uprojectName() { if (process.env.UE_UPROJECT_NAME) return process.env.UE_UPROJECT_NAME; + if (cfg("uprojectName")) return cfg("uprojectName"); const up = discoverProject().uproject; return up ? path.basename(up) : null; } export function projectName() { return discoverProject().projectName; } // The Editor build target. Convention is "Editor"; a project can -// override via env or by naming its .Target.cs differently. +// override via env, config, or by naming its .Target.cs differently. export function buildTarget() { if (process.env.UE_BUILD_TARGET) return process.env.UE_BUILD_TARGET; + if (cfg("buildTarget")) return cfg("buildTarget"); const name = projectName(); return name ? `${name}Editor` : null; } @@ -91,10 +129,16 @@ export function buildTarget() { // Default UE log file name is ".log" under Saved/Logs. export function logName() { if (process.env.UE_LOG_NAME) return process.env.UE_LOG_NAME; + if (cfg("logName")) return cfg("logName"); const name = projectName(); return name ? `${name}.log` : "UnrealEditor.log"; } +// Remote Control base URL the Node server talks to. +export function remoteControlUrl() { + return process.env.UE_REMOTE_CONTROL_URL || cfg("remoteControlUrl") || "http://127.0.0.1:30010"; +} + // --------------------------------------------------------------------------- // Engine discovery // --------------------------------------------------------------------------- @@ -117,10 +161,13 @@ export function engineDir(override) { } function resolveEngine() { - // 1. Explicit override wins. + // 1. Explicit override wins (env, then persisted config). if (process.env.UE_ENGINE_DIR && fs.existsSync(process.env.UE_ENGINE_DIR)) { return norm(process.env.UE_ENGINE_DIR); } + if (cfg("engineDir") && fs.existsSync(cfg("engineDir"))) { + return norm(cfg("engineDir")); + } const assoc = engineAssociation(); @@ -221,6 +268,9 @@ export function insightsExe() { if (process.env.UE_INSIGHTS_EXE && fs.existsSync(process.env.UE_INSIGHTS_EXE)) { return process.env.UE_INSIGHTS_EXE; } + if (cfg("insightsExe") && fs.existsSync(cfg("insightsExe"))) { + return norm(cfg("insightsExe")); + } const eng = engineDir(); if (eng) { const p = path.join(eng, "Engine/Binaries/Win64/UnrealInsights.exe"); @@ -238,3 +288,26 @@ export function insightsExe() { } return null; } + +// --------------------------------------------------------------------------- +// First-run marker — lets the server show the setup hint exactly once, and lets +// the wizard record that setup completed. Stored under Saved/ (git-ignored by +// UE) so it's per-checkout, not committed. +// --------------------------------------------------------------------------- + +export function markerPath() { + return norm(path.join(projectDir(), "Saved", "MCP", ".mcp-setup.json")); +} + +export function isFirstRun() { + try { return !fs.existsSync(markerPath()); } catch { return false; } +} + +export function markRun(extra = {}) { + try { + const p = markerPath(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify({ setupAt: new Date().toISOString(), ...extra }, null, 2) + "\n", "utf8"); + return p; + } catch { return null; } +} diff --git a/src/server.js b/src/server.js index c90a54f..5c7000d 100644 --- a/src/server.js +++ b/src/server.js @@ -30,6 +30,7 @@ import { handleCompareTraces, handleQuery, handleReport, handleRenderCharts, } from "./insights.js"; import * as PP from "./projectPaths.js"; +import { runDiagnostics, formatReport } from "./doctor.js"; const UE_SIDE_DIR_FOR_JS = UE_SIDE_DIR.replace(/\\/g, "/"); const DEFAULT_PROJECT_DIR = PP.projectDir(); @@ -107,6 +108,16 @@ const TOOLS = [ description: "Check whether UE Remote Control is reachable. Useful as a first call in any session.", inputSchema: { type: "object", properties: {} }, }, + { + name: "doctor", + description: "First-run / setup diagnostics: verifies project & engine detection, Node deps, Remote Control reachability, the Python plugin, and whether the C++ module is built. Returns each check with a suggested fix. Run this when something isn't working, or guide a new user through it. For interactive fixes the user runs `npm run setup` in the plugin folder.", + inputSchema: { + type: "object", + properties: { + deep: { type: "boolean", description: "Also probe the live editor (Remote Control / Python / C++). Default true." }, + }, + }, + }, { name: "get_active_blueprint", description: "Return the path of the Blueprint currently open in the UE editor (most-recently-focused). Use to verify auto-detect target before calling apply_graph without bp_path.", @@ -978,6 +989,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => { case "compile": return await handleCompile(args); case "read_graph": return await handleReadGraph(args || {}); case "ping_ue": return await handlePing(); + case "doctor": return await handleDoctor(args); case "get_active_blueprint": return await handleGetActiveBp(); case "read_last_result": return await handleReadLastResult(args || {}); case "read_python_log": return await handleReadPythonLog(args || {}); @@ -1189,6 +1201,18 @@ async function handlePing() { return okResult(r); } +async function handleDoctor(args = {}) { + const deep = args.deep !== false; + const res = await runDiagnostics({ deep }); + return okResult({ + ok: res.ok, + summary: res.summary, + report: formatReport(res), + checks: res.checks, + setup_hint: res.ok ? undefined : "Run `npm run setup` in the plugin folder for an interactive fix.", + }); +} + async function handleGetActiveBp() { const py = [ "import json, unreal, os", @@ -1317,4 +1341,20 @@ async function attachInlineImage(result) { function okResult(obj) { return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] }; } function errResult(msg) { return { content: [{ type: "text", text: msg }], isError: true }; } +// First-run hint: the stdio channel is the MCP protocol, so we can't prompt — +// instead we print a one-time diagnostic to stderr (visible in the client's MCP +// logs) pointing the user at `npm run setup`, then drop a marker so it's quiet +// afterwards. Best-effort: never let setup diagnostics block or break startup. +if (PP.isFirstRun()) { + try { + const res = await runDiagnostics({ deep: true }); + process.stderr.write("\n[ue-mcp] First run — environment check:\n\n" + formatReport(res) + "\n"); + if (!res.ok) { + process.stderr.write("\n[ue-mcp] Run `npm run setup` in the plugin folder to fix the items above, " + + "or call the `doctor` tool from your client.\n\n"); + } + } catch { /* ignore — diagnostics are advisory only */ } + PP.markRun({ via: "server-first-run" }); +} + await server.connect(new StdioServerTransport()); diff --git a/src/setup.js b/src/setup.js new file mode 100644 index 0000000..84ae6d7 --- /dev/null +++ b/src/setup.js @@ -0,0 +1,121 @@ +// setup.js — interactive first-run wizard for the UE MCP server. +// +// npm run setup (or: node src/setup.js) +// +// Walks the user through: installing Node deps, confirming the auto-detected +// project/engine (pinning overrides to mcp.config.json when wrong), checking the +// editor side (Remote Control + Python plugin + C++ module), and finally printing +// the MCP-client config snippet to paste. Writes a first-run marker so server.js +// stops nagging once setup is done. +// +// Top-level imports are zero-dependency on purpose — the wizard must run BEFORE +// `npm install`. + +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import * as PP from "./projectPaths.js"; +import { runDiagnostics, formatReport, STATUS } from "./doctor.js"; + +const rl = createInterface({ input, output }); +const ask = (q) => rl.question(q); +async function askYN(q, def = true) { + const a = (await ask(`${q} ${def ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase(); + if (!a) return def; + return a === "y" || a === "yes"; +} + +function hr() { console.log("─".repeat(64)); } + +async function npmInstall() { + const cmd = process.platform === "win32" ? "npm.cmd" : "npm"; + console.log(`\nRunning \`npm install\` in ${PP.PLUGIN_DIR} …\n`); + return new Promise((resolve) => { + const child = spawn(cmd, ["install"], { cwd: PP.PLUGIN_DIR, stdio: "inherit", shell: process.platform === "win32" }); + child.on("close", (code) => resolve(code === 0)); + child.on("error", () => resolve(false)); + }); +} + +async function main() { + console.log("\n Unreal Engine MCP — setup wizard\n"); + hr(); + + // 1. First diagnostic pass. + let res = await runDiagnostics({ deep: true }); + console.log("\nDetected:\n"); + console.log(formatReport(res)); + hr(); + + // 2. Node deps. + let deps = res.checks.find((c) => c.id === "node_deps"); + if (deps && deps.status !== STATUS.OK) { + if (await askYN("Node dependencies are missing. Install them now?", true)) { + const ok = await npmInstall(); + console.log(ok ? "\n✓ npm install finished.\n" : "\n✗ npm install failed — install manually, then re-run setup.\n"); + } + } + + // 3. Confirm project. + const proj = res.checks.find((c) => c.id === "project"); + if (proj && proj.status === STATUS.OK) { + if (!(await askYN(`Use detected project "${PP.projectName()}" at ${PP.projectDir()}?`, true))) { + const dir = (await ask(" Path to the folder containing your .uproject: ")).trim(); + if (dir) { PP.saveConfig({ projectDir: dir }); console.log(" ✓ pinned to mcp.config.json"); } + } + } else { + console.log("\nNo .uproject was auto-detected."); + const dir = (await ask(" Path to the folder containing your .uproject (blank to skip): ")).trim(); + if (dir) { PP.saveConfig({ projectDir: dir }); console.log(" ✓ pinned to mcp.config.json"); } + } + + // 4. Confirm engine. + const eng = res.checks.find((c) => c.id === "engine"); + const engDetected = PP.engineDir(); + if (eng && eng.status === STATUS.OK) { + if (!(await askYN(`Use detected engine at ${engDetected}?`, true))) { + const dir = (await ask(" Path to the engine root (folder containing /Engine): ")).trim(); + if (dir) { PP.saveConfig({ engineDir: dir }); console.log(" ✓ pinned to mcp.config.json"); } + } + } else { + const dir = (await ask(" Engine root not found. Enter it (folder containing /Engine, blank to skip): ")).trim(); + if (dir) { PP.saveConfig({ engineDir: dir }); console.log(" ✓ pinned to mcp.config.json"); } + } + + // 5. Optional custom Remote Control URL. + if (await askYN(`Remote Control URL is ${PP.remoteControlUrl()}. Change it?`, false)) { + const url = (await ask(" Remote Control URL: ")).trim(); + if (url) { PP.saveConfig({ remoteControlUrl: url }); console.log(" ✓ pinned to mcp.config.json"); } + } + + // 6. Re-check the editor until reachable (or user gives up). + hr(); + for (;;) { + res = await runDiagnostics({ deep: true }); + const rc = res.checks.find((c) => c.id === "remote_control"); + const cpp = res.checks.find((c) => c.id === "cpp"); + console.log("\nRe-check:\n"); + console.log(formatReport(res)); + const blocked = (rc && rc.status === STATUS.FAIL) || (cpp && cpp.status === STATUS.FAIL); + if (!blocked) break; + if (!(await askYN("\nFix the items above in the editor, then retry?", true))) break; + } + + // 7. Finish — write marker and print client config. + PP.markRun({ via: "wizard" }); + const serverEntry = path.join(PP.PLUGIN_DIR, "src", "server.js").replace(/\\/g, "/"); + hr(); + console.log("\n✓ Setup complete. Add this to your MCP client config:\n"); + console.log(JSON.stringify({ + mcpServers: { + "ue-blueprint": { command: "node", args: [serverEntry] }, + }, + }, null, 2)); + console.log("\nThen call `ping_ue` in a session to confirm the connection."); + if (PP.configPath()) console.log(`Overrides (if any) saved to: ${PP.configPath()}`); + console.log(""); + rl.close(); +} + +main().catch((e) => { console.error("setup wizard error:", e); rl.close(); process.exit(1); }); diff --git a/src/ueBridge.js b/src/ueBridge.js index 0747dde..3c40a8d 100644 --- a/src/ueBridge.js +++ b/src/ueBridge.js @@ -4,7 +4,9 @@ // Script Plugin"). Then `WebControl.EnableServerOnStartup = true` or run the // console command `WebControl.StartServer` in the editor. Default port 30010. -const DEFAULT_BASE = process.env.UE_REMOTE_CONTROL_URL || "http://127.0.0.1:30010"; +import { remoteControlUrl } from "./projectPaths.js"; + +const DEFAULT_BASE = remoteControlUrl(); /** * Execute an arbitrary Python command inside the UE editor.