- L_TestFlight: empty, zero-gravity level for Phase 0 flight testing, set as editor/game default map. - Enable UnrealMCPython + Python Editor Script Plugin, third-party MCP plugin (GenOrca/unreal-mcp v2.2.0) giving direct editor control (actors, blueprints, levels, materials, etc.) alongside Epic's built-in Unreal MCP plugin. - Vendor the mcp-server Python source used to bridge to the plugin. - .mcp.json intentionally gitignored (machine-specific absolute paths).
154 lines
4.7 KiB
Python
154 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2025 GenOrca. All Rights Reserved.
|
|
|
|
"""
|
|
Generates the dispatcher action catalog from the plugin's ue_* function signatures.
|
|
|
|
The catalog is the single source of truth that list_actions exposes to the LLM.
|
|
It MUST match the real ue_* signatures exactly, because the dispatcher passes
|
|
params straight through as ue_func(**params). Hand-transcribing signatures is
|
|
error-prone, so we extract them via AST instead.
|
|
|
|
Usage:
|
|
python generate_catalog.py # writes src/unreal_mcp/dispatchers/_catalog.py
|
|
python generate_catalog.py --check # exits 1 if the committed catalog is stale
|
|
|
|
Run with --check in CI / validate_tools to catch drift.
|
|
"""
|
|
|
|
import ast
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
MCP_SERVER_DIR = Path(__file__).parent
|
|
PLUGIN_DIR = MCP_SERVER_DIR.parent / "Plugins" / "UnrealMCPython" / "Content" / "Python" / "UnrealMCPython"
|
|
OUTPUT = MCP_SERVER_DIR / "src" / "unreal_mcp" / "dispatchers" / "_catalog.py"
|
|
|
|
# Domain → plugin action file (module is UnrealMCPython.<file stem>)
|
|
DOMAINS = [
|
|
"actor",
|
|
"anim_blueprint",
|
|
"animation",
|
|
"asset",
|
|
"behavior_tree",
|
|
"blueprint",
|
|
"control_rig",
|
|
"data_table",
|
|
"editor",
|
|
"game",
|
|
"gas",
|
|
"layer",
|
|
"level",
|
|
"level_sequence",
|
|
"material",
|
|
"retarget",
|
|
"static_mesh",
|
|
"texture",
|
|
"umg",
|
|
"util",
|
|
"vision",
|
|
]
|
|
|
|
# Actions handled by the dispatcher but NOT backed by a ue_* function
|
|
# (special TCP message types). Documented manually so list_actions shows them.
|
|
EXTRA_ACTIONS = {
|
|
"util": {
|
|
"execute_python": {
|
|
"params": "code",
|
|
"doc": "Runs arbitrary Unreal Python code. Full API access; fastest path to prototype new actions.",
|
|
},
|
|
"livecoding_compile": {
|
|
"params": "",
|
|
"doc": "Triggers C++ Live Coding and waits for the compile result.",
|
|
},
|
|
}
|
|
}
|
|
|
|
|
|
def _params(fn: ast.FunctionDef) -> str:
|
|
a = fn.args
|
|
defaults = a.defaults
|
|
pad = len(a.args) - len(defaults)
|
|
parts = []
|
|
for i, arg in enumerate(a.args):
|
|
if i >= pad:
|
|
dv = defaults[i - pad]
|
|
try:
|
|
val = ast.literal_eval(dv)
|
|
except Exception:
|
|
val = "..."
|
|
# default None means "required but validated inside" → show name only
|
|
parts.append(arg.arg if val is None else f"{arg.arg}={val!r}")
|
|
else:
|
|
parts.append(arg.arg)
|
|
return ", ".join(parts)
|
|
|
|
|
|
def _doc(fn: ast.FunctionDef) -> str:
|
|
d = ast.get_docstring(fn)
|
|
return d.splitlines()[0].strip() if d else ""
|
|
|
|
|
|
def _extract(domain: str) -> dict:
|
|
f = PLUGIN_DIR / f"{domain}_actions.py"
|
|
actions: dict[str, dict] = {}
|
|
if f.exists():
|
|
tree = ast.parse(f.read_text(encoding="utf-8"))
|
|
for n in tree.body:
|
|
if isinstance(n, ast.FunctionDef) and n.name.startswith("ue_"):
|
|
action = n.name[3:] # strip ue_
|
|
actions[action] = {"params": _params(n), "doc": _doc(n)}
|
|
actions.update(EXTRA_ACTIONS.get(domain, {}))
|
|
return dict(sorted(actions.items()))
|
|
|
|
|
|
def build() -> dict:
|
|
return {d: _extract(d) for d in DOMAINS}
|
|
|
|
|
|
def render(catalog: dict) -> str:
|
|
lines = [
|
|
"# Copyright (c) 2025 GenOrca. All Rights Reserved.",
|
|
"#",
|
|
"# AUTO-GENERATED by generate_catalog.py — do not edit by hand.",
|
|
"# Regenerate: python generate_catalog.py",
|
|
"",
|
|
"CATALOG = {",
|
|
]
|
|
for domain, actions in catalog.items():
|
|
lines.append(f" {domain!r}: {{")
|
|
for action, info in actions.items():
|
|
lines.append(f" {action!r}: {{")
|
|
lines.append(f" {'params'!r}: {info['params']!r},")
|
|
lines.append(f" {'doc'!r}: {info['doc']!r},")
|
|
lines.append(" },")
|
|
lines.append(" },")
|
|
lines.append("}")
|
|
lines.append("")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
catalog = build()
|
|
rendered = render(catalog)
|
|
|
|
if "--check" in sys.argv:
|
|
if not OUTPUT.exists():
|
|
print("FAIL: _catalog.py does not exist. Run generate_catalog.py.")
|
|
sys.exit(1)
|
|
current = OUTPUT.read_text(encoding="utf-8")
|
|
if current.strip() != rendered.strip():
|
|
print("FAIL: _catalog.py is stale. Run: python generate_catalog.py")
|
|
sys.exit(1)
|
|
total = sum(len(a) for a in catalog.values())
|
|
print(f"OK: catalog in sync ({total} actions across {len(catalog)} domains).")
|
|
sys.exit(0)
|
|
|
|
OUTPUT.write_text(rendered, encoding="utf-8")
|
|
total = sum(len(a) for a in catalog.values())
|
|
print(f"Wrote {OUTPUT} ({total} actions across {len(catalog)} domains).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|