ultimate-unreal-engine-mcp 0.1.22 → 0.1.24

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # Ultimate Unreal Engine MCP
2
2
 
3
- **132 tools. 26 domains. Full read/write access to Unreal Engine 5.7 — including live editor APIs, Blueprint graphs, and viewport screenshots.**
3
+ **133 tools. 26 domains. Full read/write access to Unreal Engine 5.7 — including live editor APIs, Blueprint graphs, and viewport screenshots.**
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
  [![Node 22+](https://img.shields.io/badge/Node.js-22%2B-brightgreen.svg)](https://nodejs.org/)
7
7
  [![UE 5.7](https://img.shields.io/badge/Unreal%20Engine-5.7-orange.svg)](https://www.unrealengine.com/)
8
8
  [![Tests](https://img.shields.io/badge/tests-1021%20passing-success.svg)](#development)
9
- [![Tools](https://img.shields.io/badge/tools-132-informational.svg)](#tool-catalog)
9
+ [![Tools](https://img.shields.io/badge/tools-133-informational.svg)](#tool-catalog)
10
10
 
11
11
  MCP (Model Context Protocol) is a standard that lets AI assistants call tools on external systems — this server exposes Unreal Engine as those tools.
12
12
 
@@ -23,6 +23,7 @@ npx ultimate-unreal-engine-mcp setup
23
23
  - [Architecture](#architecture)
24
24
  - [Tool Catalog](#tool-catalog)
25
25
  - [Visual Review Loop](#visual-review-loop)
26
+ - [Automatic Self-Verification](#automatic-self-verification)
26
27
  - [Headless vs Live](#headless-vs-live)
27
28
  - [Installation](#installation)
28
29
  - [C++ Plugin Setup](#c-plugin-setup)
@@ -51,7 +52,7 @@ npx ultimate-unreal-engine-mcp setup --project "C:/MyGame"
51
52
 
52
53
  This command writes the correct config for your client. The C++ plugin is optional — file-system tools (C++ parsing, code gen, configs, builds, docs) work without it. Install the plugin for live editor tools (Blueprints, actors, viewport, screenshots).
53
54
 
54
- > **Zero optional dependencies.** The C++ plugin compiles on any UE 5.7 install — binary or source. All 129 tools use runtime reflection for optional UE modules (MetaSound, LiveLink, GAS, etc.), so the plugin loads regardless of which plugins your project has enabled. Handlers gracefully report when their target module isn't available.
55
+ > **Zero optional dependencies.** The C++ plugin compiles on any UE 5.7 install — binary or source. All 133 tools use runtime reflection for optional UE modules (MetaSound, LiveLink, GAS, etc.), so the plugin loads regardless of which plugins your project has enabled. Handlers gracefully report when their target module isn't available.
55
56
 
56
57
  ---
57
58
 
@@ -543,6 +544,45 @@ ue_cleanup_screenshots(confirm=true)
543
544
 
544
545
  ---
545
546
 
547
+ ## Automatic Self-Verification
548
+
549
+ Verification isn't a suggestion. It's **server-side enforced**. Every mutating tool automatically queries `actor.componentBounds` after the mutation completes and appends the verification data to the response. The AI sees bounds, positions, and component state in the same tool call result, whether it asked for it or not. If something is wrong, the data is right there and impossible to miss.
550
+
551
+ **Why this matters:** In spatial work (placing doors, aligning meshes, wiring components), a single transform can have cascading effects — rotating a mesh around a pivot shifts its world position, negative scale flips face normals making geometry invisible, collision volumes don't automatically follow mesh offsets. Without verification, the AI reports "done" while the door is floating 2 meters from the wall.
552
+
553
+ **How it works:**
554
+
555
+ ```
556
+ ue_spawn_actor("BP_Door", location={x:1040, y:150, z:0})
557
+ → {actor_label: "BP_Door", _verification: {bounds: {min: {x:806}, max: {x:994}}}}
558
+ ← bounds auto-appended by server — in the doorway ✓
559
+
560
+ ue_set_actor_property("BP_Door", "DoorMesh.RelativeRotation", ...)
561
+ → {success: true, _verification: {bounds: {min: {x:1040}, max: {x:1228}}}}
562
+ ← bounds shifted! rotation moved mesh out of gap ✗
563
+
564
+ ue_set_actor_property("BP_Door", "DoorMesh.RelativeLocation", ...)
565
+ → {success: true, _verification: {bounds: {min: {x:806}, max: {x:994}}}}
566
+ ← back in place after offset compensation ✓
567
+
568
+ ue_look_at("BP_Door", angle="front")
569
+ → [screenshot inline] door visible, handle correct side ✓
570
+ ```
571
+
572
+ **The Inspect-Reason-Fix cycle:**
573
+
574
+ | Step | What happens | Tool |
575
+ |------|-------------|------|
576
+ | **Act** | Spawn, transform, or set property | `ue_spawn_actor`, `ue_transform_actor`, `ue_set_actor_property` |
577
+ | **Measure** | Get exact numerical positions | `ue_get_component_bounds` |
578
+ | **Reason** | Do bounds match expectations? | (AI reasoning) |
579
+ | **Fix** | Adjust offsets to compensate | Same mutation tools |
580
+ | **Verify** | Confirm fix worked, visually and numerically | `ue_get_component_bounds` + `ue_look_at` |
581
+
582
+ The AI also has access to a `ue://best-practices/self-verification` MCP resource that documents verification patterns, common spatial pitfalls, and the full verification matrix.
583
+
584
+ ---
585
+
546
586
  ## Headless vs Live
547
587
 
548
588
  Some tools work without the UE Editor running. Others require the MCPBridge plugin connected to a live editor instance.
package/dist/index.js CHANGED
@@ -77,6 +77,69 @@ import { registerNetworkingTools } from './tools/networking/index.js';
77
77
  registerMotionDesignTools(server); // Motion Design tools (MD-01..04)
78
78
  registerMovieRenderTools(server); // Movie Render Pipeline tools (MRP-01..04)
79
79
  registerNetworkingTools(server); // Networking & Replication tools (NET-01..04)
80
+ // ---------------------------------------------------------------------------
81
+ // Register best-practices resource: self-verification guide
82
+ // ---------------------------------------------------------------------------
83
+ server.registerResource('ue_best_practices', 'ue://best-practices/self-verification', {
84
+ title: 'UE MCP Best Practices: Self-Verification',
85
+ description: 'Guidelines for AI assistants using UE MCP tools. Covers mandatory self-verification after mutations, spatial reasoning patterns, and the inspect-reason-fix cycle.',
86
+ mimeType: 'text/markdown',
87
+ }, async (_uri) => ({
88
+ contents: [{
89
+ uri: 'ue://best-practices/self-verification',
90
+ mimeType: 'text/markdown',
91
+ text: `# UE MCP Best Practices: Self-Verification
92
+
93
+ ## Core Principle
94
+
95
+ **Always verify your own work before reporting success to the user.**
96
+
97
+ Every mutation (spawn, transform, property set, asset creation) must be followed by a verification step. Never ask the user to "check if that looks right" when you have tools to check it yourself.
98
+
99
+ ## Verification Matrix
100
+
101
+ | After this action... | Verify with... |
102
+ |---|---|
103
+ | \`ue_spawn_actor\` | \`ue_get_component_bounds\` to confirm position, then \`ue_look_at\` to visually confirm |
104
+ | \`ue_transform_actor\` | \`ue_get_component_bounds\` — rotations can shift meshes relative to pivots |
105
+ | \`ue_set_actor_property\` (spatial) | \`ue_get_component_bounds\` for position/scale, \`ue_visual_review\` for visual properties |
106
+ | \`ue_set_actor_property\` (material/mesh) | \`ue_look_at\` or \`ue_visual_review\` to confirm visual result |
107
+ | \`ue_delete_actor\` | \`ue_list_actors\` to confirm removal |
108
+ | \`ue_create_data_asset\` / \`ue_create_curve\` | \`ue_query_assets\` to confirm creation |
109
+ | Any visual change | \`ue_visual_review\` or \`ue_look_at\` to see the result |
110
+
111
+ ## The Inspect-Reason-Fix Cycle
112
+
113
+ When working with spatial placement (actors, components, doors, furniture):
114
+
115
+ 1. **Act** — spawn, transform, or set property
116
+ 2. **Measure** — \`ue_get_component_bounds\` to get exact numerical positions
117
+ 3. **Reason** — do the bounds match expectations? Are components where they should be?
118
+ 4. **Fix** — if not, adjust offsets/rotations to compensate
119
+ 5. **Verify** — re-measure to confirm the fix worked
120
+
121
+ Repeat until all components are correctly positioned. Only THEN report success to the user.
122
+
123
+ ## Common Spatial Pitfalls
124
+
125
+ - **Negative scale flips normals** — use 180-degree rotation instead of -1 scale to flip meshes
126
+ - **Rotations shift mesh position** — rotating around a pivot moves the mesh; always check bounds after rotation
127
+ - **Component offsets are relative** — a rotated parent changes where child offsets end up in world space
128
+ - **Collision doesn't auto-follow mesh** — if you move/rotate a mesh component, the collision component may need separate adjustment
129
+
130
+ ## Visual Verification
131
+
132
+ For any change that affects what the user will SEE in the viewport:
133
+ 1. Use \`ue_look_at\` to navigate the camera to the changed actor
134
+ 2. Inspect the screenshot — check for: missing meshes, wrong orientation, clipping, z-fighting, scale issues
135
+ 3. If something looks wrong, fix it BEFORE telling the user
136
+
137
+ ## Cleanup
138
+
139
+ When a visual review session is complete, call \`ue_cleanup_screenshots(confirm=true)\` to free disk space.
140
+ `,
141
+ }],
142
+ }));
80
143
  const transport = new StdioServerTransport();
81
144
  await server.connect(transport);
82
145
  log('Server ready — connected via stdio');
@@ -43,6 +43,39 @@ async function sendOrDisconnect(bridge, cmd) {
43
43
  }
44
44
  }
45
45
  // ---------------------------------------------------------------------------
46
+ // autoVerifyBounds helper
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * Queries actor.componentBounds after a mutation and appends verification
50
+ * data to the original result. If the verification call fails, the original
51
+ * result is returned unmodified (verification is best-effort).
52
+ */
53
+ async function autoVerifyBounds(bridge, actorLabel, originalResult) {
54
+ // Don't verify if the original command failed
55
+ if (originalResult.isError)
56
+ return originalResult;
57
+ try {
58
+ const verifyResponse = await bridge.sendCommand({
59
+ type: 'actor.componentBounds',
60
+ payload: { actor_label: actorLabel },
61
+ correlationId: '',
62
+ });
63
+ const verifyData = verifyResponse.success
64
+ ? { _verification: { actor_label: actorLabel, bounds: verifyResponse.data } }
65
+ : { _verification: { actor_label: actorLabel, error: verifyResponse.error } };
66
+ return {
67
+ content: [
68
+ ...originalResult.content,
69
+ { type: 'text', text: JSON.stringify(verifyData) },
70
+ ],
71
+ };
72
+ }
73
+ catch {
74
+ // Verification is best-effort; don't break the original result
75
+ return originalResult;
76
+ }
77
+ }
78
+ // ---------------------------------------------------------------------------
46
79
  // registerEditorTools
47
80
  // ---------------------------------------------------------------------------
48
81
  /**
@@ -85,7 +118,7 @@ export function registerEditorTools(server, bridge) {
85
118
  // --------------------------------------------------------------------------
86
119
  server.registerTool('ue_spawn_actor', {
87
120
  title: 'Spawn UE Actor',
88
- description: '[requires_plugin] Spawn a new actor in the currently open UE Editor level at the specified location.',
121
+ description: '[requires_plugin] Spawn a new actor in the currently open UE Editor level at the specified location. After spawning, ALWAYS verify placement with ue_get_component_bounds or ue_look_at before reporting success to the user.',
89
122
  inputSchema: z.object({
90
123
  class_name: z.string().describe('The actor class to spawn (e.g., AStaticMeshActor)'),
91
124
  location: z.object({
@@ -104,17 +137,29 @@ export function registerEditorTools(server, bridge) {
104
137
  if (args.label) {
105
138
  payload['label'] = args.label;
106
139
  }
107
- return sendOrDisconnect(_bridge, {
140
+ const result = await sendOrDisconnect(_bridge, {
108
141
  type: 'actor.spawn',
109
142
  payload,
110
143
  });
144
+ // Auto-verify: query bounds of the spawned actor
145
+ const label = args.label ?? (result.content?.[0] && 'text' in result.content[0]
146
+ ? (() => { try {
147
+ return JSON.parse(result.content[0].text)?.actor_label;
148
+ }
149
+ catch {
150
+ return undefined;
151
+ } })()
152
+ : undefined);
153
+ if (label)
154
+ return autoVerifyBounds(_bridge, label, result);
155
+ return result;
111
156
  }));
112
157
  // --------------------------------------------------------------------------
113
158
  // ue_set_actor_property
114
159
  // --------------------------------------------------------------------------
115
160
  server.registerTool('ue_set_actor_property', {
116
161
  title: 'Set Actor Property',
117
- description: '[requires_plugin] Set a UPROPERTY value on an actor by label. Supports bool, int, float, string, name, text, actor (reference by label), and asset (reference by path).',
162
+ description: '[requires_plugin] Set a UPROPERTY value on an actor by label. Supports bool, int, float, string, name, text, actor (reference by label), and asset (reference by path). After setting spatial properties (transforms, offsets, scale), verify the result with ue_get_component_bounds before reporting success.',
118
163
  inputSchema: z.object({
119
164
  actor_label: z.string().describe('The editor label of the target actor'),
120
165
  property_name: z.string().describe('The UPROPERTY name to set (e.g., RoomID, RoomDoor, DoorCurve)'),
@@ -136,17 +181,19 @@ export function registerEditorTools(server, bridge) {
136
181
  if (args.component_name) {
137
182
  payload['component_name'] = args.component_name;
138
183
  }
139
- return sendOrDisconnect(_bridge, {
184
+ const result = await sendOrDisconnect(_bridge, {
140
185
  type: 'actor.setProperty',
141
186
  payload,
142
187
  });
188
+ // Auto-verify: query bounds after property change
189
+ return autoVerifyBounds(_bridge, args.actor_label, result);
143
190
  }));
144
191
  // --------------------------------------------------------------------------
145
192
  // ue_create_data_asset
146
193
  // --------------------------------------------------------------------------
147
194
  server.registerTool('ue_create_data_asset', {
148
195
  title: 'Create Data Asset',
149
- description: '[requires_plugin] Create a UPrimaryDataAsset (or subclass) at the given path with reflection-set properties.',
196
+ description: '[requires_plugin] Create a UPrimaryDataAsset (or subclass) at the given path with reflection-set properties. After creation, verify the asset exists with ue_query_assets.',
150
197
  inputSchema: z.object({
151
198
  asset_path: z.string().describe('UE asset path, e.g. /Game/Data/DA_ValveHandle'),
152
199
  class_name: z.string().describe('Asset class name, e.g. ItemDefinition'),
@@ -171,7 +218,7 @@ export function registerEditorTools(server, bridge) {
171
218
  // --------------------------------------------------------------------------
172
219
  server.registerTool('ue_create_curve', {
173
220
  title: 'Create Float Curve',
174
- description: '[requires_plugin] Create a UCurveFloat asset with keyframes at the given path.',
221
+ description: '[requires_plugin] Create a UCurveFloat asset with keyframes at the given path. After creation, verify the asset exists with ue_query_assets.',
175
222
  inputSchema: z.object({
176
223
  asset_path: z.string().describe('UE asset path, e.g. /Game/Curves/C_DoorSwing'),
177
224
  keys: z.array(z.object({
@@ -195,7 +242,7 @@ export function registerEditorTools(server, bridge) {
195
242
  // --------------------------------------------------------------------------
196
243
  server.registerTool('ue_transform_actor', {
197
244
  title: 'Transform UE Actor',
198
- description: '[requires_plugin] Move, rotate, and/or scale an existing actor in the currently open UE Editor level.',
245
+ description: '[requires_plugin] Move, rotate, and/or scale an existing actor in the currently open UE Editor level. IMPORTANT: After transforming, always verify the result with ue_get_component_bounds (numerical check) or ue_look_at/ue_visual_review (visual check) before telling the user it worked. Rotations can shift meshes relative to pivots — always confirm final bounds.',
199
246
  inputSchema: z.object({
200
247
  actor_label: z.string().describe('The editor label of the actor to transform'),
201
248
  location: z
@@ -235,14 +282,16 @@ export function registerEditorTools(server, bridge) {
235
282
  payload['rotation'] = args.rotation;
236
283
  if (args.scale)
237
284
  payload['scale'] = args.scale;
238
- return sendOrDisconnect(_bridge, { type: 'actor.transform', payload });
285
+ const result = await sendOrDisconnect(_bridge, { type: 'actor.transform', payload });
286
+ // Auto-verify: query bounds after transform
287
+ return autoVerifyBounds(_bridge, args.actor_label, result);
239
288
  }));
240
289
  // --------------------------------------------------------------------------
241
290
  // ue_delete_actor
242
291
  // --------------------------------------------------------------------------
243
292
  server.registerTool('ue_delete_actor', {
244
293
  title: 'Delete UE Actor',
245
- description: '[requires_plugin] Delete an actor from the currently open UE Editor level. This operation is destructive.',
294
+ description: '[requires_plugin] Delete an actor from the currently open UE Editor level. This operation is destructive. After deleting, verify with ue_list_actors that the actor is gone.',
246
295
  inputSchema: z.object({
247
296
  actor_label: z.string().describe('The editor label of the actor to delete'),
248
297
  }),
@@ -251,10 +300,30 @@ export function registerEditorTools(server, bridge) {
251
300
  destructiveHint: true,
252
301
  },
253
302
  }, withKnownIssues('ue_delete_actor', async (args) => {
254
- return sendOrDisconnect(_bridge, {
303
+ const result = await sendOrDisconnect(_bridge, {
255
304
  type: 'actor.delete',
256
305
  payload: { actor_label: args.actor_label },
257
306
  });
307
+ // Auto-verify: confirm actor no longer exists
308
+ if (!result.isError) {
309
+ try {
310
+ const verifyResponse = await _bridge.sendCommand({
311
+ type: 'actor.list',
312
+ correlationId: '',
313
+ });
314
+ if (verifyResponse.success) {
315
+ const actors = verifyResponse.data;
316
+ return {
317
+ content: [
318
+ ...result.content,
319
+ { type: 'text', text: JSON.stringify({ _verification: { deleted: args.actor_label, remaining_actors: actors } }) },
320
+ ],
321
+ };
322
+ }
323
+ }
324
+ catch { /* verification is best-effort */ }
325
+ }
326
+ return result;
258
327
  }));
259
328
  // --------------------------------------------------------------------------
260
329
  // ue_query_assets
@@ -325,4 +394,25 @@ export function registerEditorTools(server, bridge) {
325
394
  }, withKnownIssues('ue_read_level_layout', async (_args) => {
326
395
  return sendOrDisconnect(_bridge, { type: 'level.layout' });
327
396
  }));
397
+ // --------------------------------------------------------------------------
398
+ // ue_get_component_bounds
399
+ // --------------------------------------------------------------------------
400
+ server.registerTool('ue_get_component_bounds', {
401
+ title: 'Get Actor Component Bounds',
402
+ description: '[requires_plugin] Get the world-space bounding box and transform of an actor and its components. Use this to verify spatial placement after spawning, transforming, or setting properties. Returns min/max bounds, center, and extent for each component.',
403
+ inputSchema: z.object({
404
+ actor_label: z.string().describe('The editor label of the actor to inspect'),
405
+ component_name: z.string().optional().describe('Optional: inspect a specific component only (e.g., DoorMesh, DoorCollision)'),
406
+ }),
407
+ annotations: {
408
+ readOnlyHint: true,
409
+ destructiveHint: false,
410
+ },
411
+ }, withKnownIssues('ue_get_component_bounds', async (args) => {
412
+ const payload = { actor_label: args.actor_label };
413
+ if (args.component_name) {
414
+ payload['component_name'] = args.component_name;
415
+ }
416
+ return sendOrDisconnect(_bridge, { type: 'actor.componentBounds', payload });
417
+ }));
328
418
  }
@@ -428,7 +428,7 @@ export function registerViewportTools(server, bridge) {
428
428
  // --------------------------------------------------------------------------
429
429
  server.registerTool('ue_visual_review', {
430
430
  title: 'Visual Review Screenshot',
431
- description: '[requires_plugin] Take a quick viewport screenshot for visual review. Returns the image inline with camera context. Use this for iterative review loops.',
431
+ description: '[requires_plugin] Take a quick viewport screenshot for visual review. Returns the image inline with camera context. Use this to VERIFY changes after any mutation (spawn, transform, property set). Always screenshot after making visual changes — do not ask the user to check manually.',
432
432
  inputSchema: z.object({
433
433
  width: z
434
434
  .number()
@@ -662,7 +662,7 @@ export function registerViewportTools(server, bridge) {
662
662
  // --------------------------------------------------------------------------
663
663
  server.registerTool('ue_iterate_scene', {
664
664
  title: 'Full Scene Context Snapshot',
665
- description: '[requires_plugin] Get full visual and semantic context in one call. Takes a screenshot AND returns structured data about nearby actors. Use this to orient yourself — see what you are looking at and what is nearby.',
665
+ description: '[requires_plugin] Get full visual and semantic context in one call. Takes a screenshot AND returns structured data about nearby actors. Use this to orient yourself and to verify complex multi-actor changes — see what you are looking at, what is nearby, and whether everything is in the right place.',
666
666
  inputSchema: z.object({
667
667
  width: z
668
668
  .number()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-unreal-engine-mcp",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "MCP server giving AI assistants full access to Unreal Engine 5.7 projects",
5
5
  "type": "module",
6
6
  "engines": {