ultimate-unreal-engine-mcp 0.1.23 → 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
 
@@ -52,7 +52,7 @@ npx ultimate-unreal-engine-mcp setup --project "C:/MyGame"
52
52
 
53
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).
54
54
 
55
- > **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.
56
56
 
57
57
  ---
58
58
 
@@ -546,7 +546,7 @@ ue_cleanup_screenshots(confirm=true)
546
546
 
547
547
  ## Automatic Self-Verification
548
548
 
549
- Every mutating tool description includes **mandatory verification guidance** the AI is instructed to verify its own work before reporting success. This isn't a suggestion; it's baked into the tool interface.
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
550
 
551
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
552
 
@@ -554,24 +554,18 @@ Every mutating tool description includes **mandatory verification guidance** —
554
554
 
555
555
  ```
556
556
  ue_spawn_actor("BP_Door", location={x:1040, y:150, z:0})
557
- Actor spawned
558
-
559
- ue_get_component_bounds("BP_Door", component="DoorMesh") ← automatic
560
- → bounds: X=[806, 994] ✓ — in the doorway
557
+ {actor_label: "BP_Door", _verification: {bounds: {min: {x:806}, max: {x:994}}}}
558
+ ← bounds auto-appended by server — in the doorway ✓
561
559
 
562
560
  ue_set_actor_property("BP_Door", "DoorMesh.RelativeRotation", ...)
563
- Property set
564
-
565
- ue_get_component_bounds("BP_Door", component="DoorMesh") ← automatic
566
- → bounds: X=[1040, 1228] ✗ — rotation shifted mesh out of gap
567
-
568
- ue_set_actor_property("BP_Door", "DoorMesh.RelativeLocation", ...) ← self-correction
569
- → Offset compensated
561
+ {success: true, _verification: {bounds: {min: {x:1040}, max: {x:1228}}}}
562
+ ← bounds shifted! rotation moved mesh out of gap ✗
570
563
 
571
- ue_get_component_bounds("BP_Door", component="DoorMesh") ← re-verify
572
- bounds: X=[806, 994] back in place
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 ✓
573
567
 
574
- ue_look_at("BP_Door", angle="front") ← visual confirm
568
+ ue_look_at("BP_Door", angle="front")
575
569
  → [screenshot inline] door visible, handle correct side ✓
576
570
  ```
577
571
 
@@ -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
  /**
@@ -104,10 +137,22 @@ 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
@@ -136,10 +181,12 @@ 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
@@ -235,7 +282,9 @@ 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
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-unreal-engine-mcp",
3
- "version": "0.1.23",
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": {