@uemcp/server 0.4.0 → 1.1.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.
- package/README.md +75 -0
- package/dist/index.mjs +1856 -125
- 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
|
+
### `read_data_table` / `write_data_table_row`
|
|
186
|
+
|
|
187
|
+
- A DataTable is where a project keeps its tuning data, and it is a binary `.uasset` — `find_assets` can locate one but nothing can read it off disk, so these are the only way in. `read_data_table` loads the asset and reports the row struct it is typed on, an ordered column list (`{ name, type }`, using the same type rendering `get_class_reflection` emits), and each row as a column-name-to-value map
|
|
188
|
+
- Values are `ExportText`, the same format `get_actor_details` and `get_cdo_defaults` emit and `set_actor_property` accepts, so a value read here writes straight back with no translation. Column names are the **authored** names, so a Blueprint (user-defined) row struct reports the names the editor shows rather than its mangled internal ones
|
|
189
|
+
- Rows page in **name order** — `RowMap` iteration follows hashing, which shifts as rows are added, so a cursor over it would skip or repeat rows between pages. Pages are trimmed by encoded bytes as well as by `maxRows`, because one row of a wide tuning struct can cost more than a whole page of asset entries; `totalRowCount` stays authoritative when a page truncates
|
|
190
|
+
- `loadIfNeeded: false` refuses to load an unloaded table and reports `asset_not_loaded`. Unlike `get_blueprint_summary` there is no metadata-only fallback: a DataTable carries no Asset Registry tags describing its rows
|
|
191
|
+
- `write_data_table_row` upserts or deletes one row. **Every column name and value is validated against the row struct before anything is written**, into scratch memory that is only copied in once the whole row imports, so a rejected write leaves the table byte-identical — there is no partial application. An unknown column reports `unknown_column` and lists the valid ones; a value that will not parse reports `invalid_row_value` and names the field and its type
|
|
192
|
+
- Numeric and boolean values are range-checked **before** `ImportText` rather than after: the engine's importer takes `not-a-number` as `0` and reports success, and silent coercion is exactly the failure a tuning-data write cannot afford
|
|
193
|
+
- The write runs inside a single `FScopedTransaction` (Ctrl+Z reverses it) and marks the package dirty **without saving** — compose with `save_dirty_packages`. `dryRun: true` validates everything and reports `state: 'validated'`, opening no transaction
|
|
194
|
+
- Deleting a row that does not exist is **not** a failure: it reports `rowExisted: false` with an explanatory message, because the table is already in the requested state
|
|
195
|
+
- A `.uasset` sits read-only on disk until it is checked out under Perforce or Git-LFS file locking. That case is detected before anything is mutated and reported as `source_control_checkout_required` rather than surfacing later as an unexplained save error
|
|
196
|
+
- In a non-editor plugin build `read_data_table` fails with `unsupported` and `write_data_table_row` returns `state: 'unsupported'`
|
|
197
|
+
|
|
198
|
+
### `export_data_table` / `import_data_table`
|
|
199
|
+
|
|
200
|
+
- The bulk half, built on the engine's own JSON and CSV serializers rather than the per-row walk above
|
|
201
|
+
- `export_data_table` returns the serialized table in **chunks by character offset**, because the engine produces one string rather than anything row-addressable. Concatenate chunks in `nextCursor` order to reassemble; a single chunk is not valid JSON or CSV on its own. Chunks are cut by encoded byte cost and never inside a surrogate pair
|
|
202
|
+
- `import_data_table` **replaces every row** — that is what the engine's importer does, so a row absent from the content is a row deleted. Use `write_data_table_row` to change one value. It runs in one transaction and dirties the package without saving, exactly like the per-row write
|
|
203
|
+
- Rows the importer rejects come back in `problems` while the rest still apply, so check `problemCount` even on success. `dryRun: true` runs the import against a transient duplicate and reports the resulting row count and problems without touching the real table
|
|
204
|
+
- The whole payload travels in one 64 KiB bridge frame, so `content` is capped at `BRIDGE_DATA_TABLE_MAX_IMPORT_CHARS` (16 K characters, sized so worst-case UTF-8 expansion still fits). A table larger than that cannot be imported this way
|
|
205
|
+
|
|
206
|
+
### `create_data_table` / `rename_data_table` / `delete_data_table` / `rename_data_table_row`
|
|
207
|
+
|
|
208
|
+
- The lifecycle half: the tools above all require a table that already exists, and until these landed nothing could make one over the bridge
|
|
209
|
+
- `create_data_table` binds a row struct at creation and never after — `UDataTable::RowStruct` can be reassigned, but that reinterprets existing row memory through the new layout, so the bridge does not expose it. Native structs are addressed as `/Script/<Module>.<Struct>`, a Blueprint struct by its object path; anything that is neither an `FTableRowBase` child nor a user-defined struct reports `invalid_row_struct` rather than producing a table that accepts no rows. The new table is dirty and **unsaved**, like every other DataTable write
|
|
210
|
+
- `rename_data_table` renames, moves, or both — one operation to the asset tools. References from other assets are fixed up and a redirector is left at the old path, so existing hard references keep resolving. `newName` must be a bare asset name: one carrying a path separator is refused rather than silently relocating the asset somewhere the caller never named
|
|
211
|
+
- `delete_data_table` is **the one DataTable tool with no transaction behind it** — asset deletion is not undoable, and there is no dirty-package step to reconsider at. A table other assets reference is refused with `asset_referenced` and its referencers reported; `force` deletes anyway and breaks them
|
|
212
|
+
- `rename_data_table_row` keeps the row's position and values, which an upsert-under-the-new-name plus a delete-of-the-old does not: that pair costs two transactions and moves the row to the end. It refuses a missing row (`row_not_found`) and a name already taken (`row_already_exists`), since renaming onto an existing row would discard that row's values. **Row names are how other assets address rows**, so a rename breaks any `FDataTableRowHandle` pointing at the old name — nothing fixes those up
|
|
213
|
+
- Rename and delete both need referencer data, which is only trustworthy once the Asset Registry's initial gather has finished — mid-scan `GetReferencers` returns a short list that reads as "nothing depends on this". Both drive the scan to completion first (starting it when a headless editor never did), and report `asset_registry_scanning` only if it is still incomplete afterwards. On a large project that wait can be long enough to hit the bridge read timeout
|
|
214
|
+
|
|
140
215
|
## MCP Resources
|
|
141
216
|
|
|
142
217
|
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.
|