Files
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

187 lines
5.5 KiB
Python

"""Single-node / single-edge edit ops + navigation.
Ops:
delete_node {bp_path, function?, guid}
break_link {bp_path, function?, from:[guid,pin], to:[guid,pin]}
break_all {bp_path, function?, guid}
move_nodes {bp_path, function?, moves:[{guid,x,y}]}
resize_comment {bp_path, function?, guid, width, height}
set_comment {bp_path, function?, guid, text, bubble?}
open_bp {bp_path}
open_graph {bp_path, function}
jump_to_node {bp_path, function?, guid}
add_case_pin {bp_path, function?, guid} — switch ops
format_text {bp_path, function?, guid, format}
reconstruct {bp_path, function?, guid}
"""
from __future__ import annotations
import json
import os
import traceback
try:
import unreal # type: ignore
except ImportError:
unreal = None
def _lib():
L = getattr(unreal, "MCPGraphLibrary", None)
if L is None:
raise RuntimeError("MCPGraphLibrary missing — rebuild C++ plugin")
return L
def _load_bp(path: str):
a = unreal.EditorAssetLibrary.load_asset(path)
if not isinstance(a, unreal.Blueprint):
raise RuntimeError(f"not a blueprint: {path!r}")
return a
def _graph(bp, fn: str | None):
L = _lib()
if fn in (None, "", "EventGraph"):
return L.find_event_graph(bp)
return L.find_function_graph(bp, fn)
def _compile_save(bp):
_lib().compile_and_save_blueprint(bp)
unreal.EditorAssetLibrary.save_loaded_asset(bp)
def delete_node(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().delete_node(g, args["guid"]))
if ok: _compile_save(bp)
return {"ok": ok, "guid": args["guid"]}
def break_link(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
(fg, fp) = args["from"]; (tg, tp) = args["to"]
ok = bool(_lib().break_link(g, fg, fp, tg, tp))
if ok: _compile_save(bp)
return {"ok": ok}
def break_all(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().break_all_node_links(g, args["guid"]))
if ok: _compile_save(bp)
return {"ok": ok}
def move_nodes(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
moves = args.get("moves") or []
ids = [m["guid"] for m in moves]
pos = [unreal.Vector2D(float(m["x"]), float(m["y"])) for m in moves]
n = int(_lib().move_nodes(g, ids, pos))
return {"ok": n > 0, "moved": n}
def resize_comment(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().resize_comment_node(g, args["guid"], unreal.Vector2D(float(args["width"]), float(args["height"]))))
return {"ok": ok}
def set_comment(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().set_node_comment(g, args["guid"], str(args.get("text", "")), bool(args.get("bubble", False))))
return {"ok": ok}
def open_bp(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
return {"ok": bool(_lib().open_blueprint_editor(bp))}
def open_graph(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
return {"ok": bool(_lib().open_graph_in_editor(bp, g))}
def jump_to_node(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
return {"ok": bool(_lib().jump_to_node_in_editor(bp, g, args["guid"]))}
def add_case_pin(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().add_case_pin_to_switch(g, args["guid"]))
if ok: _compile_save(bp)
return {"ok": ok}
def format_text(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().format_text_set_format(g, args["guid"], args["format"]))
if ok: _compile_save(bp)
return {"ok": ok}
def reconstruct(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _graph(bp, args.get("function"))
ok = bool(_lib().reconstruct_node(g, args["guid"]))
return {"ok": ok}
OPS = {
"delete_node": delete_node,
"break_link": break_link,
"break_all": break_all,
"move_nodes": move_nodes,
"resize_comment": resize_comment,
"set_comment": set_comment,
"open_bp": open_bp,
"open_graph": open_graph,
"jump_to_node": jump_to_node,
"add_case_pin": add_case_pin,
"format_text": format_text,
"reconstruct": reconstruct,
}
def entrypoint(payload_json: str) -> str:
try:
p = json.loads(payload_json)
except Exception as e: # noqa: BLE001
return _tee({"ok": False, "error": f"bad json: {e}"}, {})
fn = OPS.get(p.get("op"))
if not fn:
return _tee({"ok": False, "error": f"unknown op {p.get('op')!r}", "available": list(OPS)}, p)
try:
return _tee(fn(p), p)
except Exception as e: # noqa: BLE001
return _tee({"ok": False, "error": str(e), "traceback": traceback.format_exc()}, p)
def _tee(result: dict, p: dict) -> str:
txt = json.dumps(result)
try:
if unreal is not None:
sd = unreal.Paths.project_saved_dir() + "MCP/"
os.makedirs(sd, exist_ok=True)
rid = p.get("__rid") or "last"
with open(sd + f"{rid}.json", "w", encoding="utf-8") as f:
f.write(txt)
unreal.log("[MCP-EDIT] " + txt[:1500])
except Exception:
pass
return txt