blueprint-extractor-mcp 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ const serverInstructions = [
17
17
  'Use the composable material tools for settings, node creation, node connection, and root-property binding. Treat modify_material and modify_material_function as advanced escape hatches.',
18
18
  'Use create_input_action, modify_input_action, create_input_mapping_context, and modify_input_mapping_context for Enhanced Input authoring. Generic data asset mutation is intentionally rejected for those asset classes.',
19
19
  'Verify blueprint wiring, layout data, and authored values semantically with the existing extract_* tools before relying on screenshots.',
20
- 'After widget or UI-heavy mutations, use capture_widget_preview for visual confirmation in addition to extract_widget_blueprint.',
20
+ 'After widget or other user-facing UI mutations, treat the task as incomplete until capture_widget_preview succeeds or you explicitly report partial verification with the blocking reason.',
21
21
  'Use run_automation_tests for gameplay or runtime verification. If no Automation Spec or Functional Test exists for a mechanic, report verification as partial instead of inferring success from structure alone.',
22
22
  'Successful tool results mirror the same JSON in structuredContent and text. Recoverable execution failures return isError=true with code, message, recoverable, and next_steps.',
23
23
  ].join('\n');
@@ -38,11 +38,12 @@ const taskAwareTools = new Set([
38
38
  ]);
39
39
  export const exampleCatalog = {
40
40
  widget_blueprint: {
41
- summary: 'Inspect the current widget, apply the smallest structural change that solves the layout problem, compile, then save.',
41
+ summary: 'Inspect the current widget, apply the smallest structural change that solves the layout problem, compile, visually confirm the rendered result, then save.',
42
42
  recommended_flow: [
43
43
  'extract_widget_blueprint',
44
44
  'modify_widget_blueprint',
45
45
  'compile_widget_blueprint',
46
+ 'capture_widget_preview',
46
47
  'save_assets',
47
48
  ],
48
49
  examples: [
@@ -168,11 +169,12 @@ export const exampleCatalog = {
168
169
  ],
169
170
  },
170
171
  window_ui_polish: {
171
- summary: 'Use the thin sequencing helper when a screen change touches variable flags, class defaults, compile/save, and optional code sync in one flow.',
172
+ summary: 'Use the thin sequencing helper when a screen change touches variable flags, class defaults, compile, and optional code sync in one flow, then gate persistence on visual confirmation.',
172
173
  recommended_flow: [
173
174
  'extract_widget_blueprint',
174
175
  'apply_window_ui_changes',
175
- 'extract_widget_blueprint',
176
+ 'capture_widget_preview',
177
+ 'save_assets',
176
178
  ],
177
179
  examples: [
178
180
  {
@@ -190,7 +192,7 @@ export const exampleCatalog = {
190
192
  ActiveTitleBarMaterial: '/Game/UI/MI_TitleBarActive.MI_TitleBarActive',
191
193
  },
192
194
  compile_after: true,
193
- save_after: true,
195
+ save_after: false,
194
196
  },
195
197
  },
196
198
  ],
@@ -234,7 +236,8 @@ export const promptCatalog = {
234
236
  parent_class_path ? `Expected parent class: ${parent_class_path}.` : 'Choose the narrowest appropriate parent widget class.',
235
237
  existing_hud_asset_path ? `Inspect the existing HUD first: ${existing_hud_asset_path}.` : 'Inspect the current HUD wiring before replacing the screen.',
236
238
  existing_transition_asset_path ? `Inspect the transition asset first: ${existing_transition_asset_path}.` : 'Inspect transition widgets and activatable-window flow before redesigning layout.',
237
- 'Produce a concrete widget-tree plan, required BindWidget names, class-default changes, and compile/save steps.',
239
+ 'Produce a concrete widget-tree plan, required BindWidget names, class-default changes, and compile/capture/save steps.',
240
+ 'The plan is not complete until it includes capture_widget_preview or an explicit partial verification fallback with a blocking reason.',
238
241
  'Prefer centered_overlay, common_menu_shell, or activatable_window patterns over ad-hoc CanvasPanel placement.',
239
242
  ].join('\n'),
240
243
  },
@@ -280,15 +283,19 @@ export const promptCatalog = {
280
283
  buildPrompt: ({ widget_asset_path, compile_summary_json }) => [
281
284
  `Debug WidgetBlueprint compile failures for ${widget_asset_path}.`,
282
285
  compile_summary_json ? `Compile summary:\n${compile_summary_json}` : 'Start by compiling the widget blueprint and inspecting compile diagnostics.',
283
- 'Check for BindWidget type/name mismatches, abstract widget classes in the tree, and stale class-default references.',
284
- 'Return the minimal follow-up extract/modify/compile sequence needed to fix the compile state.',
286
+ 'Check for BindWidget type/name mismatches, abstract widget classes in the tree, stale class-default references, and degraded extraction states such as rootWidget=null with widgetTreeStatus/widgetTreeError.',
287
+ 'If the failure is tied to CommonUI button styling, treat raw UButton background/style fields as unsupported wrapper surfaces and redirect to CommonUI style assets or a project-owned material-backed button base.',
288
+ 'If the patch touched WidthOverride, HeightOverride, MinDesiredHeight, or similar overrides, verify the paired bOverride_* flags are enabled before assuming the write failed.',
289
+ 'Return the minimal follow-up extract/modify/compile sequence needed to fix the compile state, then finish with capture_widget_preview or explicit partial verification if rendering is blocked.',
285
290
  ].join('\n'),
286
291
  },
287
292
  };
288
293
  export function createBlueprintExtractorServer(client = new UEClient(), projectController = new ProjectController(), automationController = new AutomationController()) {
294
+ let cachedProjectAutomationContext = null;
295
+ let lastExternalBuildContext = null;
289
296
  const server = new McpServer({
290
297
  name: 'blueprint-extractor',
291
- version: '2.1.0',
298
+ version: '2.3.0',
292
299
  }, {
293
300
  instructions: serverInstructions,
294
301
  });
@@ -312,9 +319,261 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
312
319
  progress_message: z.string().optional(),
313
320
  }).optional(),
314
321
  }).passthrough();
322
+ const windowUiVerificationSchema = z.object({
323
+ required: z.boolean(),
324
+ status: z.enum(['compile_pending', 'unverified']),
325
+ surface: z.literal('editor_offscreen'),
326
+ recommendedTool: z.literal('capture_widget_preview'),
327
+ partialAllowed: z.boolean(),
328
+ reason: z.string(),
329
+ });
330
+ const applyWindowUiChangesResultSchema = v2ToolResultSchema.extend({
331
+ stoppedAt: z.string().optional(),
332
+ failedAfterStep: z.string().optional(),
333
+ steps: z.array(z.record(z.string(), z.unknown())).optional(),
334
+ verification: windowUiVerificationSchema.optional(),
335
+ }).passthrough();
315
336
  function isRecord(value) {
316
337
  return typeof value === 'object' && value !== null && !Array.isArray(value);
317
338
  }
339
+ function isVerificationSurface(value) {
340
+ return value === 'editor_offscreen'
341
+ || value === 'pie_runtime'
342
+ || value === 'editor_tool_viewport'
343
+ || value === 'external_packaged';
344
+ }
345
+ function inferVerificationSurface(captureType) {
346
+ if (isVerificationSurface(captureType)) {
347
+ return captureType;
348
+ }
349
+ switch (captureType) {
350
+ case 'widget_preview':
351
+ case 'comparison_diff':
352
+ default:
353
+ return 'editor_offscreen';
354
+ }
355
+ }
356
+ function unknownToStringArray(value) {
357
+ if (!Array.isArray(value)) {
358
+ return [];
359
+ }
360
+ return value.filter((entry) => typeof entry === 'string' && entry.length > 0);
361
+ }
362
+ function buildDefaultArtifactScenarioId(payload) {
363
+ const captureType = typeof payload.captureType === 'string' && payload.captureType.length > 0
364
+ ? payload.captureType
365
+ : 'capture';
366
+ const source = typeof payload.assetPath === 'string' && payload.assetPath.length > 0
367
+ ? payload.assetPath
368
+ : typeof payload.captureId === 'string' && payload.captureId.length > 0
369
+ ? payload.captureId
370
+ : 'capture';
371
+ return `${captureType}:${source}`;
372
+ }
373
+ function buildDefaultWorldContext(payload, surface) {
374
+ if (isRecord(payload.worldContext)) {
375
+ return payload.worldContext;
376
+ }
377
+ if (surface === 'editor_offscreen') {
378
+ const context = {
379
+ contextType: 'widget_blueprint',
380
+ renderLane: 'offscreen',
381
+ };
382
+ if (typeof payload.assetPath === 'string' && payload.assetPath.length > 0) {
383
+ context.assetPath = payload.assetPath;
384
+ }
385
+ if (typeof payload.widgetClass === 'string' && payload.widgetClass.length > 0) {
386
+ context.widgetClass = payload.widgetClass;
387
+ }
388
+ return context;
389
+ }
390
+ return undefined;
391
+ }
392
+ function buildDefaultCameraContext(payload, surface) {
393
+ if (isRecord(payload.cameraContext)) {
394
+ return payload.cameraContext;
395
+ }
396
+ const width = typeof payload.width === 'number' ? payload.width : undefined;
397
+ const height = typeof payload.height === 'number' ? payload.height : undefined;
398
+ if (surface === 'editor_offscreen' && (typeof width === 'number' || typeof height === 'number')) {
399
+ return {
400
+ contextType: 'offscreen_widget',
401
+ ...(typeof width === 'number' ? { width } : {}),
402
+ ...(typeof height === 'number' ? { height } : {}),
403
+ };
404
+ }
405
+ return undefined;
406
+ }
407
+ function normalizeVerificationArtifact(payload) {
408
+ const basePayload = isRecord(payload) ? { ...payload } : { data: payload };
409
+ const assetPaths = unknownToStringArray(basePayload.assetPaths);
410
+ const assetPath = typeof basePayload.assetPath === 'string'
411
+ ? basePayload.assetPath
412
+ : assetPaths[0] ?? '';
413
+ const mergedAssetPaths = assetPaths.length > 0
414
+ ? assetPaths
415
+ : assetPath
416
+ ? [assetPath]
417
+ : [];
418
+ const surface = isVerificationSurface(basePayload.surface)
419
+ ? basePayload.surface
420
+ : inferVerificationSurface(basePayload.captureType);
421
+ const worldContext = buildDefaultWorldContext(basePayload, surface);
422
+ const cameraContext = buildDefaultCameraContext(basePayload, surface);
423
+ return {
424
+ ...basePayload,
425
+ assetPath,
426
+ assetPaths: mergedAssetPaths,
427
+ surface,
428
+ scenarioId: typeof basePayload.scenarioId === 'string' && basePayload.scenarioId.length > 0
429
+ ? basePayload.scenarioId
430
+ : buildDefaultArtifactScenarioId({
431
+ ...basePayload,
432
+ assetPath,
433
+ }),
434
+ ...(worldContext ? { worldContext } : {}),
435
+ ...(cameraContext ? { cameraContext } : {}),
436
+ };
437
+ }
438
+ function normalizeVerificationComparison(payload) {
439
+ const basePayload = isRecord(payload) ? payload : {};
440
+ const nested = isRecord(basePayload.comparison) ? basePayload.comparison : {};
441
+ const result = { ...nested };
442
+ const assignString = (key) => {
443
+ const value = typeof nested[key] === 'string'
444
+ ? nested[key]
445
+ : typeof basePayload[key] === 'string'
446
+ ? basePayload[key]
447
+ : undefined;
448
+ if (typeof value === 'string' && value.length > 0) {
449
+ result[key] = value;
450
+ }
451
+ };
452
+ const assignNumber = (key) => {
453
+ const value = typeof nested[key] === 'number'
454
+ ? nested[key]
455
+ : typeof basePayload[key] === 'number'
456
+ ? basePayload[key]
457
+ : undefined;
458
+ if (typeof value === 'number' && Number.isFinite(value)) {
459
+ result[key] = value;
460
+ }
461
+ };
462
+ const assignBoolean = (key) => {
463
+ const value = typeof nested[key] === 'boolean'
464
+ ? nested[key]
465
+ : typeof basePayload[key] === 'boolean'
466
+ ? basePayload[key]
467
+ : undefined;
468
+ if (typeof value === 'boolean') {
469
+ result[key] = value;
470
+ }
471
+ };
472
+ assignString('capturePath');
473
+ assignString('referencePath');
474
+ assignString('diffCaptureId');
475
+ assignString('diffArtifactPath');
476
+ assignNumber('tolerance');
477
+ assignBoolean('pass');
478
+ assignNumber('rmse');
479
+ assignNumber('maxPixelDelta');
480
+ assignNumber('mismatchPixelCount');
481
+ assignNumber('mismatchPercentage');
482
+ return result;
483
+ }
484
+ function isImageMimeType(value) {
485
+ return typeof value === 'string' && value.startsWith('image/');
486
+ }
487
+ function inferAutomationArtifactCaptureType(name, relativePath) {
488
+ const lower = `${name} ${relativePath ?? ''}`.toLowerCase();
489
+ if (lower.includes('diff')) {
490
+ return 'automation_diff';
491
+ }
492
+ if (lower.includes('screenshot') || lower.includes('capture')) {
493
+ return 'automation_screenshot';
494
+ }
495
+ return 'automation_image_artifact';
496
+ }
497
+ function normalizeAutomationVerificationArtifacts(payload) {
498
+ const createdAt = firstDefinedString(payload.completedAt, payload.startedAt) ?? '';
499
+ const runId = firstDefinedString(payload.runId) ?? 'automation';
500
+ const automationFilter = firstDefinedString(payload.automationFilter) ?? runId;
501
+ const target = firstDefinedString(payload.target);
502
+ const projectDir = firstDefinedString(payload.projectDir);
503
+ const existing = Array.isArray(payload.verificationArtifacts)
504
+ ? payload.verificationArtifacts.filter(isRecord)
505
+ : [];
506
+ if (existing.length > 0) {
507
+ return existing.map((artifact) => {
508
+ const normalized = normalizeVerificationArtifact({
509
+ ...artifact,
510
+ surface: isVerificationSurface(artifact.surface) ? artifact.surface : 'pie_runtime',
511
+ captureType: typeof artifact.captureType === 'string' && artifact.captureType.length > 0
512
+ ? artifact.captureType
513
+ : 'automation_image_artifact',
514
+ createdAt: typeof artifact.createdAt === 'string' && artifact.createdAt.length > 0
515
+ ? artifact.createdAt
516
+ : createdAt,
517
+ });
518
+ return {
519
+ ...normalized,
520
+ resourceUri: typeof artifact.resourceUri === 'string' ? artifact.resourceUri : '',
521
+ ...(typeof artifact.mimeType === 'string' ? { mimeType: artifact.mimeType } : {}),
522
+ ...(typeof artifact.relativePath === 'string' ? { relativePath: artifact.relativePath } : {}),
523
+ };
524
+ });
525
+ }
526
+ const artifacts = Array.isArray(payload.artifacts)
527
+ ? payload.artifacts.filter(isRecord)
528
+ : [];
529
+ return artifacts
530
+ .filter((artifact) => isImageMimeType(artifact.mimeType) && typeof artifact.path === 'string')
531
+ .map((artifact, index) => {
532
+ const relativePath = typeof artifact.relativePath === 'string' ? artifact.relativePath : undefined;
533
+ const name = typeof artifact.name === 'string' && artifact.name.length > 0
534
+ ? artifact.name
535
+ : `automation_artifact_${index}`;
536
+ const normalized = normalizeVerificationArtifact({
537
+ captureId: `${runId}:${name}`,
538
+ captureType: inferAutomationArtifactCaptureType(name, relativePath),
539
+ surface: 'pie_runtime',
540
+ scenarioId: `automation:${automationFilter}:${name}`,
541
+ assetPath: '',
542
+ assetPaths: [],
543
+ artifactPath: artifact.path,
544
+ createdAt,
545
+ worldContext: {
546
+ contextType: 'automation_run',
547
+ runId,
548
+ automationFilter,
549
+ ...(target ? { target } : {}),
550
+ ...(projectDir ? { projectDir } : {}),
551
+ ...(typeof payload.nullRhi === 'boolean' ? { nullRhi: payload.nullRhi } : {}),
552
+ reportArtifactName: name,
553
+ ...(relativePath ? { relativePath } : {}),
554
+ },
555
+ cameraContext: {
556
+ contextType: 'automation_report_artifact',
557
+ captureLane: 'automation',
558
+ ...(relativePath ? { relativePath } : {}),
559
+ },
560
+ ...(projectDir ? { projectDir } : {}),
561
+ });
562
+ return {
563
+ ...normalized,
564
+ resourceUri: typeof artifact.resourceUri === 'string' ? artifact.resourceUri : '',
565
+ mimeType: artifact.mimeType,
566
+ ...(relativePath ? { relativePath } : {}),
567
+ };
568
+ });
569
+ }
570
+ function normalizeAutomationRunResult(payload) {
571
+ const basePayload = isRecord(payload) ? { ...payload } : { data: payload };
572
+ return {
573
+ ...basePayload,
574
+ verificationArtifacts: normalizeAutomationVerificationArtifacts(basePayload),
575
+ };
576
+ }
318
577
  function tryParseJsonText(text) {
319
578
  if (!text) {
320
579
  return undefined;
@@ -528,10 +787,32 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
528
787
  output_directory: z.string(),
529
788
  manifest: z.array(cascadeManifestEntrySchema),
530
789
  }).passthrough();
531
- const captureMetadataSchema = z.object({
790
+ const verificationSurfaceSchema = z.enum([
791
+ 'editor_offscreen',
792
+ 'pie_runtime',
793
+ 'editor_tool_viewport',
794
+ 'external_packaged',
795
+ ]);
796
+ const verificationContextSchema = z.record(z.string(), z.unknown());
797
+ const verificationComparisonSchema = z.object({
798
+ capturePath: z.string().optional(),
799
+ referencePath: z.string().optional(),
800
+ tolerance: z.number().min(0).optional(),
801
+ pass: z.boolean().optional(),
802
+ rmse: z.number().min(0).optional(),
803
+ maxPixelDelta: z.number().int().min(0).optional(),
804
+ mismatchPixelCount: z.number().int().min(0).optional(),
805
+ mismatchPercentage: z.number().min(0).optional(),
806
+ diffCaptureId: z.string().optional(),
807
+ diffArtifactPath: z.string().optional(),
808
+ }).passthrough();
809
+ const verificationArtifactSchema = z.object({
532
810
  captureId: z.string(),
533
- captureType: z.enum(['widget_preview', 'comparison_diff']),
811
+ captureType: z.string().min(1),
812
+ surface: verificationSurfaceSchema,
813
+ scenarioId: z.string(),
534
814
  assetPath: z.string(),
815
+ assetPaths: z.array(z.string()),
535
816
  widgetClass: z.string().optional(),
536
817
  captureDirectory: z.string(),
537
818
  artifactPath: z.string(),
@@ -540,11 +821,37 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
540
821
  height: z.number().int().min(1),
541
822
  fileSizeBytes: z.number().int().min(0),
542
823
  createdAt: z.string(),
824
+ worldContext: verificationContextSchema.optional(),
825
+ cameraContext: verificationContextSchema.optional(),
826
+ comparison: verificationComparisonSchema.optional(),
543
827
  projectDir: z.string().optional(),
544
828
  }).passthrough();
829
+ const verificationArtifactReferenceSchema = z.object({
830
+ captureId: z.string(),
831
+ captureType: z.string().min(1),
832
+ surface: verificationSurfaceSchema,
833
+ scenarioId: z.string(),
834
+ assetPath: z.string().optional(),
835
+ assetPaths: z.array(z.string()),
836
+ artifactPath: z.string(),
837
+ resourceUri: z.string(),
838
+ createdAt: z.string(),
839
+ metadataPath: z.string().optional(),
840
+ captureDirectory: z.string().optional(),
841
+ width: z.number().int().min(1).optional(),
842
+ height: z.number().int().min(1).optional(),
843
+ fileSizeBytes: z.number().int().min(0).optional(),
844
+ widgetClass: z.string().optional(),
845
+ worldContext: verificationContextSchema.optional(),
846
+ cameraContext: verificationContextSchema.optional(),
847
+ comparison: verificationComparisonSchema.optional(),
848
+ projectDir: z.string().optional(),
849
+ mimeType: z.string().optional(),
850
+ relativePath: z.string().optional(),
851
+ }).passthrough();
545
852
  const CaptureResultSchema = v2ToolResultSchema.extend({
546
853
  resourceUri: z.string(),
547
- }).merge(captureMetadataSchema).passthrough();
854
+ }).merge(verificationArtifactSchema).passthrough();
548
855
  const CompareCaptureResultSchema = v2ToolResultSchema.extend({
549
856
  capturePath: z.string(),
550
857
  referencePath: z.string(),
@@ -557,11 +864,12 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
557
864
  diffCaptureId: z.string(),
558
865
  diffArtifactPath: z.string(),
559
866
  diffResourceUri: z.string(),
867
+ comparison: verificationComparisonSchema,
560
868
  }).passthrough();
561
869
  const ListCapturesResultSchema = v2ToolResultSchema.extend({
562
870
  assetPathFilter: z.string(),
563
871
  captureCount: z.number().int().min(0),
564
- captures: z.array(captureMetadataSchema),
872
+ captures: z.array(verificationArtifactSchema),
565
873
  }).passthrough();
566
874
  const CleanupCapturesResultSchema = v2ToolResultSchema.extend({
567
875
  deletedCount: z.number().int().min(0),
@@ -606,6 +914,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
606
914
  timeoutMs: z.number().int().positive(),
607
915
  nullRhi: z.boolean(),
608
916
  artifacts: z.array(automationArtifactSchema),
917
+ verificationArtifacts: z.array(verificationArtifactReferenceSchema).optional(),
609
918
  summary: automationRunSummarySchema.optional(),
610
919
  }).passthrough();
611
920
  const AutomationRunListSchema = v2ToolResultSchema.extend({
@@ -695,7 +1004,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
695
1004
  '- modify_widget supports direct widget_name or widget_path patches for one widget.',
696
1005
  '- modify_widget_blueprint is the primary structural API: replace_tree, patch_widget, patch_class_defaults, insert_child, remove_widget, move_widget, wrap_widget, replace_widget_class, batch, or compile.',
697
1006
  '- compile_widget_blueprint validates the asset but still does not save it.',
698
- '- apply_window_ui_changes is a thin MCP helper that sequences variable-flag updates, class defaults, optional font work, compile/save, and optional code sync.',
1007
+ '- apply_window_ui_changes is a thin MCP helper that sequences variable-flag updates, class defaults, optional font work, compile, optional save, and optional code sync.',
699
1008
  '',
700
1009
  'Explicit deferrals:',
701
1010
  '- Blueprint graph authoring is explicit and opt-in via modify_blueprint_graphs; generic arbitrary graph synthesis is still intentionally bounded to targeted graph operations.',
@@ -762,7 +1071,8 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
762
1071
  '- Use the narrowest mutation tool that fits: patch one widget/member first, replace whole trees only when structure is changing broadly.',
763
1072
  '- Keep payloads small by sending only changed fields, not full extracted objects, unless the tool explicitly expects a full replacement payload.',
764
1073
  '- Re-extract after mutation when you need confirmation; do not assume UE normalized fields exactly as sent.',
765
- '- For multi-step widget work, prefer extract_widget_blueprint -> modify_widget_blueprint -> compile_widget_blueprint -> save_assets.',
1074
+ '- For multi-step widget work, prefer extract_widget_blueprint -> modify_widget_blueprint -> compile_widget_blueprint -> capture_widget_preview -> save_assets.',
1075
+ '- If widget preview capture is blocked, report partial verification explicitly instead of treating compile/save as visual proof.',
766
1076
  '- For code orchestration, pass explicit changed_paths to sync_project_code instead of relying on source-control inference.',
767
1077
  ].join('\n'),
768
1078
  }],
@@ -801,6 +1111,11 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
801
1111
  '- Prefer CommonActivatableWidget as the root parent for CommonUI screens and windows.',
802
1112
  '- Prefer VerticalBox, HorizontalBox, Overlay, Border, SizeBox, ScrollBox, and NamedSlot over CanvasPanel unless absolute positioning is required.',
803
1113
  '- Keep styling centralized. Reuse fonts, colors, and button classes instead of repeating large inline property blobs.',
1114
+ '- CommonButtonBase is not a raw UButton surface. Do not target UButton background/style fields through patch_class_defaults or patch_widget on CommonUI wrappers; use a CommonUI style asset or a project-owned material-backed button base.',
1115
+ '- When extract_widget_blueprint reports rootWidget=null, inspect widgetTreeStatus, widgetTreeError, and compile.errors before attempting structural rewrites. A degraded snapshot is still useful for recovery.',
1116
+ '- WidthOverride, HeightOverride, MinDesiredHeight, and similar fields may require paired bOverride_* flags. The write path now enables those flags automatically, but re-extract after patching to confirm the authored value landed where expected.',
1117
+ '- For long multi-step UI flows, use apply_window_ui_changes.checkpoint_after_mutation_steps=true so later compile/debug interruptions do not discard earlier successful edits.',
1118
+ '- For user-facing widget changes, compile/save is not the terminal checkpoint. Finish with capture_widget_preview or return partial verification with the blocking reason.',
804
1119
  '- Prefer event-driven updates and explicit setter functions over heavy property bindings or per-frame tick work.',
805
1120
  '- Use TSubclassOf + CreateWidget only for truly dynamic repeated elements. Keep authored static layout in the Widget Blueprint tree.',
806
1121
  ].join('\n'),
@@ -880,9 +1195,11 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
880
1195
  '- get_project_automation_context returns the editor-derived engine root, project file path, and editor target that project-control tools use as their first fallback.',
881
1196
  '- compile_project_code runs an external UBT build from the MCP host.',
882
1197
  '- compile_project_code and sync_project_code resolve engine_root, project_path, and target in this order: explicit args -> editor context -> environment.',
883
- '- trigger_live_coding requests an editor-side Live Coding compile and is only supported on Windows-focused setups. changed_paths remains an accepted compatibility input but the current editor-side trigger ignores it.',
1198
+ '- trigger_live_coding requests an editor-side Live Coding compile and is only supported on Windows-focused setups. changed_paths remains an accepted compatibility input but the current editor-side trigger ignores it. When Live Coding reports NoChanges or another fallback state, the result includes fallbackRecommended, reason, and the last external build context when available.',
884
1199
  '- restart_editor requests an editor restart, then waits for Remote Control to disconnect and reconnect. When save_dirty_assets is true, all dirty packages are saved before the restart to prevent modal save dialogs.',
885
1200
  '- sync_project_code requires explicit changed_paths and chooses Live Coding vs build_and_restart deterministically.',
1201
+ '- sync_project_code.restart_first now means shutdown-first: save/checkpoint if requested, ask the editor to close without relaunching, build with the DLL unlocked, then launch the editor from the MCP host and wait for reconnect.',
1202
+ '- apply_window_ui_changes can checkpoint after each mutation step without changing the low-level explicit-save contract. Use that when debugging editor ensures or breakpoint-heavy UI iterations.',
886
1203
  '',
887
1204
  'build_and_restart is forced for:',
888
1205
  '- .h/.hpp/.inl/.generated.h changes',
@@ -914,6 +1231,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
914
1231
  '- Gameplay mechanics, scripted interactions, runtime flows: automation tests first.',
915
1232
  '',
916
1233
  'Guardrail:',
1234
+ '- User-facing widget work is not complete until capture_widget_preview succeeds or the response explicitly reports partial verification with a blocking reason.',
917
1235
  '- If no Automation Spec or Functional Test exists for a gameplay mechanic, report verification as partial instead of inferring success from structure or screenshots alone.',
918
1236
  ].join('\n'),
919
1237
  }],
@@ -1019,6 +1337,21 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
1019
1337
  'Common BindWidget names:',
1020
1338
  '- TitleText, SubtitleText, PrimaryButton, SecondaryButton',
1021
1339
  ],
1340
+ material_button_base: [
1341
+ 'Pattern: material_button_base',
1342
+ '',
1343
+ 'Parent class:',
1344
+ '- Project-owned CommonButtonBase subclass or project-owned UserWidget wrapper',
1345
+ '',
1346
+ 'Recommended hierarchy:',
1347
+ '- Overlay Root',
1348
+ '- Image or Border MaterialPlate',
1349
+ '- NamedSlot or Overlay Content',
1350
+ '- Optional SizeBox HitTarget',
1351
+ '',
1352
+ 'Use this when the project wants button visuals driven by a material-backed plate instead of raw UButton style fields.',
1353
+ 'Keep hover, pressed, and disabled visuals centralized in the material instance or style asset, then expose only the project-owned tuning knobs through class defaults.',
1354
+ ],
1022
1355
  };
1023
1356
  server.resource('examples', new ResourceTemplate('blueprint://examples/{family}', {
1024
1357
  list: async () => ({
@@ -1097,12 +1430,14 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
1097
1430
  server.resource('captures', new ResourceTemplate('blueprint://captures/{capture_id}', {
1098
1431
  list: undefined,
1099
1432
  }), {
1100
- description: 'Read a widget preview or diff capture PNG by capture id.',
1433
+ description: 'Read a visual verification capture PNG by capture id.',
1101
1434
  mimeType: 'image/png',
1102
1435
  }, async (uri, variables) => {
1103
1436
  const captureId = String(variables.capture_id ?? '');
1104
1437
  const listed = await callSubsystemJson('ListCaptures', { AssetPathFilter: '' });
1105
- const captures = Array.isArray(listed.captures) ? listed.captures : [];
1438
+ const captures = Array.isArray(listed.captures)
1439
+ ? listed.captures.map((capture) => normalizeVerificationArtifact(capture))
1440
+ : [];
1106
1441
  const capture = captures.find((candidate) => (isRecord(candidate)
1107
1442
  && typeof candidate.captureId === 'string'
1108
1443
  && candidate.captureId === captureId
@@ -1152,6 +1487,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
1152
1487
  '- Generic create_data_asset and modify_data_asset reject Enhanced Input asset classes. Use the dedicated InputAction/InputMappingContext tools instead.',
1153
1488
  '- modify_material and modify_material_function remain available but are advanced escape hatches, not the primary authoring workflow.',
1154
1489
  '- There is still no first-class Substrate graph DSL.',
1490
+ '- CommonUI wrapper widgets are not a backdoor into internal Slate/UButton background or style fields. For CommonButtonBase-family widgets, treat raw UButton background/style properties as unsupported and use CommonUI style assets or a project-owned material-backed button base.',
1155
1491
  '- Raw authored animation track synthesis is out of scope. Animation authoring remains metadata-oriented.',
1156
1492
  '- World editing and runtime actor manipulation are out of scope for this server.',
1157
1493
  ].join('\n'),
@@ -1170,7 +1506,9 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
1170
1506
  '2. Inspect class defaults, BindWidget names, and current activatable-window flow before replacing any widget tree.',
1171
1507
  '3. Choose a preset layout pattern such as centered_overlay, common_menu_shell, activatable_window, or list_detail.',
1172
1508
  '4. Apply the smallest modify_widget_blueprint patch possible. Only use build_widget_tree or replace_tree when broad structure must change.',
1173
- '5. Compile immediately after structural changes, then save only after the compile result is clean.',
1509
+ '5. Compile immediately after structural changes. If compile fails, inspect compile diagnostics and rerun the smallest recovery patch first.',
1510
+ '6. Run capture_widget_preview after the compile result is clean so the rendered result is visually confirmed.',
1511
+ '7. Save after capture succeeds, or report partial verification explicitly when the visual checkpoint is blocked.',
1174
1512
  ].join('\n'),
1175
1513
  }],
1176
1514
  }));
@@ -1274,7 +1612,62 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
1274
1612
  }
1275
1613
  return null;
1276
1614
  }
1277
- let cachedProjectAutomationContext = null;
1615
+ function rememberExternalBuild(result) {
1616
+ lastExternalBuildContext = {
1617
+ success: result.success === true,
1618
+ operation: result.operation,
1619
+ strategy: result.strategy,
1620
+ engineRoot: result.engineRoot,
1621
+ projectPath: result.projectPath,
1622
+ target: result.target,
1623
+ platform: result.platform,
1624
+ configuration: result.configuration,
1625
+ exitCode: result.exitCode,
1626
+ durationMs: result.durationMs,
1627
+ restartRequired: result.restartRequired,
1628
+ restartReasons: result.restartReasons,
1629
+ errorCategory: result.errorCategory,
1630
+ errorSummary: result.errorSummary,
1631
+ lockedFiles: result.lockedFiles,
1632
+ };
1633
+ }
1634
+ function deriveLiveCodingFallbackReason(result) {
1635
+ const status = typeof result.status === 'string' ? result.status.toLowerCase() : '';
1636
+ const compileResult = typeof result.compileResult === 'string' ? result.compileResult.toLowerCase() : '';
1637
+ const existingReason = typeof result.reason === 'string' ? result.reason : undefined;
1638
+ if (compileResult === 'nochanges' || result.noOp === true) {
1639
+ return 'live_coding_reported_nochanges';
1640
+ }
1641
+ if (status === 'unsupported' || compileResult === 'unsupported') {
1642
+ return 'live_coding_unsupported';
1643
+ }
1644
+ if (status === 'unavailable' || compileResult === 'unavailable') {
1645
+ return 'live_coding_unavailable';
1646
+ }
1647
+ return existingReason;
1648
+ }
1649
+ function enrichLiveCodingResult(result, changedPaths = []) {
1650
+ const headerChanges = changedPaths.filter((path) => /\.(h|hpp|inl)$/i.test(path.replace(/\\/g, '/')));
1651
+ const warnings = Array.isArray(result.warnings)
1652
+ ? [...result.warnings.filter((value) => typeof value === 'string')]
1653
+ : [];
1654
+ if (headerChanges.length > 0) {
1655
+ warnings.push('Live Coding cannot add, remove, or reorder UPROPERTYs or change class layouts. '
1656
+ + 'Use compile_project_code + restart_editor for class layout changes.');
1657
+ }
1658
+ const fallbackRecommended = canFallbackFromLiveCoding(result);
1659
+ const reason = deriveLiveCodingFallbackReason(result);
1660
+ return {
1661
+ ...result,
1662
+ fallbackRecommended,
1663
+ ...(reason ? { reason } : {}),
1664
+ ...(fallbackRecommended && lastExternalBuildContext ? { lastExternalBuild: lastExternalBuildContext } : {}),
1665
+ changedPathsAccepted: changedPaths,
1666
+ changedPathsAppliedByEditor: false,
1667
+ headerChangesDetected: headerChanges,
1668
+ warnings,
1669
+ };
1670
+ }
1278
1671
  function firstDefinedString(...values) {
1279
1672
  for (const value of values) {
1280
1673
  if (typeof value === 'string' && value.length > 0) {
@@ -2594,7 +2987,7 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2594
2987
  });
2595
2988
  server.registerTool('capture_widget_preview', {
2596
2989
  title: 'Capture Widget Preview',
2597
- description: 'Render a WidgetBlueprint offscreen, write a PNG capture under Saved/BlueprintExtractor/Captures, and return metadata plus visual artifacts.',
2990
+ description: 'Render a WidgetBlueprint offscreen, write a PNG capture under Saved/BlueprintExtractor/Captures, and return a verification artifact plus visual artifacts.',
2598
2991
  inputSchema: {
2599
2992
  asset_path: z.string().describe('UE content path to the WidgetBlueprint to preview.'),
2600
2993
  width: z.number().int().min(64).max(2048).default(512).describe('Requested capture width in pixels. The editor clamps to a safe range.'),
@@ -2615,18 +3008,19 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2615
3008
  Width: width,
2616
3009
  Height: height,
2617
3010
  });
2618
- const captureId = typeof parsed.captureId === 'string' ? parsed.captureId : '';
3011
+ const artifact = normalizeVerificationArtifact(parsed);
3012
+ const captureId = typeof artifact.captureId === 'string' ? artifact.captureId : '';
2619
3013
  const resourceUri = `blueprint://captures/${encodeURIComponent(captureId)}`;
2620
3014
  const extraContent = [];
2621
3015
  if (captureId) {
2622
3016
  extraContent.push(buildResourceLinkContent(resourceUri, `Capture ${captureId}`, 'image/png', 'Rendered widget preview capture.'));
2623
3017
  }
2624
- const inlineImage = await maybeBuildInlineImageContent(parsed.artifactPath);
3018
+ const inlineImage = await maybeBuildInlineImageContent(typeof artifact.artifactPath === 'string' ? artifact.artifactPath : undefined);
2625
3019
  if (inlineImage) {
2626
3020
  extraContent.push(inlineImage);
2627
3021
  }
2628
3022
  return jsonToolSuccess({
2629
- ...parsed,
3023
+ ...artifact,
2630
3024
  resourceUri,
2631
3025
  }, { extraContent });
2632
3026
  }
@@ -2636,7 +3030,7 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2636
3030
  });
2637
3031
  server.registerTool('compare_capture_to_reference', {
2638
3032
  title: 'Compare Capture To Reference',
2639
- description: 'Compare a saved capture id or PNG path against another capture or reference PNG and produce a diff capture.',
3033
+ description: 'Compare a saved verification capture or PNG path against another capture or reference PNG and produce a diff verification artifact.',
2640
3034
  inputSchema: {
2641
3035
  capture: z.string().describe('Capture id or absolute PNG path for the actual result.'),
2642
3036
  reference: z.string().describe('Capture id or absolute PNG path for the expected reference.'),
@@ -2657,6 +3051,7 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2657
3051
  ReferenceIdOrPath: reference,
2658
3052
  Tolerance: tolerance,
2659
3053
  });
3054
+ const comparison = normalizeVerificationComparison(parsed);
2660
3055
  const diffCaptureId = typeof parsed.diffCaptureId === 'string' ? parsed.diffCaptureId : '';
2661
3056
  const diffResourceUri = `blueprint://captures/${encodeURIComponent(diffCaptureId)}`;
2662
3057
  const extraContent = [];
@@ -2670,6 +3065,7 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2670
3065
  return jsonToolSuccess({
2671
3066
  ...parsed,
2672
3067
  diffResourceUri,
3068
+ comparison,
2673
3069
  }, { extraContent });
2674
3070
  }
2675
3071
  catch (e) {
@@ -2678,7 +3074,7 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2678
3074
  });
2679
3075
  server.registerTool('list_captures', {
2680
3076
  title: 'List Captures',
2681
- description: 'List saved widget preview and comparison captures recorded by the editor-side verification lane.',
3077
+ description: 'List saved visual verification captures recorded by the editor-side verification lane.',
2682
3078
  inputSchema: {
2683
3079
  asset_path_filter: z.string().default('').describe('Optional exact asset path filter for captures created from one asset.'),
2684
3080
  },
@@ -2695,7 +3091,14 @@ RETURNS: JSON array of objects with path, name, and class for each asset (and su
2695
3091
  const parsed = await callSubsystemJson('ListCaptures', {
2696
3092
  AssetPathFilter: asset_path_filter,
2697
3093
  });
2698
- return jsonToolSuccess(parsed);
3094
+ const captures = Array.isArray(parsed.captures)
3095
+ ? parsed.captures.map((capture) => normalizeVerificationArtifact(capture))
3096
+ : [];
3097
+ return jsonToolSuccess({
3098
+ ...parsed,
3099
+ captureCount: captures.length,
3100
+ captures,
3101
+ });
2699
3102
  }
2700
3103
  catch (e) {
2701
3104
  return jsonToolError(e);
@@ -3748,11 +4151,18 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3748
4151
  timeoutMs: timeout_seconds * 1000,
3749
4152
  nullRhi: null_rhi,
3750
4153
  });
3751
- const extraContent = parsed.artifacts
4154
+ const normalized = normalizeAutomationRunResult(parsed);
4155
+ const extraContent = (Array.isArray(normalized.artifacts) ? normalized.artifacts : [])
3752
4156
  .slice(0, 6)
3753
- .map((artifact) => buildResourceLinkContent(artifact.resourceUri, `Automation ${artifact.name}`, artifact.mimeType, artifact.relativePath ?? artifact.path));
4157
+ .map((artifact) => buildResourceLinkContent(String(artifact.resourceUri ?? ''), `Automation ${String(artifact.name ?? 'artifact')}`, String(artifact.mimeType ?? 'application/octet-stream'), typeof artifact.relativePath === 'string' ? artifact.relativePath : String(artifact.path ?? '')));
4158
+ for (const artifact of (Array.isArray(normalized.verificationArtifacts) ? normalized.verificationArtifacts : []).slice(0, 2)) {
4159
+ const inlineImage = await maybeBuildInlineImageContent(typeof artifact.artifactPath === 'string' ? artifact.artifactPath : undefined);
4160
+ if (inlineImage) {
4161
+ extraContent.push(inlineImage);
4162
+ }
4163
+ }
3754
4164
  return jsonToolSuccess({
3755
- ...parsed,
4165
+ ...normalized,
3756
4166
  inputResolution: {
3757
4167
  engineRoot: resolved.sources.engineRoot,
3758
4168
  projectPath: resolved.sources.projectPath,
@@ -3785,10 +4195,17 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3785
4195
  if (!parsed) {
3786
4196
  return jsonToolError(new Error(`Automation test run '${run_id}' was not found.`));
3787
4197
  }
3788
- const extraContent = parsed.artifacts
4198
+ const normalized = normalizeAutomationRunResult(parsed);
4199
+ const extraContent = (Array.isArray(normalized.artifacts) ? normalized.artifacts : [])
3789
4200
  .slice(0, 6)
3790
- .map((artifact) => buildResourceLinkContent(artifact.resourceUri, `Automation ${artifact.name}`, artifact.mimeType, artifact.relativePath ?? artifact.path));
3791
- return jsonToolSuccess(parsed, { extraContent });
4201
+ .map((artifact) => buildResourceLinkContent(String(artifact.resourceUri ?? ''), `Automation ${String(artifact.name ?? 'artifact')}`, String(artifact.mimeType ?? 'application/octet-stream'), typeof artifact.relativePath === 'string' ? artifact.relativePath : String(artifact.path ?? '')));
4202
+ for (const artifact of (Array.isArray(normalized.verificationArtifacts) ? normalized.verificationArtifacts : []).slice(0, 2)) {
4203
+ const inlineImage = await maybeBuildInlineImageContent(typeof artifact.artifactPath === 'string' ? artifact.artifactPath : undefined);
4204
+ if (inlineImage) {
4205
+ extraContent.push(inlineImage);
4206
+ }
4207
+ }
4208
+ return jsonToolSuccess(normalized, { extraContent });
3792
4209
  }
3793
4210
  catch (e) {
3794
4211
  return jsonToolError(e);
@@ -3811,7 +4228,12 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3811
4228
  }, async ({ include_completed }) => {
3812
4229
  try {
3813
4230
  const parsed = await automationController.listAutomationTestRuns(include_completed);
3814
- return jsonToolSuccess(parsed);
4231
+ return jsonToolSuccess({
4232
+ ...parsed,
4233
+ runs: Array.isArray(parsed.runs)
4234
+ ? parsed.runs.map((run) => normalizeAutomationRunResult(run))
4235
+ : [],
4236
+ });
3815
4237
  }
3816
4238
  catch (e) {
3817
4239
  return jsonToolError(e);
@@ -3850,6 +4272,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3850
4272
  includeOutput: include_output,
3851
4273
  clearUhtCache: clear_uht_cache,
3852
4274
  });
4275
+ rememberExternalBuild(parsed);
3853
4276
  return jsonToolSuccess({
3854
4277
  ...parsed,
3855
4278
  inputResolution: {
@@ -3894,19 +4317,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3894
4317
  bEnableForSession: true,
3895
4318
  bWaitForCompletion: wait_for_completion,
3896
4319
  });
3897
- const headerChanges = (changed_paths ?? []).filter((p) => /\.(h|hpp|inl)$/i.test(p.replace(/\\/g, '/')));
3898
- const warnings = [];
3899
- if (headerChanges.length > 0) {
3900
- warnings.push('Live Coding cannot add, remove, or reorder UPROPERTYs or change class layouts. '
3901
- + 'Use compile_project_code + restart_editor for class layout changes.');
3902
- }
3903
- return jsonToolSuccess({
3904
- ...parsed,
3905
- changedPathsAccepted: changed_paths ?? [],
3906
- changedPathsAppliedByEditor: false,
3907
- headerChangesDetected: headerChanges,
3908
- warnings,
3909
- });
4320
+ return jsonToolSuccess(enrichLiveCodingResult(parsed, changed_paths ?? []));
3910
4321
  }
3911
4322
  catch (e) {
3912
4323
  return jsonToolError(e);
@@ -3933,6 +4344,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3933
4344
  const restartRequest = await callSubsystemJson('RestartEditor', {
3934
4345
  bWarn: false,
3935
4346
  bSaveDirtyAssets: save_dirty_assets,
4347
+ bRelaunch: true,
3936
4348
  });
3937
4349
  cachedProjectAutomationContext = null;
3938
4350
  if (!wait_for_reconnect || restartRequest.success === false) {
@@ -3976,7 +4388,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
3976
4388
  reconnect_timeout_seconds: z.number().int().positive().default(180).describe('Maximum seconds to wait for Remote Control to return after the editor restarts.'),
3977
4389
  include_output: z.boolean().default(false).describe('When true, include full build stdout and stderr in the result. Failure cases include output automatically.'),
3978
4390
  clear_uht_cache: z.boolean().default(false).describe('When true, delete UHT cache files (.uhtpath, .uhtsettings) from Intermediate/ before building so that Unreal Header Tool regenerates headers for any new or changed UPROPERTYs.'),
3979
- restart_first: z.boolean().default(false).describe('When true, restart the editor BEFORE building to release locked DLLs. Use this when a previous build failed due to a locked DLL (errorCategory: locked_file). Sequence: restart editor → build → restart editor again to load new binaries.'),
4391
+ restart_first: z.boolean().default(false).describe('When true, shut the editor down before building to release locked DLLs, then launch it from the MCP host after the build finishes. Use this when a previous build failed due to a locked DLL (errorCategory: locked_file).'),
3980
4392
  },
3981
4393
  annotations: {
3982
4394
  title: 'Sync Project Code',
@@ -4010,10 +4422,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4010
4422
  };
4011
4423
  }
4012
4424
  else {
4013
- const liveCoding = await callSubsystemJson('TriggerLiveCoding', {
4425
+ const liveCoding = enrichLiveCodingResult(await callSubsystemJson('TriggerLiveCoding', {
4014
4426
  bEnableForSession: true,
4015
4427
  bWaitForCompletion: true,
4016
- });
4428
+ }), changed_paths);
4017
4429
  if (!canFallbackFromLiveCoding(liveCoding)) {
4018
4430
  return jsonToolSuccess({
4019
4431
  success: liveCoding.success === true,
@@ -4043,6 +4455,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4043
4455
  const preRestart = await callSubsystemJson('RestartEditor', {
4044
4456
  bWarn: false,
4045
4457
  bSaveDirtyAssets: save_dirty_assets,
4458
+ bRelaunch: false,
4046
4459
  });
4047
4460
  cachedProjectAutomationContext = null;
4048
4461
  structuredResult.preRestart = preRestart;
@@ -4050,14 +4463,15 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4050
4463
  structuredResult.strategy = 'restart_first';
4051
4464
  return jsonToolSuccess(structuredResult);
4052
4465
  }
4053
- const preReconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4466
+ const preDisconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4054
4467
  disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
4055
4468
  reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
4469
+ waitForReconnect: false,
4056
4470
  });
4057
- structuredResult.preReconnect = preReconnect;
4058
- if (!preReconnect.reconnected) {
4059
- // Editor is down — build without it running (DLL is free)
4060
- structuredResult.editorDownForBuild = true;
4471
+ structuredResult.preDisconnect = preDisconnect;
4472
+ if (!preDisconnect.success) {
4473
+ structuredResult.strategy = 'restart_first';
4474
+ return jsonToolSuccess(structuredResult);
4061
4475
  }
4062
4476
  }
4063
4477
  const build = await projectController.compileProjectCode({
@@ -4070,6 +4484,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4070
4484
  includeOutput: include_output,
4071
4485
  clearUhtCache: clear_uht_cache,
4072
4486
  });
4487
+ rememberExternalBuild(build);
4073
4488
  structuredResult.strategy = restart_first ? 'restart_first' : 'build_and_restart';
4074
4489
  structuredResult.build = build;
4075
4490
  if (!build.success) {
@@ -4084,26 +4499,42 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4084
4499
  return jsonToolSuccess(structuredResult);
4085
4500
  }
4086
4501
  }
4087
- // If restart_first, editor may already be running (preReconnect succeeded) or down.
4088
- // Either way, restart to pick up new binaries.
4089
- const restartRequest = restart_first && structuredResult.editorDownForBuild
4090
- ? { success: true, message: 'Editor was already down during build; starting fresh.' }
4091
- : await callSubsystemJson('RestartEditor', {
4502
+ let reconnect;
4503
+ if (restart_first) {
4504
+ const launch = await projectController.launchEditor({
4505
+ engineRoot: resolvedProjectInputs.engineRoot,
4506
+ projectPath: resolvedProjectInputs.projectPath,
4507
+ });
4508
+ cachedProjectAutomationContext = null;
4509
+ structuredResult.editorLaunch = launch;
4510
+ if (!launch.success) {
4511
+ return jsonToolSuccess(structuredResult);
4512
+ }
4513
+ reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4514
+ disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
4515
+ reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
4516
+ waitForDisconnect: false,
4517
+ });
4518
+ }
4519
+ else {
4520
+ const restartRequest = await callSubsystemJson('RestartEditor', {
4092
4521
  bWarn: false,
4093
- bSaveDirtyAssets: restart_first ? false : save_dirty_assets,
4522
+ bSaveDirtyAssets: save_dirty_assets,
4523
+ bRelaunch: true,
4524
+ });
4525
+ cachedProjectAutomationContext = null;
4526
+ structuredResult.restartRequest = restartRequest;
4527
+ structuredResult.restartRequestSaveDirtyAssetsAccepted = save_dirty_assets;
4528
+ if (restartRequest.success === false) {
4529
+ return jsonToolSuccess(structuredResult);
4530
+ }
4531
+ reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4532
+ disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
4533
+ reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
4094
4534
  });
4095
- cachedProjectAutomationContext = null;
4096
- structuredResult.restartRequest = restartRequest;
4097
- structuredResult.restartRequestSaveDirtyAssetsAccepted = restart_first ? false : save_dirty_assets;
4098
- if (restartRequest.success === false) {
4099
- return jsonToolSuccess(structuredResult);
4100
4535
  }
4101
- const reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4102
- disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
4103
- reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
4104
- });
4105
4536
  structuredResult.reconnect = reconnect;
4106
- structuredResult.success = reconnect.success;
4537
+ structuredResult.success = reconnect.success === true;
4107
4538
  return jsonToolSuccess(structuredResult);
4108
4539
  }
4109
4540
  catch (e) {
@@ -4113,7 +4544,8 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4113
4544
  });
4114
4545
  server.registerTool('apply_window_ui_changes', {
4115
4546
  title: 'Apply Window UI Changes',
4116
- description: 'Thin helper that applies variable flags, class defaults, font work, compile/save, and optional code sync in one ordered flow.',
4547
+ description: 'Thin helper that applies variable flags, class defaults, font work, compile, optional save, and optional code sync in one ordered flow. It does not replace the final visual verification step.',
4548
+ outputSchema: applyWindowUiChangesResultSchema,
4117
4549
  inputSchema: {
4118
4550
  asset_path: z.string().describe('UE content path to the WidgetBlueprint to update.'),
4119
4551
  variable_widgets: z.array(WidgetSelectorFieldsSchema.extend({
@@ -4129,7 +4561,8 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4129
4561
  }).optional().describe('Optional explicit-file-path font import payload passed through to ImportFonts.'),
4130
4562
  font_applications: z.array(WindowFontApplicationSchema).optional().describe('Optional compact font applications passed through to ApplyWidgetFonts.'),
4131
4563
  compile_after: z.boolean().default(true).describe('When true, compile the widget Blueprint after the requested mutations.'),
4132
- save_after: z.boolean().default(true).describe('When true, save the widget asset and any explicit extra save paths after a successful compile.'),
4564
+ save_after: z.boolean().default(false).describe('When true, save the widget asset and any explicit extra save paths after a successful compile. Leave false to keep visual verification ahead of final persistence.'),
4565
+ checkpoint_after_mutation_steps: z.boolean().default(false).describe('When true, save checkpoint assets after each successful mutation step so multi-step UI flows can recover from later interruptions.'),
4133
4566
  save_asset_paths: z.array(z.string()).optional().describe('Optional extra asset paths to save with the widget asset.'),
4134
4567
  sync_project_code: z.object({
4135
4568
  changed_paths: z.array(z.string()).min(1),
@@ -4155,9 +4588,71 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4155
4588
  idempotentHint: false,
4156
4589
  openWorldHint: false,
4157
4590
  },
4158
- }, async ({ asset_path, variable_widgets, class_defaults, font_import, font_applications, compile_after, save_after, save_asset_paths, sync_project_code, }) => {
4591
+ }, async ({ asset_path, variable_widgets, class_defaults, font_import, font_applications, compile_after, save_after, checkpoint_after_mutation_steps, save_asset_paths, sync_project_code, }) => {
4159
4592
  try {
4160
4593
  const steps = [];
4594
+ const buildVerification = () => {
4595
+ const status = compile_after ? 'unverified' : 'compile_pending';
4596
+ return {
4597
+ required: true,
4598
+ status,
4599
+ surface: 'editor_offscreen',
4600
+ recommendedTool: 'capture_widget_preview',
4601
+ partialAllowed: true,
4602
+ reason: compile_after
4603
+ ? 'apply_window_ui_changes completed the mutation flow but did not perform the final rendered-widget verification step.'
4604
+ : 'apply_window_ui_changes completed the mutation flow without compiling the widget, so compile and visual verification are still pending.',
4605
+ };
4606
+ };
4607
+ const buildVerificationNextSteps = (status) => {
4608
+ if (status === 'compile_pending') {
4609
+ return [
4610
+ 'Compile the widget blueprint or rerun apply_window_ui_changes with compile_after=true before visual verification.',
4611
+ `Run capture_widget_preview for ${asset_path} after the compile result is clean.`,
4612
+ 'If preview capture is blocked, report partial verification explicitly with the blocking reason.',
4613
+ ];
4614
+ }
4615
+ return [
4616
+ `Run capture_widget_preview for ${asset_path} to visually confirm the rendered widget before calling the change verified.`,
4617
+ 'If preview capture is blocked, report partial verification explicitly with the blocking reason.',
4618
+ ];
4619
+ };
4620
+ const collectCheckpointAssetPaths = (extraPaths = []) => {
4621
+ const assetPaths = new Set([asset_path]);
4622
+ for (const extraPath of save_asset_paths ?? []) {
4623
+ assetPaths.add(extraPath);
4624
+ }
4625
+ if (font_import?.font_asset_path) {
4626
+ assetPaths.add(font_import.font_asset_path);
4627
+ }
4628
+ for (const extraPath of extraPaths) {
4629
+ assetPaths.add(extraPath);
4630
+ }
4631
+ return Array.from(assetPaths);
4632
+ };
4633
+ const checkpointMutationStep = async (stepName, extraPaths = []) => {
4634
+ if (!checkpoint_after_mutation_steps) {
4635
+ return null;
4636
+ }
4637
+ const result = await callSubsystemJson('SaveAssets', {
4638
+ AssetPathsJson: JSON.stringify(collectCheckpointAssetPaths(extraPaths)),
4639
+ });
4640
+ steps.push({
4641
+ step: 'checkpoint_after_mutation_step',
4642
+ afterStep: stepName,
4643
+ result,
4644
+ });
4645
+ if (result.success === false) {
4646
+ return jsonToolSuccess({
4647
+ success: false,
4648
+ operation: 'apply_window_ui_changes',
4649
+ stoppedAt: 'checkpoint_after_mutation_step',
4650
+ failedAfterStep: stepName,
4651
+ steps,
4652
+ });
4653
+ }
4654
+ return null;
4655
+ };
4161
4656
  for (const selector of variable_widgets) {
4162
4657
  const widgetIdentifier = getWidgetIdentifier(selector.widget_name, selector.widget_path);
4163
4658
  if (!widgetIdentifier) {
@@ -4184,6 +4679,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4184
4679
  steps,
4185
4680
  });
4186
4681
  }
4682
+ const checkpointResult = await checkpointMutationStep('mark_widget_variable');
4683
+ if (checkpointResult) {
4684
+ return checkpointResult;
4685
+ }
4187
4686
  }
4188
4687
  if (class_defaults) {
4189
4688
  const result = await callSubsystemJson('ModifyWidgetBlueprintStructure', {
@@ -4204,6 +4703,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4204
4703
  steps,
4205
4704
  });
4206
4705
  }
4706
+ const checkpointResult = await checkpointMutationStep('patch_class_defaults');
4707
+ if (checkpointResult) {
4708
+ return checkpointResult;
4709
+ }
4207
4710
  }
4208
4711
  if (font_import) {
4209
4712
  const result = await callSubsystemJson('ImportFonts', {
@@ -4222,6 +4725,15 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4222
4725
  steps,
4223
4726
  });
4224
4727
  }
4728
+ const importedAssetPaths = Array.isArray(result.importedObjects)
4729
+ ? result.importedObjects
4730
+ .map((value) => (typeof value === 'object' && value !== null && typeof value.assetPath === 'string' ? value.assetPath : null))
4731
+ .filter((value) => value !== null)
4732
+ : [];
4733
+ const checkpointResult = await checkpointMutationStep('import_fonts', importedAssetPaths);
4734
+ if (checkpointResult) {
4735
+ return checkpointResult;
4736
+ }
4225
4737
  }
4226
4738
  if (font_applications && font_applications.length > 0) {
4227
4739
  const result = await callSubsystemJson('ApplyWidgetFonts', {
@@ -4241,6 +4753,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4241
4753
  steps,
4242
4754
  });
4243
4755
  }
4756
+ const checkpointResult = await checkpointMutationStep('apply_widget_fonts');
4757
+ if (checkpointResult) {
4758
+ return checkpointResult;
4759
+ }
4244
4760
  }
4245
4761
  if (compile_after) {
4246
4762
  const result = await callSubsystemJson('CompileWidgetBlueprint', {
@@ -4258,6 +4774,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4258
4774
  steps,
4259
4775
  });
4260
4776
  }
4777
+ const checkpointResult = await checkpointMutationStep('compile_widget_blueprint');
4778
+ if (checkpointResult) {
4779
+ return checkpointResult;
4780
+ }
4261
4781
  }
4262
4782
  if (save_after) {
4263
4783
  const assetPaths = new Set([asset_path]);
@@ -4292,10 +4812,10 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4292
4812
  });
4293
4813
  let needsBuildRestart = syncPlan.strategy === 'build_and_restart' || !projectController.liveCodingSupported;
4294
4814
  if (syncPlan.strategy === 'live_coding' && projectController.liveCodingSupported) {
4295
- const liveCoding = await callSubsystemJson('TriggerLiveCoding', {
4815
+ const liveCoding = enrichLiveCodingResult(await callSubsystemJson('TriggerLiveCoding', {
4296
4816
  bEnableForSession: true,
4297
4817
  bWaitForCompletion: true,
4298
- });
4818
+ }), sync_project_code.changed_paths);
4299
4819
  if (!canFallbackFromLiveCoding(liveCoding)) {
4300
4820
  steps.push({
4301
4821
  step: 'sync_project_code',
@@ -4327,19 +4847,21 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4327
4847
  const preRestart = await callSubsystemJson('RestartEditor', {
4328
4848
  bWarn: false,
4329
4849
  bSaveDirtyAssets: sync_project_code.save_dirty_assets ?? true,
4850
+ bRelaunch: false,
4330
4851
  });
4331
4852
  cachedProjectAutomationContext = null;
4332
- const preReconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4853
+ const preDisconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4333
4854
  disconnectTimeoutMs: (sync_project_code.disconnect_timeout_seconds ?? 60) * 1000,
4334
4855
  reconnectTimeoutMs: (sync_project_code.reconnect_timeout_seconds ?? 180) * 1000,
4856
+ waitForReconnect: false,
4335
4857
  });
4336
4858
  steps.push({
4337
4859
  step: 'sync_project_code_pre_restart',
4338
4860
  strategy: 'restart_first',
4339
4861
  restartRequest: preRestart,
4340
- reconnect: preReconnect,
4862
+ disconnect: preDisconnect,
4341
4863
  });
4342
- if (preRestart.success === false) {
4864
+ if (preRestart.success === false || !preDisconnect.success) {
4343
4865
  return jsonToolSuccess({
4344
4866
  success: false,
4345
4867
  operation: 'apply_window_ui_changes',
@@ -4360,6 +4882,7 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4360
4882
  includeOutput: sync_project_code.include_output ?? false,
4361
4883
  clearUhtCache: sync_project_code.clear_uht_cache ?? false,
4362
4884
  });
4885
+ rememberExternalBuild(build);
4363
4886
  steps.push({
4364
4887
  step: 'compile_project_code',
4365
4888
  result: build,
@@ -4378,36 +4901,68 @@ RETURNS: JSON with validation summary, diagnostics, and dirtyPackages. Changes a
4378
4901
  steps,
4379
4902
  });
4380
4903
  }
4381
- const restartRequest = await callSubsystemJson('RestartEditor', {
4382
- bWarn: false,
4383
- bSaveDirtyAssets: useRestartFirst ? false : (sync_project_code.save_dirty_assets ?? true),
4384
- });
4385
- cachedProjectAutomationContext = null;
4386
- const reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4387
- disconnectTimeoutMs: (sync_project_code.disconnect_timeout_seconds ?? 60) * 1000,
4388
- reconnectTimeoutMs: (sync_project_code.reconnect_timeout_seconds ?? 180) * 1000,
4389
- });
4390
- steps.push({
4391
- step: 'sync_project_code',
4392
- strategy: useRestartFirst ? 'restart_first' : 'build_and_restart',
4393
- restartRequest,
4394
- saveDirtyAssetsAccepted: useRestartFirst ? false : (sync_project_code.save_dirty_assets ?? true),
4395
- reconnect,
4396
- });
4397
- if (restartRequest.success === false || !reconnect.success) {
4398
- return jsonToolSuccess({
4399
- success: false,
4400
- operation: 'apply_window_ui_changes',
4401
- stoppedAt: 'sync_project_code',
4402
- steps,
4904
+ if (useRestartFirst) {
4905
+ const editorLaunch = await projectController.launchEditor({
4906
+ engineRoot: resolvedProjectInputs.engineRoot,
4907
+ projectPath: resolvedProjectInputs.projectPath,
4908
+ });
4909
+ cachedProjectAutomationContext = null;
4910
+ const reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4911
+ disconnectTimeoutMs: (sync_project_code.disconnect_timeout_seconds ?? 60) * 1000,
4912
+ reconnectTimeoutMs: (sync_project_code.reconnect_timeout_seconds ?? 180) * 1000,
4913
+ waitForDisconnect: false,
4914
+ });
4915
+ steps.push({
4916
+ step: 'sync_project_code',
4917
+ strategy: 'restart_first',
4918
+ editorLaunch,
4919
+ reconnect,
4920
+ });
4921
+ if (!editorLaunch.success || !reconnect.success) {
4922
+ return jsonToolSuccess({
4923
+ success: false,
4924
+ operation: 'apply_window_ui_changes',
4925
+ stoppedAt: 'sync_project_code',
4926
+ steps,
4927
+ });
4928
+ }
4929
+ }
4930
+ else {
4931
+ const restartRequest = await callSubsystemJson('RestartEditor', {
4932
+ bWarn: false,
4933
+ bSaveDirtyAssets: sync_project_code.save_dirty_assets ?? true,
4934
+ bRelaunch: true,
4403
4935
  });
4936
+ cachedProjectAutomationContext = null;
4937
+ const reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
4938
+ disconnectTimeoutMs: (sync_project_code.disconnect_timeout_seconds ?? 60) * 1000,
4939
+ reconnectTimeoutMs: (sync_project_code.reconnect_timeout_seconds ?? 180) * 1000,
4940
+ });
4941
+ steps.push({
4942
+ step: 'sync_project_code',
4943
+ strategy: 'build_and_restart',
4944
+ restartRequest,
4945
+ saveDirtyAssetsAccepted: sync_project_code.save_dirty_assets ?? true,
4946
+ reconnect,
4947
+ });
4948
+ if (restartRequest.success === false || !reconnect.success) {
4949
+ return jsonToolSuccess({
4950
+ success: false,
4951
+ operation: 'apply_window_ui_changes',
4952
+ stoppedAt: 'sync_project_code',
4953
+ steps,
4954
+ });
4955
+ }
4404
4956
  }
4405
4957
  }
4406
4958
  }
4959
+ const verification = buildVerification();
4407
4960
  return jsonToolSuccess({
4408
4961
  success: true,
4409
4962
  operation: 'apply_window_ui_changes',
4410
4963
  steps,
4964
+ verification,
4965
+ next_steps: buildVerificationNextSteps(verification.status),
4411
4966
  });
4412
4967
  }
4413
4968
  catch (e) {