"""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