Metadata-Version: 2.4
Name: uefn-mcp
Version: 0.1.6
Summary: MCP server for Unreal Editor for Fortnite (UEFN) — lets Claude / Cursor drive UEFN via Python
Project-URL: Homepage, https://github.com/missatjuhvdk1/uefn-mcp
Project-URL: Repository, https://github.com/missatjuhvdk1/uefn-mcp
Project-URL: Issues, https://github.com/missatjuhvdk1/uefn-mcp/issues
Project-URL: Changelog, https://github.com/missatjuhvdk1/uefn-mcp/blob/main/README.md
Author: meesv
License-Expression: MIT
License-File: LICENSE
Keywords: ai,claude,cursor,fortnite,mcp,model-context-protocol,uefn,unreal,verse
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Games/Entertainment
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp[cli]>=1.2.0
Description-Content-Type: text/markdown

# uefn-mcp

> MCP server for **Unreal Editor for Fortnite** — let Claude / Cursor drive UEFN
> through Python, the same way `rbxstudio-mcp` drives Roblox Studio.

**Now at v1.6** — Verse digest tooling + a human-in-the-loop coordination
primitive. The LLM can now (a) ask "what devices exist in Fortnite?" and get a
parsed answer from `Fortnite.digest.verse` without dumping 50K lines into
context, and (b) **block on `wait_for_events(...)` until the user does
something** — press Ctrl+Shift+B to build, click an actor in the viewport —
instead of asking and re-asking. Transport is HTTP long-poll, not literal
WebSockets (UEFN's embedded Python is painful to pip-install into; the UX is
identical for the events that actually fire). 47 tools total (was 43). See
§`v1.6 highlights` below.

> **Honest receipt on `wait_for_events`:** today the events that fire are
> `log.error` / `log.warning` (mostly echoes of the LLM's own `execute_python`
> errors), `verse.file_written` (circular — the LLM emits it itself), and the
> two genuinely useful ones: `verse.build` (user pressed Ctrl+Shift+B) and
> `selection.changed` (user clicked an actor). There are **no gameplay hooks**
> wired in — no `player.spawned` or `device.triggered` from PIE — and the MCP
> tool surface can't start a play session anyway. Treat this as "wait for the
> human at the keyboard to do X", not as autonomous reactivity.

**v1.5 (previous)** — audit fixup release. v1.4 shipped MVVM notes that contained
a handful of errors (phantom `set_*_conversion_function` methods, wrong type
on `override_execution_mode`, an undercount of `MVVMBlueprintView` properties,
two fabricated quotes). v1.5 rewrites `MVVM_API_NOTES.md` from the actual
Python 5.7 docs, fixes a real runtime bug where the conversion-function
parameter on `add_view_binding` silently no-op'd, and acknowledges that the
entire conversion-function surface is **Read-Only** in 5.7 Python (set it in
the editor UI). See §`v1.5 audit fixup` below.

---

## Architecture

```
┌──────────────┐   stdio MCP    ┌──────────────┐   HTTP   ┌────────────────────┐
│ Claude/Cursor│ ◄────────────► │  uefn-mcp    │ ◄──────► │ UEFN listener      │
│              │                │ (this server)│  :8766   │ (embedded Python)  │
└──────────────┘                └──────────────┘          └────────────────────┘
                                                                   │
                                                                   ▼
                                                           unreal.* on game thread
```

- The **MCP server** runs outside UEFN (talks stdio to your IDE).
- The **listener** lives inside UEFN's embedded Python 3.11 interpreter and
  serves an HTTP API on `127.0.0.1:8766`. Every command is parked on a queue
  and executed on the game thread via `register_slate_post_tick_callback` —
  because all `unreal.*` calls have to happen there.
- Mutating tools wrap their snippet in `unreal.ScopedEditorTransaction` so
  each tool call is a single Ctrl+Z step.

---

## Install

### 1. Install the UEFN plugin

Copy the **two files** in `uefn_plugin/` into your UEFN project:

```
[YourUEFNProject]/Content/Python/init_unreal.py
[YourUEFNProject]/Content/Python/uefn_listener.py
```

If the `Python/` folder doesn't exist, create it. UEFN's embedded interpreter
auto-runs `init_unreal.py` on project open.

Make sure UEFN's Python plugin is enabled (Edit ▸ Plugins ▸ search "Python").
For MVVM tools you also need the **Model View Viewmodel** plugin enabled
(Edit ▸ Plugins ▸ search "MVVM").

Open your project. You should see in the Output Log:

```
[uefn-mcp] ✅ Listener started on http://127.0.0.1:8766
```

Sanity check from a terminal:

```bash
curl http://127.0.0.1:8766/health
# {"ok": true, "service": "uefn-mcp-listener", "queue_depth": 0, ...}
```

### 2. Install the MCP server

From inside this repo:

```bash
pip install -e .
# or, if you're a uv person:
uv pip install -e .
```

That gives you a `uefn-mcp` console script.

### 3. Wire it into your IDE

**Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json` (mac) or `%APPDATA%\Claude\claude_desktop_config.json` (windows):

```json
{
  "mcpServers": {
    "uefn": {
      "command": "uefn-mcp"
    }
  }
}
```

**Cursor** — Settings ▸ MCP ▸ Add new MCP Server:

```json
{
  "uefn": { "command": "uefn-mcp" }
}
```

Restart your IDE. You should see `uefn` show up as a connected MCP server.

---

## Project layout (v1.5)

```
src/uefn_mcp/
├── server.py        ~70 lines: entry point — imports tools/ and runs FastMCP
├── _bridge.py       HTTP transport + transaction wrapper + shared mcp instance
├── _snippets.py     reusable Python text snippets (ACTOR/MVVM/FRAME/VERSE/WIDGET)
└── tools/
    ├── system.py        ping, execute_python, get_output_log
    ├── actors.py        query, property access, mutation
    ├── navigation.py    select_actors, focus_viewport_on
    ├── discovery.py     class distribution, tags, device catalog
    ├── level.py         level info / save / undo / redo
    ├── verse.py         list / read / write / restore Verse files
    ├── assets.py        list_assets
    ├── screenshots.py   viewport / actor screenshots
    ├── widgets.py       WBP create / duplicate / compile / inspect
    └── mvvm.py          view bindings, viewmodel contexts, conversions
```

`MVVM_API_NOTES.md` documents the verified Read-Only / Read-Write surface
map per UE Python 5.7 docs — read it before touching `tools/mvvm.py` or
`_snippets.MVVM_HELPERS`.

---

## Tools (v1.6 — 47 total)

### System

| Tool             | What it does                                |
| ---------------- | ------------------------------------------- |
| `ping`           | Health check on the listener                |
| `execute_python` | Run arbitrary Python on the game thread     |
| `get_output_log` | Read recent UEFN log messages               |

### Actor — query

| Tool                    | What it does                                                |
| ----------------------- | ----------------------------------------------------------- |
| `get_all_actors`        | List actors in the level (summary view, paginated)          |
| `get_selected_actors`   | Actors selected in the viewport                             |
| `get_actor_details`     | One actor with `summary`/`detailed`/`full` verbosity        |
| `find_actors`           | Filter by name regex, class, or tag                         |
| `get_actor_components`  | List components on an actor                                 |
| `get_actor_property`    | Read any editor property (supports nested paths)            |
| `get_class_distribution`| Count actors per class                                      |
| `get_all_tags`          | List every tag in use across the level                      |
| `get_device_catalog`    | List runtime UE class names (e.g. `TriggerDevice_C`) — for spawning into the level via Python. Distinct from v1.6's `list_verse_devices`, which returns Verse-language names for writing `.verse` code |

### Actor — mutate (transaction-wrapped)

| Tool                  | What it does                                                  |
| --------------------- | ------------------------------------------------------------- |
| `set_actor_property`  | Write an editor property (with type coercion)                 |
| `spawn_actor`         | Spawn an actor from an asset path                             |
| `delete_actor`        | Delete an actor by label                                      |
| `set_actor_transform` | Move/rotate/scale an actor                                    |
| `select_actors`       | Select a list of actors                                       |
| `focus_viewport_on`   | Frame the viewport on an actor (F-key equivalent)             |

### Level

| Tool                  | What it does                            |
| --------------------- | --------------------------------------- |
| `get_level_info`      | World name + actor count                |
| `save_current_level`  | Save the open level                     |
| `undo`                | Undo N steps                            |
| `redo`                | Redo N steps                            |

### Verse

| Tool                    | What it does                                                       |
| ----------------------- | ------------------------------------------------------------------ |
| `list_verse_modules`    | List `.verse` files (scans `VerseDevices`, `Verse`, plugin Verse)  |
| `read_verse_file`       | Read a Verse file                                                  |
| `write_verse_file`      | Write a Verse file (auto-snapshots prior content)                  |
| `list_verse_snapshots`  | List recent snapshots of a Verse file                              |
| `restore_verse_file`    | Restore from a snapshot (still hit `Ctrl+Shift+B` to build)        |

### Assets

| Tool          | What it does                                  |
| ------------- | --------------------------------------------- |
| `list_assets` | List asset paths under a directory            |

### Screenshots

| Tool                  | What it does                                                            |
| --------------------- | ----------------------------------------------------------------------- |
| `screenshot_viewport` | Take a viewport screenshot (returns inline MCP image — Claude can SEE it) |
| `screenshot_actor`    | Frame an actor first, then screenshot it                                |

### UMG / Widget Blueprint

| Tool                       | What it does                                                  |
| -------------------------- | ------------------------------------------------------------- |
| `create_widget_blueprint`  | Create a new WBP at a path                                    |
| `duplicate_widget_blueprint` | Duplicate a WBP                                             |
| `compile_widget_blueprint` | Compile + save a WBP                                          |
| `inspect_widget`           | Read class, slots, viewmodels, bindings count                 |
| `get_widget_tree`          | 🆕 Read-only widget hierarchy (5.7 Python doesn't expose authoring; design in the UMG Designer) |

### MVVM (v1.5 — re-verified against UE Python 5.7 docs)

| Tool                          | What it does                                                                   |
| ----------------------------- | ------------------------------------------------------------------------------ |
| `inspect_viewmodel`           | List a ViewModel's bindable fields and types                                   |
| `list_view_bindings`          | Existing MVVM bindings on a WBP                                                |
| `list_available_conversions`  | 🆕 Conversion functions valid for a specific source→dest pair                  |
| `add_viewmodel_context`       | Add a viewmodel context to a WBP                                               |
| `add_view_binding`            | Bind viewmodel field → widget property (with optional conversion + mode)       |
| `remove_view_binding`         | Remove a binding by index                                                      |
| `remove_viewmodel_context`    | Remove a viewmodel context                                                     |

### Verse digests (v1.6 — new)

The Verse `*.digest.verse` files are the canonical API surface for
Fortnite devices, UnrealEngine types, and core Verse types. These tools
parse them server-side so the LLM gets targeted answers instead of 50K-line
file dumps.

| Tool                       | What it does                                                                   |
| -------------------------- | ------------------------------------------------------------------------------ |
| `list_verse_digests`       | 🆕 Find every `*.digest.verse` file in the project (Fortnite, UnrealEngine, Verse, …) |
| `list_verse_devices`       | 🆕 Top-level classes from a digest, optional name-substring filter             |
| `get_verse_device_api`     | 🆕 Full parsed API of one class — header, methods, events, fields              |
| `search_verse_digest`      | 🆕 Regex grep across digests — find anything by line                           |

Typical workflow: *"Create a system that uses a trigger and a prop_mover"* →
`list_verse_devices(filter_substring="trigger")` → `get_verse_device_api("trigger_device")`
→ `get_verse_device_api("prop_mover_device")` → draft Verse → `write_verse_file`.

### Reactive events — `wait_for_events` (v1.6 — new)

The listener gained an event buffer + HTTP long-poll endpoint. The LLM can
**block on `wait_for_events`** instead of asking the user "did the build
finish yet?" in a loop.

| Tool              | What it does                                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `wait_for_events` | 🆕 Block up to 60s until matching events arrive. `timeout_s=0` peeks the buffer without blocking (`max_count` caps the slice). |

Single tool, two modes — peek (timeout 0) was previously a separate
`get_recent_events`; that was just `wait_for_events(timeout_s=0)` under the
hood, so it folded in.

Built-in event kinds: `log.error`, `log.warning`, `verse.build` (heuristic),
`selection.changed`, `verse.file_written`. Custom kinds can be emitted from
any snippet via the pre-injected `push_event(kind, data)` callable
(e.g. `execute_python("push_event('build.done', {'ok': True})")`).
The listener also exposes raw `POST /events/push` for external clients.

**Realistic use cases** (be honest about what fires):

```python
# 1. You wrote a Verse file, ask the user to build, wait for the result
write_verse_file("MyDevice.verse", code)
# tell the user: "Press Ctrl+Shift+B"
out = wait_for_events(kinds=["verse.build"], timeout_s=60)
if out["events"]:
    status = out["events"][0]["data"]["status"]   # "ok" or "fail"

# 2. Coordinate with the user via viewport selection
# tell the user: "Click the wall you want me to align to"
out = wait_for_events(kinds=["selection.changed"], timeout_s=120)
```

What it's **not** good for: autonomous LLM-only reactivity. `log.error` and
`log.warning` mostly echo what `execute_python` already returns synchronously.
There are no game-event hooks (no `player.spawned`, no `device.triggered`),
and the MCP surface can't start a play session anyway. If gameplay hooks
ever land, the primitive is ready for them.

Why long-poll instead of WebSockets? UEFN's embedded Python is painful to
pip-install into, and stdlib has no WS support. Long-poll gives the same
"wake immediately when events arrive" UX with zero new deps. The bridge's
HTTP timeout for these calls is `server_timeout + 10s` (default 35s total).

`execute_python` is the escape hatch — anything not exposed as a tool can still
be done with raw Python. Pre-injected names: `unreal`, `actor_sub`, `asset_sub`,
`level_sub`, `editor_sub`, `mvvm_sub`, `push_event`, `result`. Assign to `result`
to return a value. (`mvvm_sub` is `None` if the MVVM plugin isn't enabled.)

Example prompt: *"Spawn a cube at (100, 0, 50), name it Hello, screenshot it,
then read it back."*

---

## A note on MVVM

UEFN's Python surface for MVVM (`MVVMEditorSubsystem`, `MVVMBlueprintView`,
`MVVMBlueprintPropertyPath`) varies a bit between UE 5.5 / 5.6 / 5.7. The
v1.5 implementation has been re-verified against the canonical UE Python 5.7
docs after a second-pass audit caught errors in the v1.4 notes (see
`MVVM_API_NOTES.md` §9 "Audit fixup" for the full diff).

Verified facts baked into the code:

- ✅ `unreal.get_editor_subsystem(unreal.MVVMEditorSubsystem)` is the right
  accessor (the runtime `MVVMSubsystem` CDO has none of the editor verbs).
- ✅ `MVVMBlueprintView.bindings` and `available_view_models` are Read-Write
  arrays — accessed via `view.get_editor_property(...)`. (The full property
  set is 5: also `compiled_binding_library_id`, `conditions`, `events`.)
- ✅ `MVVMBindingMode` enum: `ONE_TIME_TO_DESTINATION=0`,
  `ONE_WAY_TO_DESTINATION=1`, `TWO_WAY=2`, `ONE_WAY_TO_SOURCE=4` — note the
  gap at value 3.
- ✅ `MVVMEditorSubsystem.remove_view_model` takes `Name`, not `Guid`.
- ✅ `MVVMBlueprintViewBinding.override_execution_mode` is an
  `MVVMExecutionMode` enum (not `bool`).
- ⚠️ ALL `MVVMBlueprintPropertyPath` properties are Read-Only per docs, and
  there are no documented `set_widget_name` / `set_view_model_id` /
  `set_property_path` UFUNCTIONs. The path setters in `_snippets.MVVM_HELPERS`
  use a multi-strategy fallback (typed UFUNCTION → `set_editor_property` →
  direct `paths` array writes via `MVVMBlueprintFieldPath`) and stamp the
  `source` enum (`WIDGET` / `VIEW_MODEL`) for editor-UI sync.
- 🚧 The **conversion-function surface is fully Read-Only** in 5.7 Python:
  `MVVMBlueprintViewBinding.conversion`, both directions on
  `MVVMBlueprintViewConversionPath`, and all 6 fields on
  `MVVMBlueprintViewConversionFunction`. There are no documented
  `set_*_conversion_function` methods on `MVVMEditorSubsystem` (v1.4 claimed
  there were — they're phantom). v1.5 attempts a best-effort in-place
  mutation when `add_view_binding` is called with `conversion_function`, but
  it returns a clear note when the engine refuses, in which case **set the
  conversion in the editor UI** (Widget Designer → ViewModels panel → click
  the binding → choose conversion).

If a binding tool fails, the fast diagnostic is one `execute_python` call —
e.g. `execute_python("result = sorted(dir(unreal.MVVMEditorSubsystem))")` —
to see exactly which methods exist on `MVVMEditorSubsystem` /
`MVVMBlueprintView` / `MVVMBlueprintPropertyPath` /
`MVVMBlueprintViewBinding` in your specific install. (v1.4–v1.5 shipped a
dedicated `probe_mvvm_api` tool for this; v1.6 dropped it — the bundled
`dir()` walk is one line via `execute_python`, and `MVVM_API_NOTES.md` is
the documented baseline.)

The **widget tree itself** stays in your hands. There is no UEFN-specific
"lock" — what's actually happening is **lock by omission** in UE 5.7's Python
bindings:

- `WidgetTree` (5.7 docs page): stub. `Bases: Object`, zero methods, zero
  properties. No `construct_widget`, no `root_widget`.
- `BaseWidgetBlueprint` / `UserWidgetBlueprint` / `WidgetBlueprint`: all stubs.
  Only blueprint metadata (`compile_mode`, `blueprint_category`, etc.). **No
  `widget_tree` editor property accessor anywhere in the chain.**
- C++ `UWidgetTree::ConstructWidget<T>` is a templated method, which Unreal's
  reflection system doesn't bind to Python (UnrealHeaderTool can't UFUNCTION-
  expose templates).
- `WidgetLibrary` / `WidgetLayoutLibrary` / `UserWidgetFunctionLibrary` /
  `WidgetReferenceBlueprintFunctionLibrary` /
  `WidgetAnimationHandleFunctionLibrary`: all runtime-mutation helpers. Zero
  authoring methods across all five.
- `PanelWidget.add_child()` / `clear_children()` / `remove_child()` etc. **are**
  exposed — but only for **runtime mutation** of already-instantiated widgets
  at play time. They don't help author the persisted WBP at editor time.

This matches Epic's design intent: author UMG in the Designer →
attach bindings/conditions/events via `MVVMEditorSubsystem` Python verbs →
consume at runtime from Verse. Editor-time UMG hierarchy authoring routes
through the Designer; runtime widget creation in cooked builds routes through
Verse, not Python. (Conversion functions are also Designer-set, see above.)

This MCP automates everything *around* the widget tree — viewmodels, bindings,
compilation, save, screenshots, MVVM conversions where the engine permits.

Per Epic's official UEFN docs, MCP is a sanctioned use case:
> "Remote execution of well made scripts or tools, such as a MCP integration
> with an Agentic AI"
> — _Python Tools in UEFN_, dev.epicgames.com (line 18 of the scrape)

---

## v1.5 audit fixup

A second-pass audit against the firecrawl scrape of `dev.epicgames.com/python-api`
caught the following errors in v1.4. v1.5 fixes them all:

| Issue (v1.4)                                                                        | Fix (v1.5)                                                                                       |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `tools/mvvm.py` called phantom `sub.set_source_to_destination_conversion_function()` | Removed phantom calls; replaced with best-effort in-place mutation on `binding.conversion`       |
| `MVVM_API_NOTES.md` claimed `MVVMEditorSubsystem` has ~9 methods                     | Documented all 21 (added `add_condition`, `add_event`, `rename_view_model`, `reparent_view_model`, etc.) |
| `MVVMBlueprintView` listed as 2 properties                                          | Now lists all 5 (`compiled_binding_library_id`, `conditions`, `events` were missing)             |
| `override_execution_mode` typed as `bool`                                           | Corrected to `MVVMExecutionMode` enum                                                            |
| Restrictions section quoted `uefn-vs-ue-in-unreal-editor-for-fortnite.md`           | Quote doesn't exist in source — section now only cites `python-tools-in-uefn.md` with line nums  |
| `MVVMBindingMode` listed without flagging the value-3 gap                            | Now explicitly noted: values are `[0, 1, 2, 4]`                                                  |

No new tools added — the audit's "missing methods" list (`add_condition`,
`add_event`, `rename_view_model`, `get_child_view_models`, etc.) was
deliberately left out of the tool surface to avoid count inflation. Use
`execute_python` with the pre-injected `mvvm_sub` to call them directly.

---

## v1.6 highlights

Two big features. Both shipped together because they're complementary —
digest tooling tells the LLM **what** to build, reactive events tell it
**when** something happened.

### 1. Verse digest tooling — 4 new tools (`tools/verse_digest.py`)

The problem v1.5 left open: the LLM's "what devices exist?" mental model.
Without digest awareness, the LLM falls back to its training data, which is
stale and incomplete for UEFN Verse. With it, you get:

```
list_verse_devices(filter_substring="trigger")
  → [{"name": "trigger_device", ...}, {"name": "billboard_trigger_device", ...}]

get_verse_device_api("trigger_device")
  → {
      "header": "trigger_device := class<concrete>(creative_device_base):",
      "members": [
        {"kind": "event",  "name": "Triggered", "signature": "..."},
        {"kind": "method", "name": "Trigger",   "signature": "..."},
        {"kind": "method", "name": "Reset",     "signature": "..."},
        ...
      ]
    }
```

Parsing happens **server-side** in the listener — the bridge never receives
the 50K-line digest, only the targeted slice. `search_verse_digest` adds a
regex grep across all digests in case the structured tools miss something.

The fix to `_verse_search_roots` is small but important: digest files live
at `Plugins/<X>/Content/<X>.digest.verse`, NOT in `Plugins/<X>/Content/Verse/`.
Pre-v1.6 `list_verse_modules` missed them entirely.

### 2. Human-in-the-loop coordination — `wait_for_events` (1 new tool)

The roadmap entry said "WebSocket transport for reactive workflows". The
ship reality: **HTTP long-poll** with the same UX. The transport tradeoff:

| Aspect                | True WebSockets                        | HTTP long-poll (shipped)             |
| --------------------- | -------------------------------------- | ------------------------------------ |
| Wake latency          | < 10 ms                                | < 10 ms (cond var notify)            |
| Listener deps         | Needs `websockets` lib in UEFN Python  | **Stdlib only**                      |
| Code size             | ~400 LoC (server + client + framing)   | ~150 LoC                             |
| Persistent connection | Yes                                    | No (one connection per `wait` call)  |
| LLM-facing API        | Identical                              | Identical                            |

UEFN's embedded Python is painful to pip-install into. Until that changes,
long-poll is the right call. The `True WebSocket transport` roadmap entry
is still there if/when a use case actually needs the persistent connection
(e.g. a separate web dashboard mirroring editor state in real time).

**What this tool actually is** (post-audit, fixed up from the v1.6 launch
narrative): a **human-in-the-loop coordination primitive**, not autonomous
reactivity. The events that fire today are mostly user-driven (`verse.build`
on Ctrl+Shift+B, `selection.changed` on click) or self-echoing
(`log.error` / `log.warning` from the LLM's own `execute_python` errors,
`verse.file_written` from its own writes). There are no gameplay-event
hooks; the MCP tool surface can't start a play session anyway. So the
realistic value is "block until the user does the thing", not "react to
the game".

`get_recent_events` was originally shipped as a separate peek tool; it was
literally `wait_for_events(timeout_s=0)` under the hood. Folded back in.
Set `timeout_s=0` to peek; `max_count` caps the slice (default 100, server
buffer holds 500).

What you can do today:

```
# Wait for the user to press Ctrl+Shift+B after you wrote Verse code
out = wait_for_events(kinds=["verse.build"], timeout_s=60)
if out["events"]:
    status = out["events"][0]["data"]["status"]   # "ok" or "fail"

# Coordinate with the user via viewport selection
out = wait_for_events(kinds=["selection.changed"], timeout_s=120)

# Peek for recent errors without blocking
out = wait_for_events(kinds=["log.error"], timeout_s=0, max_count=20)

# Cursor across multiple polls
out = wait_for_events(since_id=out["last_id"], timeout_s=25)
```

Built-in event sources:

- `log.error` / `log.warning` — anything passed to `unreal.log_error/_warning`
- `verse.build` — heuristic: matches `"Verse compilation succeeded"` /
  `"...failed"` patterns in `LogVerse`. Tweak `_maybe_emit_verse_build_event`
  in `uefn_listener.py` if your UEFN version uses different strings.
- `selection.changed` — actor selection in viewport (polled in the tick
  callback because UEFN doesn't expose a Python selection delegate)
- `verse.file_written` — emitted by `write_verse_file` after a successful write

Custom kinds: any tool can call `push_event(kind, data)` (pre-injected) to
emit its own events.

---

## Limitations

- **Verse builds are manual.** UEFN doesn't expose a programmatic build trigger.
  After `write_verse_file`, you (or the user) hit `Ctrl+Shift+B` in UEFN.
  *Mitigated in v1.6:* `wait_for_events(kinds=["verse.build"])` blocks until
  the build finishes so the LLM can react without spin-polling.
- **Game thread only** (for `unreal.*` calls). Long-running snippets block the
  editor for up to 30s (the listener timeout). The new `/events/wait` endpoint
  runs in the HTTP worker thread, not the game thread — long-polling for
  events does **not** block the editor.
- **`verse.build` event detection is heuristic.** The listener pattern-matches
  "Verse compilation succeeded/failed" strings out of `LogVerse`. If a future
  UEFN version changes the wording, `_maybe_emit_verse_build_event` in
  `uefn_listener.py` needs a one-line tweak. (Polling `LogVerse` text is the
  only path; UEFN doesn't expose a build delegate to Python.)
- **Local only.** Listener binds to `127.0.0.1`. No auth — don't expose port 8766.
- **Widget-tree authoring is not exposed** in 5.7 Python (lock-by-omission, not a UEFN-specific policy). Design the visual layout in the UMG Designer. Bindings/conditions/events around the tree are scriptable. See "A note on MVVM" above for the full receipts.
- **MVVM plugin required** for the MVVM tool family. The listener still starts
  fine without it; just `mvvm_sub` will be `None` and the MVVM tools will tell
  you to enable the plugin.

---

## Documentation provenance

Two pools of source material drive the implementation, kept out of this repo
(in a sibling chat workspace) but referenced when building each version:

- **Verse API docs** (`Fortnite.com`, `UnrealEngine.com`, `Verse.org` modules,
  scraped from `dev.epicgames.com`) — drive the Verse tool family
  (`list_verse_modules`, `read_verse_file`, etc.) and inform device-catalog
  / Verse-template work on the roadmap.
- **UE Python 5.7 API docs** (`unreal.MVVMEditorSubsystem`,
  `unreal.MVVMBlueprintView`, `unreal.MVVMBlueprintPropertyPath`,
  `unreal.MVVMBlueprintViewBinding`, `unreal.MVVMBlueprintViewConversionPath`,
  `unreal.MVVMBlueprintViewConversionFunction`, `unreal.MVVMBlueprintFieldPath`,
  `unreal.MVVMBindingMode`, `unreal.MVVMExecutionMode`,
  `unreal.MVVMBlueprintFieldPathSource`,
  `unreal.MVVMBlueprintViewModelContextCreationType`, plus the **Python
  Tools in UEFN** guide that explicitly endorses MCP) — drive the v1.5 MVVM
  verification. The full Read-Only / Read-Write surface map plus enum values
  is captured in `MVVM_API_NOTES.md` (re-verified line-by-line in v1.5 after
  a second-pass audit).

If a binding tool ever fails on a UEFN install whose MVVM plugin diverges
from those docs, the diagnostic is one `execute_python` call — e.g.
`execute_python("result = sorted(dir(unreal.MVVMEditorSubsystem))")` —
to dump the actual API surface in your install for diff'ing against the
notes. (v1.4–v1.5 wrapped this in a `probe_mvvm_api` tool; v1.6 dropped
the wrapper because it was a packaged `dir()` walk competing with
`execute_python` for the LLM's routing slot.)

---

## Roadmap

See `UEFN_MCP_BUILD_PLAN.md` for the full vision. Short list:

- [x] `ScopedEditorTransaction` undo wrapping
- [x] Device catalog scan
- [x] Screenshots (viewport + framed actor)
- [x] MVVM tool family (v1.3 actually works)
- [x] Modular refactor — split the 2k-line `server.py` into focused modules (v1.4)
- [x] MVVM verification against canonical UE Python 5.7 docs (v1.4)
- [x] MVVM audit fixup — corrected docs + fixed phantom-method bug in conversion path (v1.5)
- [x] Visual widget authoring — **investigated, closed**: not feasible at design time in 5.7 Python. `BaseWidgetBlueprint`, `UserWidgetBlueprint`, `WidgetBlueprint`, `WidgetTree`, `UserWidgetExtension` all docs-stubbed by Epic (only metadata exposed, no `widget_tree` accessor, no `construct_widget`). `PanelWidget.add_child()` etc. are exposed but **runtime-only** — once instantiated. UMG hierarchy authoring routes through the Designer; runtime widget creation in cooked builds routes through Verse. See "A note on MVVM" §widget-tree below.
- [x] **Verse digest tooling (v1.6)** — `list_verse_digests` / `list_verse_devices` / `get_verse_device_api` / `search_verse_digest`. The LLM can now answer "what devices exist?" / "what's the API of trigger_device?" without dumping 50K-line digests into context.
- [x] **HITL coordination primitive (v1.6)** — `wait_for_events` blocks up to 60s for editor events (`verse.build` on Ctrl+Shift+B, `selection.changed` on click, plus `log.error` / `log.warning` / `verse.file_written` for completeness). Transport is HTTP long-poll, not WebSockets — UEFN's embedded Python has no stdlib WS support; long-poll gives identical UX. Honest framing: the events that fire today are user-driven or self-echoing, not gameplay-reactive (no PIE hooks shipped).
- [ ] Semantic actor/asset search — embed-based "find the boss spawner" lookup that beats regex-on-name (`find_actors` today is literal-pattern only)
- [ ] World-state JSON export (Toolbelt-style)
- [ ] Verse templates / scaffolding (next logical step after digest tooling — pre-canned device wirings)
- [ ] MCP resources & prompts (not just tools)
- [ ] `uvx uefn-mcp` distribution via PyPI
- [ ] **True WebSocket transport** (rather than long-poll) — would require either a vendored `websockets` lib in the listener or a hand-rolled stdlib WS implementation (~250 LoC for handshake + framing). Punted because long-poll already delivers the reactive UX; revisit if/when a use case actually needs persistent push (e.g. mirroring editor state to a web dashboard).

---

## Credits

- Inspired by [kirchuvakov/uefn-mcp-server](https://github.com/kirchuvakov/uefn-mcp-server)
  and [UEFN Toolbelt](https://github.com/spillage/uefn-toolbelt).
- Architecture mirrors my own [rbxstudio-mcp](https://github.com/meesv/rbxstdiomcp).
- License: MIT.
