@uemcp/server 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +75 -0
  2. package/dist/index.mjs +1630 -269
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -32,6 +32,18 @@ The MCP tool surface currently exposes:
32
32
  - `set_config_value`
33
33
  - `list_config_section`
34
34
  - `run_clang_tidy`
35
+ - `list_actors`
36
+ - `get_actor_details`
37
+ - `get_current_level`
38
+ - `spawn_actor`
39
+ - `destroy_actor`
40
+ - `set_actor_transform`
41
+ - `set_actor_property`
42
+ - `select_actors`
43
+ - `undo`
44
+ - `redo`
45
+ - `open_level`
46
+ - `get_open_level_status`
35
47
 
36
48
  ## Project Identity
37
49
 
@@ -137,6 +149,69 @@ This is intended to let an MCP client resolve the canonical Unreal project ident
137
149
  - Spawns `clang-tidy` (server-configured via `CLANG_TIDY_PATH`, else `PATH`) over the translation units and returns structured findings (file, line, column, check, severity, message)
138
150
  - Report only — does not apply fixes
139
151
 
152
+ ### `list_actors` / `get_actor_details` / `get_current_level`
153
+
154
+ - The read half of the level surface. All three answer about actors instantiated in the **editor world** — not the PIE world, and not assets on disk (`find_assets` remains the tool for those)
155
+ - Every actor is identified by `actorPath` — `AActor::GetPathName()`, e.g. `/Game/Maps/Test.Test:PersistentLevel.StaticMeshActor_3` — carried alongside the display `label`. The path is the only stable, unique, resolvable identifier; labels are neither unique nor resolvable, so never address an actor by one. The same `actorPath` round-trips through `get_editor_state`, `list_actors`, and `select_actors`
156
+ - `list_actors` filters by class and by name substring, and pages with `nextCursor` / `totalCount` / `truncated`. Pages are trimmed by encoded bytes as well as by entry count, because actor paths vary wildly in length — `totalCount` stays authoritative when a page truncates
157
+ - `get_actor_details` reports the actor's class, transform, folder, tags, components, and reflected properties, paging components and properties independently. Property values are `ExportText`, the same format `get_cdo_defaults` emits and `set_actor_property` accepts
158
+ - `get_current_level` reports the loaded map's package name plus its streaming levels and World Partition state
159
+
160
+ ### `spawn_actor` / `destroy_actor` / `set_actor_transform` / `set_actor_property` / `select_actors`
161
+
162
+ - The mutating half. **Every one of these runs inside a single `FScopedTransaction` on the game thread, so one Ctrl+Z in the editor reverses the whole call** — a batch of 64 spawns undoes as one step, not 64. The response reports the `transactionTitle` the batch collapsed into
163
+ - All five take **arrays** and return per-item results (`{ index, ok, actorPath?, error? }`) with `successCount` / `failureCount`. One bad entry fails its own item and neither aborts the batch nor rolls back the transaction. The batch bound is 64 (`BRIDGE_LEVEL_EDIT_MAX_BATCH`), sized so the request also stays under the 64 KiB payload cap
164
+ - `dryRun: true` validates everything the real call validates — class resolves, actor exists, property path resolves and the value coerces — and reports what *would* change with `state: 'validated'`, opening no transaction and touching no world state
165
+ - Mutations `Modify()` the objects they touch and dirty their packages, so `save_dirty_packages` subsequently reports and persists them
166
+ - `set_actor_transform` treats each of `location` / `rotation` / `scale` as optional: an omitted component keeps its current value, so a move need not restate the rest. An entry naming none of the three is rejected rather than silently doing nothing
167
+ - `set_actor_property` addresses a property on the actor (`bHidden`) or on one of its components (`StaticMeshComponent.RelativeScale3D`), and writes via `ImportText_InContainer` — the exact inverse of the reader, so a value read from `get_actor_details` or `get_cdo_defaults` can be edited and written back with no format translation. `EditConst` and non-editable properties are rejected with an explicit error rather than silently no-op'ing
168
+ - `select_actors` sets the editor selection; an empty `actorPaths` clears it, and `addToSelection` adds rather than replaces. It reports the resulting `selectedActorCount`
169
+ - In a non-editor plugin build every one of these returns `state: 'unsupported'` rather than failing
170
+
171
+ ### `undo` / `redo`
172
+
173
+ - Act on the editor's own transaction buffer — the same buffer Ctrl+Z and Ctrl+Y drive — and report the `transactionTitle` they acted on, or `state: 'nothing_to_undo'` / `'nothing_to_redo'`
174
+ - The head of that buffer may be a **human's** edit rather than the agent's, so check the returned `transactionTitle` before assuming what was reversed
175
+
176
+ ### `open_level` / `get_open_level_status`
177
+
178
+ - Loading a map tears down and rebuilds the world, which runs far past the plugin's 5 s game-thread budget, so `open_level` does not load inline: it validates the request, schedules the load onto the next editor tick, and returns `state: 'opening'` with an `openLevelJobId`. Poll it with `get_open_level_status` (or `wait_for_job`) until the state leaves `opening`
179
+ - `dryRun: true` checks that the level package exists and reports `state: 'validated'` with no job — nothing was scheduled, so nothing is pollable
180
+ - Unsaved changes in the current world are **discarded** unless `saveDirtyPackages: true`, which saves before the teardown. There is no user in front of a bridge request to answer the editor's usual "save these?" prompt
181
+ - **Not undoable.** Loading a map clears the transaction buffer, so `undo` cannot bring the previous level back
182
+ - Actor paths do not survive a load: re-run `list_actors` against the new world rather than reusing paths from the old one
183
+ - One load at a time per editor; a second request while one is running is refused with `open_level_in_progress` rather than queued
184
+
185
+ ## MCP Resources
186
+
187
+ Resources carry context a client can attach without spending a tool call and a model turn. Every one of them is served from in-memory state or the local filesystem: **a resource read never opens a bridge socket and never waits on the editor**, so an unreachable or absent editor produces a structured payload rather than a timeout.
188
+
189
+ All resources use the `uemcp://` URI scheme, are `application/json`, and are capped at 64 KB (`MAX_RESOURCE_PAYLOAD_BYTES`). A read that hits the cap drops entries from the end of the list and sets `truncated: true` while `totalCount` stays authoritative — the same convention the paged tools use. The authority segment (`registry`, `engines`, `workspace`) is the type discriminator.
190
+
191
+ | URI | Contents |
192
+ | --- | --- |
193
+ | `uemcp://registry` | Every registered Unreal Editor: `projectId`, `projectName`, `projectPath`, `port`, `pid`, `registeredAt`, `lastSeenAt`, plus `totalCount` and `truncated` |
194
+ | `uemcp://registry/{projectId}` | One registration, addressed by canonical ProjectID. Enumerable — the template's `list` callback returns the current registry, and `projectId` offers completions from it |
195
+ | `uemcp://engines` | Unreal installs found under `UE_INSTALLS_DIR`, ascending by version, each with `engineVersion`, `engineRoot`, and `unrealBuildToolResolved`. Source builds and GUID engine associations are excluded, matching `resolveEngine` |
196
+ | `uemcp://workspace/project` | The Unreal project identity of the server's working directory, resolved from `Config/DefaultGame.ini` |
197
+
198
+ `uemcp://registry/{projectId}` reports `found: false` with a `reason` for an unknown or malformed ProjectID, and `uemcp://engines` / `uemcp://workspace/project` report a `reason` when `UE_INSTALLS_DIR` is unset or the directory is not an Unreal project — none of them fail the read.
199
+
200
+ Registering or deregistering an editor over the HTTP API emits `notifications/resources/list_changed`. There is no `resources/subscribe`: the MCP SDK's `McpServer` does not implement it, so the list-changed notification is the only change signal.
201
+
202
+ ## MCP Prompts
203
+
204
+ Prompts ship the workflows this server knows about as discoverable entry points for any MCP client, not just ones that can read the repo's `.agents/skills/` runbooks. They are compiled into the bundle, reference only tools this server registers, and assume no shell, no checkout, and no particular client.
205
+
206
+ | Prompt | Arguments | What it drives |
207
+ | --- | --- | --- |
208
+ | `diagnose_crash` | `projectId` | Registration check, `get_crash_callstack`, `get_editor_state`, then the output log around the crash |
209
+ | `diagnose_bridge_disconnect` | `projectPath` (optional) | Which leg of the bridge is broken: registry state, `get_project_id` cross-check, `ping_project`, plugin-side log evidence |
210
+ | `smoke_test_bridge` | `projectId` | Green-path proof: registration, `ping_project`, `get_editor_state`, and a full log subscribe-emit-drain cycle |
211
+ | `add_gameplay_feature` | `projectId`, `className`, `parentClass`, `moduleName` (optional) | `create_cpp_class` → `wait_for_job` → implement → `trigger_live_compile` → `run_automation_tests` → `save_dirty_packages` |
212
+
213
+ `diagnose_bridge_disconnect` and `smoke_test_bridge` are distilled from the same-named skills in `.agents/skills/`. Those files stay separate on purpose: they are maintainer runbooks that reference `just` recipes, `registry.json` paths, and pino module loggers, and the published package ships `dist` only, so they cannot be read at runtime. Each skill carries a pointer to its prompt counterpart so the two do not silently diverge.
214
+
140
215
  ## Configuration
141
216
 
142
217
  - Engine toolchain (`build_target`, `generate_project_files`, `clean_build`):