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>
This commit is contained in:
180
ue_side/variables.py
Normal file
180
ue_side/variables.py
Normal file
@ -0,0 +1,180 @@
|
||||
"""Blueprint member-variable authoring ops.
|
||||
|
||||
Wraps the (already-compiled) C++ helpers on unreal.MCPGraphLibrary:
|
||||
AddMemberVariable / SetVariableDefaultValue / SetVariableMetadata /
|
||||
RenameVariable / DeleteVariable / DescribeBlueprintJson.
|
||||
|
||||
Reachable from JS as the `variable_op` tool, and also merged into `asset_op`'s
|
||||
op-table for zero-restart availability (see assets.py).
|
||||
|
||||
Ops:
|
||||
add {bp_path, name, type, default?, instance_editable?, expose_on_spawn?,
|
||||
category?, tooltip?} — create a member variable, optionally set its
|
||||
default value + metadata. type is one of
|
||||
bool|int|float|double|string|name|text or a
|
||||
class path / short name for an object ref.
|
||||
set_default {bp_path, name, value}
|
||||
set_meta {bp_path, name, instance_editable?, expose_on_spawn?, category?, tooltip?}
|
||||
rename {bp_path, old, new}
|
||||
delete {bp_path, name}
|
||||
list {bp_path} — current member variables (name+type) as JSON.
|
||||
"""
|
||||
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 _compile_save(bp):
|
||||
_lib().compile_and_save_blueprint(bp)
|
||||
unreal.EditorAssetLibrary.save_loaded_asset(bp)
|
||||
|
||||
|
||||
def _variables(bp) -> list:
|
||||
"""Return [{name,type,...}] from DescribeBlueprintJson."""
|
||||
try:
|
||||
info = json.loads(_lib().describe_blueprint_json(bp))
|
||||
return info.get("variables", [])
|
||||
except Exception: # noqa: BLE001
|
||||
return []
|
||||
|
||||
|
||||
def _has_var(bp, name: str) -> bool:
|
||||
return any(v.get("name") == name for v in _variables(bp))
|
||||
|
||||
|
||||
def add(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
name = args["name"]
|
||||
type_spec = args.get("type") or args.get("type_spec")
|
||||
if not name or not type_spec:
|
||||
return {"ok": False, "error": "add requires {name, type}"}
|
||||
if _has_var(bp, name):
|
||||
return {"ok": False, "error": f"variable {name!r} already exists", "exists": True}
|
||||
|
||||
ok = bool(_lib().add_member_variable(bp, name, type_spec))
|
||||
if not ok:
|
||||
return {"ok": False, "error": f"add_member_variable failed (bad type {type_spec!r}?)"}
|
||||
|
||||
# Optional default value + metadata in the same transaction.
|
||||
if "default" in args and args["default"] is not None:
|
||||
_lib().set_variable_default_value(bp, name, str(args["default"]))
|
||||
if any(k in args for k in ("instance_editable", "expose_on_spawn", "category", "tooltip")):
|
||||
_lib().set_variable_metadata(
|
||||
bp, name,
|
||||
bool(args.get("instance_editable", False)),
|
||||
bool(args.get("expose_on_spawn", False)),
|
||||
str(args.get("category", "")),
|
||||
str(args.get("tooltip", "")),
|
||||
)
|
||||
_compile_save(bp)
|
||||
return {"ok": True, "name": name, "type": type_spec}
|
||||
|
||||
|
||||
def set_default(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
ok = bool(_lib().set_variable_default_value(bp, args["name"], str(args.get("value", ""))))
|
||||
if ok: _compile_save(bp)
|
||||
return {"ok": ok, "name": args["name"]}
|
||||
|
||||
|
||||
def set_meta(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
ok = bool(_lib().set_variable_metadata(
|
||||
bp, args["name"],
|
||||
bool(args.get("instance_editable", False)),
|
||||
bool(args.get("expose_on_spawn", False)),
|
||||
str(args.get("category", "")),
|
||||
str(args.get("tooltip", "")),
|
||||
))
|
||||
if ok: _compile_save(bp)
|
||||
return {"ok": ok, "name": args["name"]}
|
||||
|
||||
|
||||
def rename(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
ok = bool(_lib().rename_variable(bp, args["old"], args["new"]))
|
||||
if ok: _compile_save(bp)
|
||||
return {"ok": ok, "old": args["old"], "new": args["new"]}
|
||||
|
||||
|
||||
def delete(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
ok = bool(_lib().delete_variable(bp, args["name"]))
|
||||
if ok: _compile_save(bp)
|
||||
return {"ok": ok, "name": args["name"]}
|
||||
|
||||
|
||||
def list_vars(args: dict) -> dict:
|
||||
bp = _load_bp(args["bp_path"])
|
||||
return {"ok": True, "variables": _variables(bp)}
|
||||
|
||||
|
||||
# op-table — also merged into assets.OPS for zero-restart availability.
|
||||
VAR_OPS = {
|
||||
"add_variable": add,
|
||||
"add": add,
|
||||
"set_var_default": set_default,
|
||||
"set_default": set_default,
|
||||
"set_var_meta": set_meta,
|
||||
"set_meta": set_meta,
|
||||
"rename_variable": rename,
|
||||
"rename": rename,
|
||||
"delete_variable": delete,
|
||||
"delete": delete,
|
||||
"list_variables": list_vars,
|
||||
"list": list_vars,
|
||||
}
|
||||
|
||||
|
||||
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}"}, {})
|
||||
op = p.get("op")
|
||||
fn = VAR_OPS.get(op)
|
||||
if not fn:
|
||||
return _tee({"ok": False, "error": f"unknown op {op!r}", "available": sorted(VAR_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 + "last.json", "w", encoding="utf-8") as f:
|
||||
f.write(txt)
|
||||
if rid != "last":
|
||||
with open(sd + f"{rid}.json", "w", encoding="utf-8") as f:
|
||||
f.write(txt)
|
||||
unreal.log("[MCP-VARS] " + txt[:1500])
|
||||
except Exception:
|
||||
pass
|
||||
return txt
|
||||
Reference in New Issue
Block a user