ultimate-unreal-engine-mcp 0.1.21 → 0.1.23

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
@@ -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)
@@ -543,6 +544,51 @@ ue_cleanup_screenshots(confirm=true)
543
544
 
544
545
  ---
545
546
 
547
+ ## Automatic Self-Verification
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.
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 spawned
558
+
559
+ ue_get_component_bounds("BP_Door", component="DoorMesh") ← automatic
560
+ → bounds: X=[806, 994] ✓ — in the doorway
561
+
562
+ 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
570
+
571
+ ue_get_component_bounds("BP_Door", component="DoorMesh") ← re-verify
572
+ → bounds: X=[806, 994] ✓ — back in place
573
+
574
+ ue_look_at("BP_Door", angle="front") ← visual confirm
575
+ → [screenshot inline] door visible, handle correct side ✓
576
+ ```
577
+
578
+ **The Inspect-Reason-Fix cycle:**
579
+
580
+ | Step | What happens | Tool |
581
+ |------|-------------|------|
582
+ | **Act** | Spawn, transform, or set property | `ue_spawn_actor`, `ue_transform_actor`, `ue_set_actor_property` |
583
+ | **Measure** | Get exact numerical positions | `ue_get_component_bounds` |
584
+ | **Reason** | Do bounds match expectations? | (AI reasoning) |
585
+ | **Fix** | Adjust offsets to compensate | Same mutation tools |
586
+ | **Verify** | Confirm fix worked, visually and numerically | `ue_get_component_bounds` + `ue_look_at` |
587
+
588
+ 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.
589
+
590
+ ---
591
+
546
592
  ## Headless vs Live
547
593
 
548
594
  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');
@@ -85,7 +85,7 @@ export function registerEditorTools(server, bridge) {
85
85
  // --------------------------------------------------------------------------
86
86
  server.registerTool('ue_spawn_actor', {
87
87
  title: 'Spawn UE Actor',
88
- description: '[requires_plugin] Spawn a new actor in the currently open UE Editor level at the specified location.',
88
+ 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
89
  inputSchema: z.object({
90
90
  class_name: z.string().describe('The actor class to spawn (e.g., AStaticMeshActor)'),
91
91
  location: z.object({
@@ -114,7 +114,7 @@ export function registerEditorTools(server, bridge) {
114
114
  // --------------------------------------------------------------------------
115
115
  server.registerTool('ue_set_actor_property', {
116
116
  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).',
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). After setting spatial properties (transforms, offsets, scale), verify the result with ue_get_component_bounds before reporting success.',
118
118
  inputSchema: z.object({
119
119
  actor_label: z.string().describe('The editor label of the target actor'),
120
120
  property_name: z.string().describe('The UPROPERTY name to set (e.g., RoomID, RoomDoor, DoorCurve)'),
@@ -146,7 +146,7 @@ export function registerEditorTools(server, bridge) {
146
146
  // --------------------------------------------------------------------------
147
147
  server.registerTool('ue_create_data_asset', {
148
148
  title: 'Create Data Asset',
149
- description: '[requires_plugin] Create a UPrimaryDataAsset (or subclass) at the given path with reflection-set properties.',
149
+ 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
150
  inputSchema: z.object({
151
151
  asset_path: z.string().describe('UE asset path, e.g. /Game/Data/DA_ValveHandle'),
152
152
  class_name: z.string().describe('Asset class name, e.g. ItemDefinition'),
@@ -171,7 +171,7 @@ export function registerEditorTools(server, bridge) {
171
171
  // --------------------------------------------------------------------------
172
172
  server.registerTool('ue_create_curve', {
173
173
  title: 'Create Float Curve',
174
- description: '[requires_plugin] Create a UCurveFloat asset with keyframes at the given path.',
174
+ description: '[requires_plugin] Create a UCurveFloat asset with keyframes at the given path. After creation, verify the asset exists with ue_query_assets.',
175
175
  inputSchema: z.object({
176
176
  asset_path: z.string().describe('UE asset path, e.g. /Game/Curves/C_DoorSwing'),
177
177
  keys: z.array(z.object({
@@ -195,7 +195,7 @@ export function registerEditorTools(server, bridge) {
195
195
  // --------------------------------------------------------------------------
196
196
  server.registerTool('ue_transform_actor', {
197
197
  title: 'Transform UE Actor',
198
- description: '[requires_plugin] Move, rotate, and/or scale an existing actor in the currently open UE Editor level.',
198
+ 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
199
  inputSchema: z.object({
200
200
  actor_label: z.string().describe('The editor label of the actor to transform'),
201
201
  location: z
@@ -242,7 +242,7 @@ export function registerEditorTools(server, bridge) {
242
242
  // --------------------------------------------------------------------------
243
243
  server.registerTool('ue_delete_actor', {
244
244
  title: 'Delete UE Actor',
245
- description: '[requires_plugin] Delete an actor from the currently open UE Editor level. This operation is destructive.',
245
+ 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
246
  inputSchema: z.object({
247
247
  actor_label: z.string().describe('The editor label of the actor to delete'),
248
248
  }),
@@ -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.21",
3
+ "version": "0.1.23",
4
4
  "description": "MCP server giving AI assistants full access to Unreal Engine 5.7 projects",
5
5
  "type": "module",
6
6
  "engines": {
@@ -208,51 +208,24 @@ static FString TakeScreenshotToFile(int32 Width, int32 Height)
208
208
  return FString();
209
209
  }
210
210
 
211
- // Enable realtime rendering so the viewport actually renders frames.
211
+ // Enable realtime rendering so the viewport continuously renders frames.
212
212
  // Without this, the editor viewport only renders when "dirty" and
213
- // ReadPixels captures a stale/blank framebuffer.
214
- const bool bWasRealtime = ViewportClient->IsRealtime();
213
+ // FScreenshotRequest never gets picked up on subsequent calls.
215
214
  ViewportClient->SetRealtime(true);
216
215
 
217
- // Force a full viewport redraw. Invalidate marks it dirty,
218
- // then Draw executes the render pipeline synchronously.
219
- ViewportClient->Viewport->Invalidate();
220
- ViewportClient->Viewport->Draw();
221
- FlushRenderingCommands();
222
-
223
- // Read pixels from the viewport framebuffer.
224
- TArray<FColor> Pixels;
225
- const bool bReadOk = ViewportClient->Viewport->ReadPixels(Pixels);
226
-
227
- // Restore previous realtime state.
228
- if (!bWasRealtime)
229
- {
230
- ViewportClient->SetRealtime(false);
231
- }
232
-
233
- if (!bReadOk)
234
- {
235
- return FString();
236
- }
237
-
238
- const int32 VPWidth = ViewportClient->Viewport->GetSizeXY().X;
239
- const int32 VPHeight = ViewportClient->Viewport->GetSizeXY().Y;
240
-
241
- if (Pixels.Num() != VPWidth * VPHeight || VPWidth == 0 || VPHeight == 0)
242
- {
243
- return FString();
244
- }
245
-
246
- // Compress to PNG.
247
- TArray64<uint8> PngData;
248
- FImageUtils::PNGCompressImageArray(VPWidth, VPHeight, Pixels, PngData);
249
-
250
- // Write to file.
251
216
  const FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S_%s"));
252
217
  const FString FileName = FString::Printf(TEXT("mcp_screenshot_%s.png"), *Timestamp);
253
218
  const FString FilePath = FPaths::Combine(GetMCPScreenshotDir(), FileName);
254
219
 
255
- FFileHelper::SaveArrayToFile(PngData, *FilePath);
220
+ GScreenshotResolutionX = Width;
221
+ GScreenshotResolutionY = Height;
222
+
223
+ // Invalidate to ensure the viewport redraws with current camera state.
224
+ ViewportClient->Viewport->Invalidate();
225
+
226
+ // Request screenshot to a specific file path. With realtime enabled,
227
+ // the renderer will process this on the next frame.
228
+ FScreenshotRequest::RequestScreenshot(FilePath, false /* bShowUI */, false /* bAddFilenameSuffix */);
256
229
 
257
230
  return FilePath;
258
231
  }
@@ -361,7 +334,7 @@ void RegisterViewportCommands(FMCPCommandRouter& Router)
361
334
  Data->SetStringField(TEXT("file_path"), FilePath);
362
335
  Data->SetNumberField(TEXT("width"), static_cast<double>(Width));
363
336
  Data->SetNumberField(TEXT("height"), static_cast<double>(Height));
364
- Data->SetBoolField(TEXT("saved"), true);
337
+ Data->SetBoolField(TEXT("async"), true);
365
338
 
366
339
  SendResponse(BuildViewportSuccessResponse(CorrId, Data) + TEXT("\n"));
367
340
  });