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

154 lines
5.0 KiB
Python

"""Read-only discovery ops via MCPGraphLibrary v0.3.
Ops:
list_open_bps {} — open BP editors
list_graphs {bp_path} — all graphs of a BP
describe_bp {bp_path} — parent/interfaces/vars/components/funcs/dispatchers
get_selection {bp_path} — selected nodes in BP editor
resolve_call {bp_path, function, guid} — CallFunction node → source bp/class
find_var_refs {bp_path, name} — variable usage sites
find_fn_refs {bp_path, name} — function call sites in this BP
find_bp_by_parent {parent_class_path} — AssetRegistry search
get_refs_of {asset_path} — assets referenced by this asset
get_referencers {asset_path} — assets that reference this asset
read_function {bp_path, function} — wrapper over apply_graph.read_graph for function
"""
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 _find_graph(bp, name: str):
L = _lib()
if name in (None, "", "EventGraph"):
return L.find_event_graph(bp)
return L.find_function_graph(bp, name)
def list_open_bps(args: dict) -> dict:
return {"ok": True, "open": list(_lib().list_open_blueprints())}
def list_graphs(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
raw = _lib().list_graphs_json(bp)
return {"ok": True, "bp_path": args["bp_path"], **json.loads(raw)}
def describe_bp(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
raw = _lib().describe_blueprint_json(bp)
return {"ok": True, **json.loads(raw)}
def get_selection(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
raw = _lib().get_selected_nodes_json(bp)
return {"ok": True, "bp_path": args["bp_path"], **json.loads(raw)}
def resolve_call(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
g = _find_graph(bp, args.get("function") or "EventGraph")
if g is None:
return {"ok": False, "error": "graph not found"}
raw = _lib().resolve_function_source_json(g, args["guid"])
return {"ok": True, **json.loads(raw)}
def find_var_refs(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
raw = _lib().find_variable_references_json(bp, args["name"])
return {"ok": True, **json.loads(raw)}
def find_fn_refs(args: dict) -> dict:
bp = _load_bp(args["bp_path"])
raw = _lib().find_function_references_json(bp, args["name"])
return {"ok": True, **json.loads(raw)}
def find_bp_by_parent(args: dict) -> dict:
found = list(_lib().find_blueprints_by_parent(args["parent_class_path"]))
return {"ok": True, "blueprints": found, "count": len(found)}
def get_refs_of(args: dict) -> dict:
return {"ok": True, "references": list(_lib().get_referenced_assets(args["asset_path"]))}
def get_referencers(args: dict) -> dict:
return {"ok": True, "referencers": list(_lib().get_referencers_of_asset(args["asset_path"]))}
def read_function(args: dict) -> dict:
# Convenience: read_graph for a named function
import importlib, apply_graph
importlib.reload(apply_graph)
return apply_graph.read_graph(args["bp_path"], args.get("function") or "EventGraph")
OPS = {
"list_open_bps": list_open_bps,
"list_graphs": list_graphs,
"describe_bp": describe_bp,
"get_selection": get_selection,
"resolve_call": resolve_call,
"find_var_refs": find_var_refs,
"find_fn_refs": find_fn_refs,
"find_bp_by_parent": find_bp_by_parent,
"get_refs_of": get_refs_of,
"get_referencers": get_referencers,
"read_function": read_function,
}
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}"}, p={})
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-DISCOVER] " + txt[:1500])
except Exception:
pass
return txt