unreal-engine-mcp-server 0.6.0 → 0.6.1-beta-b

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/CHANGELOG.md CHANGED
@@ -13,6 +13,169 @@ _Nothing yet._
13
13
 
14
14
  ---
15
15
 
16
+ ## 🏷️ [0.6.1-beta-b] - 2026-11-08
17
+
18
+ > [!NOTE]
19
+ > **Beta.** Published as a semver prerelease (`0.6.1-beta-b`) under the npm `beta` dist-tag, so `npm install` keeps serving the newest stable release, **`0.6.0`**. This section is everything on `dev` since the `v0.6.1-beta-a` tag, written from the code diff.
20
+
21
+ <details>
22
+ <summary><b>🔄 Changed</b></summary>
23
+
24
+ - **Inputs are held to their contract**: execute refuses a string or object key past its `maxLength`, or past the 1023 characters an FName holds where none is set (code, scripts and prose parameters take more); a vector or color sent as an array must hold exactly the type's numbers; `add_keyframe` requires `property`.
25
+
26
+ </details>
27
+
28
+ <details>
29
+ <summary><b>🔧 Fixed</b></summary>
30
+
31
+ - **Builds**: the plugin builds on macOS again and on UE 5.0 and 5.1, and a source check in CI keeps those compiler traps out.
32
+ - **Crashes**: an overlong tag, name or key and an `add_keyframe` with no `property` no longer close the editor.
33
+ - **Vectors and colors**: Niagara parameters, effect and skeleton `scale`, light, debug shape and texture colors, material vector defaults, blackboard vector and rotator keys and actor location, rotation and scale no longer pad, cut or zero an array that is not exactly their numbers and answer success.
34
+ - **Sequence keys**: an `add_keyframe` Transform whose value keys nothing is refused `INVALID_ARGUMENT` naming the value instead of reading as an unsupported property, a value that wraps `property` or `value` is refused, and every refusal shows a key's whole params (the old example put `property` inside `value`).
35
+
36
+ </details>
37
+
38
+ <details>
39
+ <summary><b>⚠️ Migration</b></summary>
40
+
41
+ - **Long text**: a string over 1023 characters, or over a parameter's own `maxLength`, is refused `OUT_OF_RANGE`; send long text only in the parameters that take it (code, scripts, descriptions, comments, subtitles, widget text).
42
+ - **Vector and color arrays**: send exactly the type's numbers (three for a vector, three or four for a color), or the object form.
43
+ - **Sequence keys**: send `property` beside `value` in `add_keyframe`; a Transform whose value keys nothing answers `INVALID_ARGUMENT`, not `UNSUPPORTED_PROPERTY`.
44
+
45
+ </details>
46
+
47
+ <details>
48
+ <summary><b>🔄 Dependencies</b></summary>
49
+
50
+ - **GitHub Actions**: `github/codeql-action` (init, autobuild, analyze) 4.39.0 → 4.39.2. No npm package changed, and the package still has no runtime dependencies.
51
+
52
+ </details>
53
+
54
+ <details>
55
+ <summary><b>📊 Change Statistics</b></summary>
56
+
57
+ | Metric | Count |
58
+ |--------|-------|
59
+ | Diff range | `v0.6.1-beta-a..v0.6.1-beta-b` |
60
+ | Commits | 12, the version bump and the release cut included |
61
+ | Files changed | 80 (65 hand-written, not counting generated files or line-ending-only changes) |
62
+ | Insertions / deletions | 2,035 / 540 (hand-written: 889 / 101) |
63
+ | Capability records | 489 |
64
+ | Callable `{tool, action}` pairs | 1,658 |
65
+ | C++ domain directories | 58 |
66
+
67
+ </details>
68
+
69
+ ---
70
+
71
+ ## 🏷️ [0.6.1-beta-a] - 2026-11-06
72
+
73
+ > [!NOTE]
74
+ > **Beta.** Published as a semver prerelease (`0.6.1-beta-a`) under the npm `beta` dist-tag, so `npm install` keeps serving the newest stable release, **`0.6.0`**. This section is everything on `dev` since the `v0.6.0` tag, written from the code diff.
75
+
76
+ <details>
77
+ <summary><b>✨ Added</b></summary>
78
+
79
+ - **Engine versions**: a capability an older engine cannot run reads `unavailable` and is refused `ENGINE_VERSION_UNSUPPORTED` (Geometry Script 5.1, MetaSound graph reads and skinning 5.5, retargeting 5.8).
80
+ - **Search and execute**: a typo is read as the word meant (`corrected`), `options.aggregate` counts a long list by group, and an alias sent as tool + action resolves.
81
+ - **Blueprints**: `lint_blueprint` follows values (a divisor that can be 0, an index past the end, a Set outside a Value Range, `==` on computed floats, a decided Branch, an endless While); `fuzz_blueprint` calls a Blueprint's functions in Play with edge-case inputs and names each error's node; MVVM viewmodels (UE 5.4+); `add_variable` `exposeToCinematics`.
82
+ - **Graph as code**: `read_graph_as_code` `entry` and `codeOnly`; `write_graph_from_code` reads back only what it wrote.
83
+ - **Views and cameras**: `camera` looks through a camera actor (`screenshot`, `describe_view`, `analyze_view`, `audit_materials`); `pause` and `list_track_keys` name each cut's camera; `screenshot` `renderSize` and `pathTraced` (a denoised path-traced image); `fov` on every view read.
84
+ - **Plugins and project**: `get_plugin_info`, `create_plugin` / `remove_plugin`, game features (UE 5.1+), data registries, an unused-asset report, `set_project_thumbnail`, `get_console_variables`, `remove_cpp_class`.
85
+ - **Editor**: `read_editor_ui`, `configure_editor` `close_tab`, `set_selected_actors`, viewport `show_flags`, `reimport` without a dialog, `set_current_level`, `console_command` `filter`.
86
+ - **Actors**: snap to grid, distribute, align, lock, group, randomize, replace, convert to a Blueprint, move to a level, numbered rename, copies along a spline, `place_decals`; `list` `bounds` and `find` locations; `sample_motion` `feel` (jump, run, key latency, camera jerk).
87
+ - **Sequences**: `add_actors` `rebind` points a sequence copied with its level at the new level's actors; `add_behaviour` `sequence_trigger` plays one when the player walks in.
88
+ - **Geometry**: `append_tubes`, `append_polygons` `repeat` and `bake_vertex_colors` `coarseShare`.
89
+ - **Look and sound**: material `color` / `glow`, light `flicker`, a first-person view, `sound_trigger`, subtitles, sound volume and looping, footsteps, animation play rate, `massKg`.
90
+ - **Scripts and sheets**: `run_script` takes `(if cond a b)` as a value and waits within `deadlineMs` for a call that answers later; contact and thumbnail sheets take `keepFile: false`.
91
+ - **Frame budget**: `fit_frame_budget` finds which lights' shadows and far small meshes to turn down to meet a GPU frame time at the least change to the picture, and keeps them as one undo step.
92
+ - **Proxy CLI**: `init <client>` writes a client's MCP config; `doctor` checks the setup (the token is never shown).
93
+
94
+ </details>
95
+
96
+ <details>
97
+ <summary><b>🔄 Changed</b></summary>
98
+
99
+ - **Movie Render Queue, Takes and MetaSound are required plugins**: EngineWire links their modules, so enabling it turns them on (left off in a Blueprint-only project, EngineWire did not load on Windows and the editor crashed at startup on macOS).
100
+ - **Write replies pass on only lint errors**: the warnings (unreachable nodes, heavy Tick work) stay in Validate Assets, saves and `validate_assets`, so a graph built one node at a time is not told its newest node never runs.
101
+ - **No-op and mid-compile edits are refused**: an edit that changes nothing is refused instead of answered done, and so is any edit while a Live Coding compile runs.
102
+ - **Scopes follow what a call writes**: `export_level` needs `write`, and `export_mesh` `outputPath`, `screenshot` `path` and `get_transform` (`sample_motion`) `inputs` raise a call to `write`.
103
+
104
+ </details>
105
+
106
+ <details>
107
+ <summary><b>🛡️ Security</b></summary>
108
+
109
+ - **Native `/mcp` binds to loopback only**, whatever **Allow Non Loopback** says: it has no TLS, so a token would cross the network in clear text. A WebSocket listener on a LAN address needs the capability token and TLS.
110
+ - **The stdio proxy sends its token only to a loopback upstream** unless `UNREAL_MCP_ALLOW_REMOTE_TOKEN=1`, and `doctor` follows the same rule.
111
+ - **A path-restricted token's prefixes hold everywhere**: saves, deletes, reflection reads, searches and listings run as the request's principal, and a file the caller places is written through a handle checked inside the project.
112
+ - **Inputs and consent**: script, search and list inputs are bounded, and a consent grant comes back only when no handler ran.
113
+
114
+ </details>
115
+
116
+ <details>
117
+ <summary><b>🔧 Fixed</b></summary>
118
+
119
+ - **Plugin loading**: geometry and skinning actions refuse with `GEOMETRYSCRIPTING_PLUGIN_NOT_ENABLED` while GeometryScripting is off instead of crashing, and the game features module compiles on UE 5.6 and 5.7.
120
+ - **Crashes**: a glTF or GLB export and a key tapped with no Play session no longer take the editor down.
121
+ - **Input**: simulated game keys stay out of the editor's panels.
122
+ - **Search**: about 250 more plain requests find their capability, and an alias a family member declared runs that member.
123
+ - **Replies**: long lists page and aggregate from the reply's `data`; console replies stay short; a script step still running at the deadline says so; logpoints read an unwired input's own value; the tool says how to get a fresh consent grant.
124
+ - **Undo**: asset edits are one step each (notifies, sockets, physics bodies, blackboard keys, Behavior Trees and more), enum undo no longer crashes UE 5.7+, and a script's rollback undoes only its own steps.
125
+ - **Graph as code**: `%`, `==` on text, calls used as values, ids with underscores, custom event calls, other classes' variables, expose-on-spawn inputs, calls on the Blueprint itself and events bound to a delegate write as `read_graph_as_code` prints them.
126
+ - **Placement**: props in a room built as one mesh read as they are; a support with no collision is named; `fix_coplanar` checks each move against the others.
127
+ - **Views**: frame readers warn while shaders, meshes or textures still build; `contact_sheet` draws beside water and titles small sheets in full.
128
+ - **Geometry**: `bevel` and `chamfer` cut whole parts, and material and PolyGroup edits are their own undo steps.
129
+ - **Assets**: `check_mesh` finds parts wound inside out, on skeletal meshes too; FBX imports open no Message Log window and convert units with options; material instances draw their parent; morph targets and skin weight profiles survive rebuilds; output files reach captures, exports and thumbnails.
130
+ - **Editor state**: `save_all` saves streaming sublevels; dry runs no longer compile or save; Blueprint compiles leave placed skeletal meshes posed; Behavior Trees build from their Root.
131
+ - **Niagara**: a created emitter keeps the name it was asked for.
132
+ - **Game framework and materials**: `default_pawn_class` takes `None`, and material pins connect by the short names the material editor shows.
133
+ - **Sequence sections**: a sound section plays in the editor and the game, and adding one is one undo step.
134
+
135
+ </details>
136
+
137
+ <details>
138
+ <summary><b>⚠️ Migration</b></summary>
139
+
140
+ - **A client on another machine**: the native server no longer binds to a LAN address. Tunnel to it (`ssh -L 3000:127.0.0.1:3000 <user>@<editor machine>`) and keep `UNREAL_MCP_URL` at `http://127.0.0.1:3000/mcp`; a stdio proxy pointed at another machine sends no token unless `UNREAL_MCP_ALLOW_REMOTE_TOKEN=1`. A WebSocket listener on a LAN address also needs **Enable Tls**.
141
+ - **Scoped tokens**: a token without `write` no longer runs `export_level`, nor `export_mesh` with `outputPath`, `screenshot` with `path` or `get_transform` with `inputs`.
142
+ - **Projects that turned off Movie Render Queue, Takes or MetaSound**: enabling EngineWire turns them on again.
143
+ - **Lint warnings after a write**: read them with `lint_blueprint` or `validate_assets`; write replies carry only lint errors.
144
+
145
+ </details>
146
+
147
+ <details>
148
+ <summary><b>🔄 Dependencies</b></summary>
149
+
150
+ - None: no npm package and no GitHub Action changed, and the package still has no runtime dependencies.
151
+
152
+ </details>
153
+
154
+ <details>
155
+ <summary><b>👥 Contributors</b></summary>
156
+
157
+ - @SoloGorilla for keeping a created Niagara emitter's requested name (#669), and for reporting the crash when a key was tapped with no play session (#685) and simulated keys reaching editor panels, `sample_motion` inputs included (#686).
158
+ - @claudemlandry for reporting the macOS startup crash on UE 5.7.4 (#705).
159
+
160
+ </details>
161
+
162
+ <details>
163
+ <summary><b>📊 Change Statistics</b></summary>
164
+
165
+ | Metric | Count |
166
+ |--------|-------|
167
+ | Diff range | `v0.6.0..v0.6.1-beta-a` |
168
+ | Commits | 164, the version bump and the release cut included |
169
+ | Files changed | 767 (741 hand-written, not counting generated files or line-ending-only changes) |
170
+ | Insertions / deletions | 43,428 / 5,766 (hand-written: 29,413 / 3,729) |
171
+ | Capability records | 489 |
172
+ | Callable `{tool, action}` pairs | 1,658 |
173
+ | C++ domain directories | 58 |
174
+
175
+ </details>
176
+
177
+ ---
178
+
16
179
  ## 🏷️ [0.6.0] - 2026-10-27
17
180
 
18
181
  > [!NOTE]
@@ -83,7 +246,7 @@ _Nothing yet._
83
246
  - **`manage_effect cleanup`** needs destructive consent and a `filter`, is one undo step and refuses Play; debug shapes clear through `debug_shape`.
84
247
  - **Object paths** work in `open_level`, `load` and `add_sublevel`; relative `.t3d` exports land in the project.
85
248
  - **Git**: any commit message is safe (a quote could inject options); checkout and submit list skipped paths.
86
- - **`/mcp` discovery**: search rows, `describe` by domain and family, `cursor` and param `suggestions`.
249
+ - **`/mcp` discovery**: search rows, `describe` by domain and family, `cursor` and param `suggestions`; plain requests ("delete all cubes", "make it rain", "measure distance between actors") reach the capability that answers them.
87
250
  - **A disabled tool's `nextCall` turns it back on** (`configure` `enable_tools`).
88
251
 
89
252
  </details>
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
  <a href="https://github.com/ChiR24/Unreal_mcp/discussions"><b>💬 Discussions</b></a>
35
35
  </p>
36
36
 
37
- > 📌 **Which version is this?** This README describes **0.6.0**, the stable release: npm `unreal-engine-mcp-server` (`latest`) and the plugin on the [Releases page](https://github.com/ChiR24/Unreal_mcp/releases) and on [Fab](https://www.fab.com/listings/9407137f-14b3-44a8-af0e-e9c6a9c9f6fc). The previous release, **0.5.30**, exposes 23 separate tools instead of one; see [Upgrading from 0.5.x](https://github.com/ChiR24/Unreal_mcp/wiki/Upgrading).
37
+ > 📌 **Which version is this?** This README describes **0.6.0**, the stable release: the plugin on [Fab](https://www.fab.com/listings/9407137f-14b3-44a8-af0e-e9c6a9c9f6fc) and on the [Releases page](https://github.com/ChiR24/Unreal_mcp/releases), and npm `unreal-engine-mcp-server` (`latest`). The previous release, **0.5.30**, exposes 23 separate tools instead of one; staying on a 0.5.30 plugin means pinning `unreal-engine-mcp-server@0.5.30`. See [Upgrading from 0.5.x](https://github.com/ChiR24/Unreal_mcp/wiki/Upgrading).
38
38
 
39
39
  **Contents** · [What it does](#what-it-does) · [How it works](#how-it-works) · [Quick start](#quick-start) · [The `unreal` tool](#the-unreal-tool) · [Configuration](#configuration) · [Security](#security) · [Engine plugins](#engine-plugins) · [Docker](#docker) · [Documentation](#documentation) · [Development](#development) · [Community](#community)
40
40
 
@@ -85,7 +85,7 @@ Run Play-In-Editor with synthetic input, take screenshots the model can see, rea
85
85
  </tr>
86
86
  </table>
87
87
 
88
- Nearly 400 capabilities in all, behind a single MCP tool. The assistant finds them by searching in plain words, so nobody has to learn their names. The full list is the generated [Action Reference](https://github.com/ChiR24/Unreal_mcp/blob/dev/docs/action-reference.generated.md).
88
+ Over 450 capabilities in all, behind a single MCP tool. The assistant finds them by searching in plain words, so nobody has to learn their names. The full list is the generated [Action Reference](https://github.com/ChiR24/Unreal_mcp/blob/dev/docs/action-reference.generated.md).
89
89
 
90
90
  <p align="center">
91
91
  <img src="https://raw.githubusercontent.com/wiki/ChiR24/Unreal_mcp/assets/screenshots/editor.webp" alt="Unreal Editor 5.8 showing a platformer level built with the MCP; the status bar reads MCP :3000 (1)" width="100%">
@@ -116,7 +116,7 @@ The [Quick Start](https://github.com/ChiR24/Unreal_mcp/wiki/Quick-Start) page wa
116
116
 
117
117
  ### 1. Add the plugin
118
118
 
119
- **From Fab (no compiler needed).** Get [EngineWire on Fab](https://www.fab.com/listings/9407137f-14b3-44a8-af0e-e9c6a9c9f6fc). In the Epic Games Launcher, open **Unreal Engine › Library › Fab Library**, find **EngineWire** and click **Install to Engine** for your engine version. Open your project, enable **EngineWire** in **Edit › Plugins** and restart the editor. The Fab build ships with the native server off: turn on **Enable Native MCP Server** in **Edit › Project Settings › Plugins › EngineWire › Native MCP** and restart once more. Fab updates follow Epic's review, so the Fab build can trail the newest release here.
119
+ **From Fab (no compiler needed).** Get [EngineWire on Fab](https://www.fab.com/listings/9407137f-14b3-44a8-af0e-e9c6a9c9f6fc). In the Epic Games Launcher, open **Unreal Engine › Library › Fab Library**, find **EngineWire** and click **Install to Engine** for your engine version. Open your project, enable **EngineWire** in **Edit › Plugins** and restart the editor. Fab's 0.6.0 build ships with the native server off: if the status bar reads **`MCP off`**, turn on **Enable Native MCP Server** in **Edit › Project Settings › Plugins › EngineWire › Native MCP** and restart once more. Fab updates follow Epic's review, so the Fab build can trail the newest release here.
120
120
 
121
121
  **From a release.** Download `EngineWire-plugin-<version>.zip` from the newest release on the [Releases page](https://github.com/ChiR24/Unreal_mcp/releases), and copy the `EngineWire` folder it contains into your project:
122
122
 
@@ -141,7 +141,7 @@ Build the plugin once on a machine with the engine and a compiler, then hand out
141
141
  node scripts/package-plugin.mjs "C:/Program Files/Epic Games/UE_5.7"
142
142
  ```
143
143
 
144
- This writes `build/EngineWire-v<version>-UE5.7-<Platform>.zip`, where `<version>` is the `package.json` version (currently `0.6.0`). Unzip it into `YourProject/Plugins/`. Binaries only work with the engine minor and platform they were built for: a 5.6 build won't load in 5.5, 5.7 or 5.8.
144
+ This writes `build/EngineWire-v<version>-UE5.7-<Platform>.zip`, where `<version>` is the `package.json` version (currently `0.6.1-beta-b`). Unzip it into `YourProject/Plugins/`. Binaries only work with the engine minor and platform they were built for: a 5.6 build won't load in 5.5, 5.7 or 5.8.
145
145
 
146
146
  </details>
147
147
 
@@ -149,7 +149,7 @@ This writes `build/EngineWire-v<version>-UE5.7-<Platform>.zip`, where `<version>
149
149
 
150
150
  **Route A · Native HTTP (no Node.js)**
151
151
 
152
- 1. **Enable Native MCP Server** is on by default (**Edit › Project Settings › Plugins › EngineWire**, port `3000`); the Fab build ships with it off. A project where it is off needs it on and an editor restart.
152
+ 1. **Enable Native MCP Server** is on by default (**Edit › Project Settings › Plugins › EngineWire**, port `3000`); Fab's 0.6.0 build ships with it off. A project where it is off needs it on and an editor restart.
153
153
 
154
154
  <img src="https://raw.githubusercontent.com/wiki/ChiR24/Unreal_mcp/assets/screenshots/settings-native-mcp.png" alt="The Native MCP section of the plugin settings, with Enable Native MCP Server ticked" width="520">
155
155
 
@@ -200,6 +200,8 @@ Add this to your client's MCP configuration, for example Claude Desktop's `claud
200
200
  }
201
201
  ```
202
202
 
203
+ Or let the package write it (0.6.1-beta-a and later, so `@beta` until 0.6.1 is the stable release): `npx unreal-engine-mcp-server@beta init claude --project C:/Path/To/YourProject` shows the entry for Claude Code's `.mcp.json` (also `cursor`, `vscode`, `gemini` and `codex`), keeping the file's other servers, and `--write` saves it. `npx unreal-engine-mcp-server@beta doctor` checks Node, the token (never shown), the editor's answer and that both sides run the same version.
204
+
203
205
  `UE_PROJECT_PATH` (the project folder or its `.uproject`) is how the proxy finds the capability token (`MCP_AUTOMATION_CAPABILITY_TOKEN` overrides it). `UNREAL_MCP_URL` points it at another endpoint (default `http://127.0.0.1:3000/mcp`). Pair it with a 0.6 plugin; a 0.5.x plugin serves its 23 separate tools instead of the `unreal` gateway.
204
206
 
205
207
  ### 3. Try it
@@ -241,7 +243,7 @@ A typical exchange:
241
243
  ```
242
244
 
243
245
  - **Parameters are strict.** An undeclared name is refused with `UNDECLARED_PARAMETER` and the list of allowed names; every error carries an executable `nextCall`.
244
- - **Deletes need consent.** 62 capabilities (all destructive ones, plus some writes) only run with the `consentGrant` from `describe`, passed back as a top-level `consent` field.
246
+ - **Deletes need consent.** Every destructive capability, plus some risky writes, only runs with the `consentGrant` from `describe`, passed back as a top-level `consent` field.
245
247
  - **Names.** A call names the `tool` + `action` pair or a capability id, such as `"capability": "control_actor.spawn"`.
246
248
 
247
249
  Full reference with real replies: [Using the Gateway](https://github.com/ChiR24/Unreal_mcp/wiki/Using-the-Gateway).
@@ -314,7 +316,7 @@ Most setups touch one setting: `UE_PROJECT_PATH` for Route B; Route A needs none
314
316
 
315
317
  ## Security
316
318
 
317
- - **Local by default.** Both routes listen on `127.0.0.1` only. LAN access needs **Allow Non Loopback**, and the native server refuses to bind off-loopback unless **Require Capability Token** is on.
319
+ - **Local by default.** Both routes listen on `127.0.0.1` only. The native server never binds off-loopback: it has no TLS, so reach it from another machine through an SSH tunnel (`ssh -L 3000:127.0.0.1:3000`) or a TLS proxy. The WebSocket bridge binds a LAN address only with **Allow Non Loopback**, **Require Capability Token** and **Enable Tls** all on. The proxy sends the token to another machine only with `UNREAL_MCP_ALLOW_REMOTE_TOKEN=1`.
318
320
  - **Capability token.** Generated per project at `<Project>/Saved/MCP/capability-token` (a value typed into **Capability Token** overrides it) and compared in constant time. Delete the file and restart the editor to rotate it.
319
321
  - **Consent for destructive work.** Deletes and some other writes need a per-call consent grant, which the plugin checks itself.
320
322
  - **Guard rails.** Asset paths are limited to `/Game`, `/Engine`, `/Script`, `/Temp` and `/Niagara`, and a content path may also sit under a mount the connected editor reports; arguments the server treats as files (such as `filePath`, and `outputPath` even where it names an asset) never use the reported mounts; console commands that chain or quit the editor are blocked; the plugin's own settings are out of reach of automation.
@@ -337,6 +339,8 @@ The bridge declares its engine-plugin dependencies, so Unreal enables them toget
337
339
  | **Niagara** | Visual effects |
338
340
  | **Gameplay Abilities** | `manage_gas` |
339
341
  | **Smart Objects** | AI smart objects |
342
+ | **Movie Render Pipeline**, **Movie Pipeline Mask Render Pass**, **Takes** | `manage_sequence`: Movie Render Queue and Take Recorder |
343
+ | **MetaSound** | `manage_audio`: MetaSound authoring |
340
344
 
341
345
  </details>
342
346
 
@@ -347,14 +351,16 @@ The bridge declares its engine-plugin dependencies, so Unreal enables them toget
347
351
 
348
352
  | Plugin | Used for |
349
353
  | --- | --- |
350
- | **Level Sequence Editor**, **Takes**, **Movie Render Pipeline**, **Movie Pipeline Mask Render Pass**, **Electra Player** | `manage_sequence`: Sequencer, Take Recorder, Movie Render Queue, media playback |
354
+ | **Level Sequence Editor**, **Electra Player** | `manage_sequence`: Sequencer, media playback |
351
355
  | **Control Rig**, **RigVM**, **IK Rig**, **Animation Data** | `animation_physics`: Control Rig and IK |
352
356
  | **Chaos Vehicles**, **Chaos Cloth** | `animation_physics`: vehicles and cloth |
353
357
  | **Niagara Editor** | `manage_effect`: Niagara authoring |
354
358
  | **Behavior Tree Editor**, **Environment Query Editor**, **StateTree**, **Mass Gameplay** | `manage_ai` |
355
359
  | **Geometry Scripting**, **Geometry Processing**, **Mesh Modeling Toolset**, **Procedural Mesh Component** | `manage_geometry` (Catmull-Clark, Loop and bilinear subdivision need Mesh Modeling Toolset) |
356
360
  | **PCG** | `manage_pcg`, compiled in only when the project itself enables the PCG plugin |
357
- | **MetaSound**, **Synthesis** | `manage_audio`: MetaSound authoring |
361
+ | **Game Features** | `system_control`: game feature plugins (listed, activated, made with `create_plugin`) through the `EngineWireGameFeatures` module, loaded only when the project itself enables Game Features (EngineWire never turns it on) |
362
+ | **Data Registry** | `manage_asset`: data registries listed, read and made over data tables, by reflection when the project itself enables Data Registry (EngineWire never turns it on) |
363
+ | **Synthesis** | `manage_audio`: source effect presets |
358
364
  | **Enhanced Input**, **Online Subsystem**, **Online Subsystem Utils** | `manage_networking`: input mappings, sessions |
359
365
  | **Interchange**, **Interchange OpenUSD**, **Data Validation**, **StructUtils** | Import/export, validation, struct helpers |
360
366
  | **Fab**, **Bridge** | Fab asset library access (optional `EngineWireFab` module) |
package/dist/cli.js CHANGED
@@ -1,5 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import { runDoctor } from './proxy/doctor.js';
3
+ import { runInit } from './proxy/init.js';
2
4
  import { startNativeProxy } from './proxy/native-proxy.js';
3
- startNativeProxy();
4
- process.stdin.once('end', () => process.exit(0));
5
+ const [command, ...rest] = process.argv.slice(2);
6
+ if (command === 'doctor') {
7
+ process.exitCode = await runDoctor();
8
+ }
9
+ else if (command === 'init') {
10
+ const result = runInit(rest);
11
+ process.stdout.write(`${result.content ? `${result.file}\n\n${result.content}\n` : ''}${result.message}\n`);
12
+ process.exitCode = result.ok ? 0 : 1;
13
+ }
14
+ else {
15
+ startNativeProxy();
16
+ process.stdin.once('end', () => process.exit(0));
17
+ }
5
18
  //# sourceMappingURL=cli.js.map
@@ -0,0 +1,94 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { request } from 'node:http';
3
+ import { join } from 'node:path';
4
+ import { PACKAGE } from '../constants.js';
5
+ import { projectRootFromEnv } from '../utils/config/ini-reader.js';
6
+ import { oneLine } from '../utils/logging/logger.js';
7
+ import { readCapabilityToken, sendsTokenTo } from './native-proxy.js';
8
+ const NODE_FLOOR = [20, 19];
9
+ export function nodeLine(version = process.versions.node) {
10
+ const [major = 0, minor = 0] = version.split('.').map(Number);
11
+ const fits = major > NODE_FLOOR[0] || (major === NODE_FLOOR[0] && minor >= NODE_FLOOR[1]);
12
+ return fits ? { level: 'ok', text: `Node ${version}` }
13
+ : { level: 'FAIL', text: `Node ${version} is older than ${NODE_FLOOR.join('.')}: install Node ${NODE_FLOOR.join('.')} or later` };
14
+ }
15
+ export function tokenLine(token) {
16
+ if (process.env.MCP_AUTOMATION_CAPABILITY_TOKEN) {
17
+ return { level: 'ok', text: 'Capability token from MCP_AUTOMATION_CAPABILITY_TOKEN (its value is not shown)' };
18
+ }
19
+ const root = projectRootFromEnv();
20
+ if (!root)
21
+ return { level: 'warn', text: 'No capability token: set UE_PROJECT_PATH to the project folder (or MCP_AUTOMATION_CAPABILITY_TOKEN)' };
22
+ if (!existsSync(root))
23
+ return { level: 'warn', text: `UE_PROJECT_PATH names ${root}, which does not exist` };
24
+ const file = join(root, 'Saved', 'MCP', 'capability-token');
25
+ return token ? { level: 'ok', text: `Capability token found in ${file} (its value is not shown)` }
26
+ : { level: 'warn', text: `No valid token in ${file}: open the project in the editor once, so the plugin writes it` };
27
+ }
28
+ function send(url, method, headers, body = '') {
29
+ return new Promise((resolve, reject) => {
30
+ const sent = request(url, { method, agent: false, timeout: 5000, headers: { Connection: 'close', ...headers } }, (reply) => {
31
+ let text = '';
32
+ reply.setEncoding('utf8');
33
+ reply.on('data', (chunk) => { text += chunk; });
34
+ reply.on('end', () => resolve({ status: reply.statusCode ?? 0, headers: reply.headers, text }));
35
+ reply.on('error', reject);
36
+ });
37
+ sent.on('timeout', () => sent.destroy(new Error('no answer within 5 s')));
38
+ sent.on('error', reject);
39
+ sent.end(body);
40
+ });
41
+ }
42
+ function initializeAnswer(text) {
43
+ const json = text.trimStart().startsWith('{') ? text : text.split(/\r?\n/u).find((line) => line.startsWith('data:'))?.slice(5) ?? '';
44
+ try {
45
+ return JSON.parse(json) ?? {};
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ export async function doctorLines(url = process.env.UNREAL_MCP_URL ?? 'http://127.0.0.1:3000/mcp') {
52
+ const found = await readCapabilityToken();
53
+ const lines = [nodeLine(), tokenLine(found)];
54
+ const token = sendsTokenTo(url) ? found : undefined;
55
+ if (found && !token) {
56
+ lines.push({ level: 'warn', text: `${url} is not this machine, so the token is not sent over plain HTTP; set UNREAL_MCP_ALLOW_REMOTE_TOKEN=1 to send it anyway` });
57
+ }
58
+ const auth = token ? { 'X-MCP-Capability-Token': token } : {};
59
+ const body = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2025-11-25',
60
+ capabilities: {}, clientInfo: { name: `${PACKAGE.name} doctor`, version: PACKAGE.version } } });
61
+ let hello;
62
+ try {
63
+ hello = await send(url, 'POST', { ...auth, 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream',
64
+ 'Content-Length': Buffer.byteLength(body) }, body);
65
+ }
66
+ catch (error) {
67
+ lines.push({ level: 'FAIL', text: `Nothing answers at ${url} (${error instanceof Error ? error.message : String(error)}): start the `
68
+ + 'editor; if it runs, turn on Project Settings > Plugins > EngineWire > Enable Native MCP Server, or set UNREAL_MCP_URL' });
69
+ return lines;
70
+ }
71
+ const session = hello.headers['mcp-session-id'];
72
+ if (typeof session === 'string')
73
+ await send(url, 'DELETE', { ...auth, 'Mcp-Session-Id': session }).catch(() => undefined);
74
+ const { result, error } = initializeAnswer(hello.text);
75
+ const server = result?.serverInfo;
76
+ if (hello.status === 401 || hello.status === 403) {
77
+ lines.push({ level: 'FAIL', text: `The editor at ${url} refused this client (HTTP ${hello.status}): the token is missing or not this project's` });
78
+ }
79
+ else if (!server?.version) {
80
+ lines.push({ level: 'FAIL', text: `${url} answered HTTP ${hello.status}, ${error?.message ? `refusing: ${oneLine(error.message)}` : 'not an MCP initialize reply'}` });
81
+ }
82
+ else {
83
+ lines.push({ level: 'ok', text: `Editor answers at ${url}: ${oneLine(server.name ?? 'server')} ${oneLine(server.version)}, protocol ${oneLine(result?.protocolVersion ?? '?')}` });
84
+ lines.push(server.version === PACKAGE.version ? { level: 'ok', text: `Proxy and plugin are both ${PACKAGE.version}` }
85
+ : { level: 'warn', text: `Proxy ${PACKAGE.version} and plugin ${oneLine(server.version)} differ: update the older one` });
86
+ }
87
+ return lines;
88
+ }
89
+ export async function runDoctor(write = (text) => { process.stdout.write(text); }) {
90
+ const lines = await doctorLines();
91
+ write(`${PACKAGE.name} ${PACKAGE.version} doctor\n${lines.map((line) => `${line.level.padEnd(4)} ${line.text}`).join('\n')}\n`);
92
+ return lines.some((line) => line.level === 'FAIL') ? 1 : 0;
93
+ }
94
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,88 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
4
+ import { PACKAGE } from '../constants.js';
5
+ export const CLIENTS = {
6
+ claude: { file: '.mcp.json', key: 'mcpServers' },
7
+ cursor: { file: '.cursor/mcp.json', key: 'mcpServers' },
8
+ vscode: { file: '.vscode/mcp.json', key: 'servers', type: 'stdio' },
9
+ gemini: { file: '.gemini/settings.json', key: 'mcpServers' },
10
+ codex: { file: '.codex/config.toml', home: true, toml: true },
11
+ };
12
+ const NAME = 'unreal-engine';
13
+ export function projectFor(argument, cwd, env) {
14
+ const named = argument ?? env.UE_PROJECT_PATH;
15
+ if (named)
16
+ return (isAbsolute(named) ? named : resolve(cwd, named)).replace(/\\/gu, '/');
17
+ return readdirSync(cwd).some((file) => file.toLowerCase().endsWith('.uproject')) ? cwd.replace(/\\/gu, '/') : undefined;
18
+ }
19
+ export function launch(platform) {
20
+ const npx = ['-y', PACKAGE.name];
21
+ return platform === 'win32' ? { command: 'cmd', args: ['/c', 'npx', ...npx] } : { command: 'npx', args: npx };
22
+ }
23
+ export function serverEntry(client, project, platform) {
24
+ return { ...(client.type ? { type: client.type } : {}), ...launch(platform), ...(project ? { env: { UE_PROJECT_PATH: project } } : {}) };
25
+ }
26
+ const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
27
+ export function mergeJson(existing, client, entry) {
28
+ const text = existing?.replace(/^\uFEFF/u, '').trim();
29
+ const config = text ? JSON.parse(text) : {};
30
+ const key = client.key ?? 'mcpServers';
31
+ const servers = isObject(config) ? config[key] ?? {} : undefined;
32
+ if (!isObject(config) || !isObject(servers))
33
+ throw new Error(`not an object with "${key}"`);
34
+ config[key] = { ...servers, [NAME]: entry };
35
+ return `${JSON.stringify(config, null, 2)}\n`;
36
+ }
37
+ export function tomlBlock(project, platform) {
38
+ const { command, args } = launch(platform);
39
+ return `[mcp_servers.${NAME}]\ncommand = ${JSON.stringify(command)}\nargs = [${args.map((arg) => JSON.stringify(arg)).join(', ')}]\n`
40
+ + (project ? `\n[mcp_servers.${NAME}.env]\nUE_PROJECT_PATH = ${JSON.stringify(project)}\n` : '');
41
+ }
42
+ export function runInit(argv, options = {}) {
43
+ const cwd = options.cwd ?? process.cwd();
44
+ const env = options.env ?? process.env;
45
+ const platform = options.platform ?? process.platform;
46
+ let name;
47
+ let projectArg;
48
+ for (let index = 0; index < argv.length; index++) {
49
+ if (argv[index] === '--project')
50
+ projectArg = argv[++index];
51
+ else if (!argv[index].startsWith('--'))
52
+ name ??= argv[index];
53
+ }
54
+ const client = name ? CLIENTS[name] : undefined;
55
+ if (!client) {
56
+ return { ok: false, file: '', content: '', written: false, message: `usage: ${PACKAGE.name} init <${Object.keys(CLIENTS).join('|')}> `
57
+ + '[--project <folder>] [--write]' };
58
+ }
59
+ const project = projectFor(projectArg, cwd, env);
60
+ const file = join(client.home ? options.home ?? homedir() : cwd, client.file);
61
+ const existing = existsSync(file) ? readFileSync(file, 'utf8') : undefined;
62
+ let content;
63
+ if (client.toml) {
64
+ if (existing && /^\s*\[\s*mcp_servers\s*\.\s*"?unreal-engine"?\s*[\].]/mu.test(existing)) {
65
+ return { ok: false, file, content: tomlBlock(project, platform), written: false,
66
+ message: `${file} already has [mcp_servers.${NAME}]; it is left as it is, so edit it to read as above` };
67
+ }
68
+ content = `${existing?.trim() ? `${existing.trimEnd()}\n\n` : ''}${tomlBlock(project, platform)}`;
69
+ }
70
+ else {
71
+ try {
72
+ content = mergeJson(existing, client, serverEntry(client, project, platform));
73
+ }
74
+ catch {
75
+ return { ok: false, file, content: '', written: false,
76
+ message: `${file} is not a plain JSON object with "${client.key}" (comments?): add the "${NAME}" entry by hand` };
77
+ }
78
+ }
79
+ const write = argv.includes('--write');
80
+ if (write) {
81
+ mkdirSync(dirname(file), { recursive: true });
82
+ writeFileSync(file, content);
83
+ }
84
+ const noProject = project ? '' : ' No project folder was found, so UE_PROJECT_PATH is left out: pass --project <folder>.';
85
+ return { ok: true, file, content, written: write,
86
+ message: (write ? `Written to ${file}.` : `Not written: run again with --write to save it to ${file}.`) + noProject };
87
+ }
88
+ //# sourceMappingURL=init.js.map
@@ -44,6 +44,10 @@ export async function readCapabilityToken() {
44
44
  return undefined;
45
45
  }
46
46
  }
47
+ export function sendsTokenTo(upstream) {
48
+ const host = new URL(upstream).hostname.replace(/^\[|\]$/gu, '');
49
+ return /^(localhost|127(\.\d{1,3}){3}|::1)$/u.test(host) || process.env.UNREAL_MCP_ALLOW_REMOTE_TOKEN === '1';
50
+ }
47
51
  export function startNativeProxy(options = {}) {
48
52
  const upstream = options.upstream ?? process.env.UNREAL_MCP_URL ?? 'http://127.0.0.1:3000/mcp';
49
53
  const cacheFile = options.cacheFile ?? process.env.UNREAL_MCP_PROXY_CACHE
@@ -51,6 +55,9 @@ export function startNativeProxy(options = {}) {
51
55
  const output = options.output ?? process.stdout;
52
56
  const log = new Logger('NativeProxy');
53
57
  const token = options.token ?? readCapabilityToken;
58
+ const sendsToken = sendsTokenTo(upstream);
59
+ if (!sendsToken)
60
+ log.warn(`${upstream} is not this machine, so the capability token is not sent over plain HTTP; set UNREAL_MCP_ALLOW_REMOTE_TOKEN=1 to send it anyway.`);
54
61
  const notConnected = `NOT_CONNECTED: the Unreal Editor is not running (nothing answers at ${upstream}). `
55
62
  + 'Start the editor and call again; this server stays connected, no reconnect is needed. If the editor is '
56
63
  + 'running, turn on Project Settings > EngineWire > Enable Native MCP Server and restart it.';
@@ -86,7 +93,7 @@ export function startNativeProxy(options = {}) {
86
93
  'Content-Length': Buffer.byteLength(payload),
87
94
  ...modern
88
95
  };
89
- const capabilityToken = await token();
96
+ const capabilityToken = sendsToken ? await token() : undefined;
90
97
  if (capabilityToken)
91
98
  headers['X-MCP-Capability-Token'] = capabilityToken;
92
99
  if (current) {
@@ -99,6 +106,7 @@ export function startNativeProxy(options = {}) {
99
106
  let reply;
100
107
  let text = '';
101
108
  let pending = '';
109
+ let scanned = 0;
102
110
  const take = (message) => {
103
111
  if (message.id !== undefined && message.id === body.id && ('result' in message || 'error' in message))
104
112
  reply = message;
@@ -122,10 +130,11 @@ export function startNativeProxy(options = {}) {
122
130
  if (text.length < 2000)
123
131
  text += chunk;
124
132
  pending += chunk.replace(/\r/g, '');
125
- for (let cut = pending.indexOf('\n\n'); cut >= 0; cut = pending.indexOf('\n\n')) {
133
+ for (let cut = pending.indexOf('\n\n', scanned); cut >= 0; cut = pending.indexOf('\n\n')) {
126
134
  takeEvent(pending.slice(0, cut));
127
135
  pending = pending.slice(cut + 2);
128
136
  }
137
+ scanned = Math.max(0, pending.length - 1);
129
138
  });
130
139
  res.on('end', () => {
131
140
  if (sse && pending.trim())
@@ -8,6 +8,12 @@ function normalizeLogLevel(value, fallback) {
8
8
  const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
9
9
  return Object.hasOwn(LOG_LEVEL_ORDER, normalized) ? normalized : fallback;
10
10
  }
11
+ export function oneLine(text) {
12
+ return Array.from(text, (c) => {
13
+ const code = c.charCodeAt(0);
14
+ return (code < 0x20 && c !== '\t') || code === 0x7f ? `\\u${code.toString(16).padStart(4, '0')}` : c;
15
+ }).join('');
16
+ }
11
17
  export class Logger {
12
18
  scope;
13
19
  level;
@@ -20,7 +26,7 @@ export class Logger {
20
26
  }
21
27
  write(level, args) {
22
28
  if (this.isEnabled(level))
23
- console.error(`[${this.scope}]`, ...args);
29
+ console.error(`[${this.scope}]`, ...args.map((arg) => typeof arg === 'string' ? oneLine(arg) : arg));
24
30
  }
25
31
  debug(...args) { this.write('debug', args); }
26
32
  info(...args) { this.write('info', args); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unreal-engine-mcp-server",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta-b",
4
4
  "mcpName": "io.github.ChiR24/unreal-engine-mcp",
5
5
  "description": "EngineWire: an MCP server and Unreal Editor plugin that let AI assistants build in Unreal Engine 5.0-5.8: levels, Blueprints, UI, materials, effects and more.",
6
6
  "homepage": "https://enginewire.dev",
package/server.json CHANGED
@@ -3,13 +3,13 @@
3
3
  "name": "io.github.ChiR24/unreal-engine-mcp",
4
4
  "description": "EngineWire: drive the Unreal Editor (UE 5.0-5.8): levels, Blueprints, UI, materials, VFX and more.",
5
5
  "websiteUrl": "https://enginewire.dev",
6
- "version": "0.6.0",
6
+ "version": "0.6.1-beta-b",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "registryBaseUrl": "https://registry.npmjs.org",
11
11
  "identifier": "unreal-engine-mcp-server",
12
- "version": "0.6.0",
12
+ "version": "0.6.1-beta-b",
13
13
  "transport": {
14
14
  "type": "stdio"
15
15
  },