blueprint-extractor-mcp 3.1.0 → 3.2.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.
@@ -6,7 +6,9 @@ export async function callSubsystemJson(client, method, params, options) {
6
6
  }
7
7
  const parsed = JSON.parse(result);
8
8
  if (typeof parsed.error === 'string' && parsed.error.length > 0) {
9
- throw new Error(parsed.error);
9
+ const err = new Error(parsed.error);
10
+ err.ueResponse = parsed;
11
+ throw err;
10
12
  }
11
13
  // Check for error-only failure responses (success: false with an explicit error message
12
14
  // but no business-level fields). Structured responses with success: false are passed
@@ -14,12 +16,31 @@ export async function callSubsystemJson(client, method, params, options) {
14
16
  if (parsed.success === false) {
15
17
  const explicitMessage = parsed.message ?? parsed.errorMessage;
16
18
  if (typeof explicitMessage === 'string' && explicitMessage.length > 0) {
17
- throw new Error(explicitMessage);
19
+ const err = new Error(explicitMessage);
20
+ err.ueResponse = parsed;
21
+ throw err;
18
22
  }
19
23
  }
24
+ // Catch structured error responses with diagnostics but no explicit message.
25
+ // These come from FAssetMutationContext.BuildResult(false) in the C++ plugin.
26
+ if (parsed.success === false
27
+ && Array.isArray(parsed.diagnostics)
28
+ && parsed.diagnostics.length > 0) {
29
+ const messages = parsed.diagnostics
30
+ .filter((d) => typeof d === 'object' && d !== null && typeof d.message === 'string')
31
+ .map((d) => d.message);
32
+ const synthesized = messages.length > 0
33
+ ? messages.join('; ')
34
+ : `Operation failed with ${parsed.diagnostics.length} diagnostic(s)`;
35
+ const err = new Error(synthesized);
36
+ err.ueResponse = parsed;
37
+ throw err;
38
+ }
20
39
  if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
21
40
  const firstError = parsed.errors[0];
22
- throw new Error(typeof firstError === 'string' ? firstError : JSON.stringify(firstError));
41
+ const err = new Error(typeof firstError === 'string' ? firstError : JSON.stringify(firstError));
42
+ err.ueResponse = parsed;
43
+ throw err;
23
44
  }
24
45
  if (Object.keys(parsed).length === 0) {
25
46
  throw new Error('Empty response from subsystem');
@@ -28,6 +49,15 @@ export async function callSubsystemJson(client, method, params, options) {
28
49
  }
29
50
  export function jsonToolSuccess(parsed, options = {}) {
30
51
  const structuredContent = isRecord(parsed) ? parsed : { data: parsed };
52
+ // Guard: if the UE response indicates failure, route through error path.
53
+ // This prevents tool handlers from accidentally passing error payloads as successes.
54
+ if (isRecord(parsed) && parsed.success === false) {
55
+ return {
56
+ content: [{ type: 'text', text: `Error: ${typeof parsed.message === 'string' ? parsed.message : 'Operation failed'}` }],
57
+ structuredContent,
58
+ isError: true,
59
+ };
60
+ }
31
61
  return {
32
62
  content: options.extraContent ? [...options.extraContent] : [],
33
63
  structuredContent,
@@ -65,6 +65,15 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
65
65
  const diagnostics = Array.isArray(payload.diagnostics)
66
66
  ? payload.diagnostics
67
67
  : [];
68
+ // Merge diagnostics from ueResponse if the error carries one (set by callSubsystemJson).
69
+ if (diagnostics.length === 0
70
+ && payloadOrError instanceof Error
71
+ && isRecord(payloadOrError.ueResponse)) {
72
+ const ueResp = payloadOrError.ueResponse;
73
+ if (Array.isArray(ueResp.diagnostics)) {
74
+ diagnostics.push(...ueResp.diagnostics);
75
+ }
76
+ }
68
77
  const firstDiagnostic = diagnostics.find((candidate) => (isRecord(candidate)
69
78
  && typeof candidate.message === 'string'
70
79
  && candidate.message.length > 0));
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { exampleCatalog } from './catalogs/example-catalog.js';
3
3
  import { collectRelatedResources, collectToolExampleFamilies as collectToolExampleFamiliesFromCatalog, summarizeOutputSchema, summarizeSchemaFields, } from './helpers/tool-help.js';
4
- import { AnimMontageMutationOperationSchema, AnimationNotifySelectorSchema, AnimSequenceMutationOperationSchema, BehaviorTreeMutationOperationSchema, BehaviorTreeNodeSelectorSchema, BlackboardKeySchema, BlackboardMutationOperationSchema, BlendParameterSchema, BlendSpaceMutationOperationSchema, BlendSpaceSampleSchema, BlueprintGraphMutationOperationSchema, BlueprintMemberMutationOperationSchema, BuildConfigurationSchema, BuildPlatformSchema, CurveChannelSchema, CurveKeyDeleteSchema, CurveKeyUpsertSchema, CurveTableModeSchema, CurveTableRowSchema, CurveTypeSchema, DataTableRowSchema, ExtractAssetTypeSchema, EnhancedInputValueTypeSchema, FontImportItemSchema, ImportJobListSchema, ImportJobSchema, ImportPayloadSchema, InputMappingSchema, JsonObjectSchema, MaterialConnectionSelectorFieldsSchema, MaterialFontParameterSchema, MaterialFunctionAssetKindSchema, MaterialGraphOperationKindSchema, MaterialGraphOperationSchema, MaterialGraphPayloadSchema, MaterialLayerStackSchema, MaterialNodePositionSchema, MaterialScalarParameterSchema, MaterialStaticSwitchParameterSchema, MaterialTextureParameterSchema, MaterialVectorParameterSchema, MeshImportPayloadSchema, StateTreeEditorNodeSelectorSchema, StateTreeMutationOperationSchema, StateTreeStateSelectorSchema, StateTreeTransitionSelectorSchema, TextureImportPayloadSchema, UserDefinedEnumEntrySchema, UserDefinedEnumMutationOperationSchema, UserDefinedStructFieldSchema, UserDefinedStructMutationOperationSchema, WidgetBlueprintMutationOperationSchema, WidgetNodeSchema, WidgetSelectorFieldsSchema, WindowFontApplicationSchema, } from './schemas/tool-inputs.js';
4
+ import { AnimMontageMutationOperationSchema, AnimationNotifySelectorSchema, AnimSequenceMutationOperationSchema, BehaviorTreeMutationOperationSchema, BehaviorTreeNodeSelectorSchema, BlackboardKeySchema, BlackboardMutationOperationSchema, BlendParameterSchema, BlendSpaceMutationOperationSchema, BlendSpaceSampleSchema, BlueprintGraphMutationOperationSchema, BlueprintMemberMutationOperationSchema, BuildConfigurationSchema, BuildPlatformSchema, CurveChannelSchema, CurveKeyDeleteSchema, CurveKeyUpsertSchema, CurveTableModeSchema, CurveTableRowSchema, CurveTypeSchema, DataTableRowSchema, ExtractAssetTypeSchema, EnhancedInputValueTypeSchema, FontImportItemSchema, ImportJobListSchema, ImportJobSchema, ImportPayloadSchema, InputMappingSchema, JsonObjectSchema, MaterialConnectionSelectorFieldsSchema, MaterialFontParameterSchema, MaterialFunctionAssetKindSchema, MaterialGraphOperationKindSchema, MaterialGraphOperationSchema, MaterialGraphPayloadSchema, MaterialLayerStackSchema, MaterialNodePositionSchema, MaterialScalarParameterSchema, MaterialStaticSwitchParameterSchema, MaterialTextureParameterSchema, MaterialVectorParameterSchema, MeshImportPayloadSchema, StateTreeEditorNodeSelectorSchema, StateTreeMutationOperationSchema, StateTreeStateSelectorSchema, StateTreeTransitionSelectorSchema, StateTreeBindingSchema, TextureImportPayloadSchema, UserDefinedEnumEntrySchema, UserDefinedEnumMutationOperationSchema, UserDefinedStructFieldSchema, UserDefinedStructMutationOperationSchema, WidgetBlueprintMutationOperationSchema, WidgetNodeSchema, WidgetSelectorFieldsSchema, WindowFontApplicationSchema, } from './schemas/tool-inputs.js';
5
5
  import { applyWindowUiChangesResultSchema, AutomationRunListSchema, automationRunSchema, CaptureResultSchema, CascadeResultSchema, CleanupCapturesResultSchema, CompareCaptureResultSchema, CompareMotionCaptureBundleResultSchema, CreateModifyWidgetAnimationResultSchema, ExtractWidgetAnimationResultSchema, ListCapturesResultSchema, motionCaptureModeSchema, MotionCaptureBundleResultSchema, widgetAnimationCheckpointSchema, } from './schemas/tool-results.js';
6
6
  import { registerAnimationAuthoringTools } from './tools/animation-authoring.js';
7
7
  import { registerAutomationRunTools } from './tools/automation-runs.js';
@@ -166,6 +166,7 @@ export function registerServerTools({ server, client, projectController, automat
166
166
  stateTreeStateSelectorSchema: StateTreeStateSelectorSchema,
167
167
  stateTreeEditorNodeSelectorSchema: StateTreeEditorNodeSelectorSchema,
168
168
  stateTreeTransitionSelectorSchema: StateTreeTransitionSelectorSchema,
169
+ stateTreeBindingSchema: StateTreeBindingSchema,
169
170
  });
170
171
  registerAnimationAuthoringTools({
171
172
  server,
@@ -68,7 +68,7 @@ export function registerStaticDocResources(server) {
68
68
  '- AnimSequence: notify selector by notifyId/notifyGuid with notifyIndex or track metadata as fallback; operations replace_notifies, patch_notify, replace_sync_markers, replace_curve_metadata.',
69
69
  '- AnimMontage: notify selector by notifyId/notifyGuid with notifyIndex or track metadata as fallback; operations replace_notifies, patch_notify, replace_sections, replace_slots.',
70
70
  '- BlendSpace: sample selector by sampleIndex; operations replace_samples, patch_sample, set_axes.',
71
- '- Blueprint members: selectors by variableName, componentName, and functionName; operations replace_variables, patch_variable, replace_components, patch_component, replace_function_stubs, patch_class_defaults, compile.',
71
+ '- Blueprint members: selectors by variableName, componentName, and functionName; operations replace_variables, patch_variable, replace_components, patch_component, add_component, replace_function_stubs, patch_class_defaults, compile.',
72
72
  '- Blueprint graphs: operation upsert_function_graphs preserves unrelated graphs; append_function_call_to_sequence patches an existing sequence-style initializer without replacing the whole graph.',
73
73
  '',
74
74
  'WidgetBlueprint guidance:',
@@ -3038,7 +3038,7 @@ export declare const StateTreeMutationOperationSchema: z.ZodEnum<["replace_tree"
3038
3038
  export declare const AnimSequenceMutationOperationSchema: z.ZodEnum<["replace_notifies", "patch_notify", "replace_sync_markers", "replace_curve_metadata"]>;
3039
3039
  export declare const AnimMontageMutationOperationSchema: z.ZodEnum<["replace_notifies", "patch_notify", "replace_sections", "replace_slots"]>;
3040
3040
  export declare const BlendSpaceMutationOperationSchema: z.ZodEnum<["replace_samples", "patch_sample", "set_axes"]>;
3041
- export declare const BlueprintMemberMutationOperationSchema: z.ZodEnum<["replace_variables", "patch_variable", "replace_components", "patch_component", "replace_function_stubs", "patch_class_defaults", "compile"]>;
3041
+ export declare const BlueprintMemberMutationOperationSchema: z.ZodEnum<["replace_variables", "patch_variable", "replace_components", "patch_component", "add_component", "replace_function_stubs", "patch_class_defaults", "compile"]>;
3042
3042
  export declare const BlueprintGraphMutationOperationSchema: z.ZodEnum<["upsert_function_graphs", "append_function_call_to_sequence", "compile"]>;
3043
3043
  export declare const UserDefinedStructFieldSchema: z.ZodObject<{
3044
3044
  guid: z.ZodOptional<z.ZodString>;
@@ -3146,6 +3146,22 @@ export declare const StateTreeTransitionSelectorSchema: z.ZodObject<{
3146
3146
  transitionId: z.ZodOptional<z.ZodString>;
3147
3147
  id: z.ZodOptional<z.ZodString>;
3148
3148
  }, z.ZodTypeAny, "passthrough">>;
3149
+ export declare const StateTreeBindingSchema: z.ZodObject<{
3150
+ sourceTask: z.ZodString;
3151
+ sourceProperty: z.ZodString;
3152
+ targetTask: z.ZodString;
3153
+ targetProperty: z.ZodString;
3154
+ }, "strip", z.ZodTypeAny, {
3155
+ sourceTask: string;
3156
+ sourceProperty: string;
3157
+ targetTask: string;
3158
+ targetProperty: string;
3159
+ }, {
3160
+ sourceTask: string;
3161
+ sourceProperty: string;
3162
+ targetTask: string;
3163
+ targetProperty: string;
3164
+ }>;
3149
3165
  export declare const AnimationNotifySelectorSchema: z.ZodObject<{
3150
3166
  notifyId: z.ZodOptional<z.ZodString>;
3151
3167
  notifyGuid: z.ZodOptional<z.ZodString>;
@@ -359,6 +359,7 @@ export const BlueprintMemberMutationOperationSchema = z.enum([
359
359
  'patch_variable',
360
360
  'replace_components',
361
361
  'patch_component',
362
+ 'add_component',
362
363
  'replace_function_stubs',
363
364
  'patch_class_defaults',
364
365
  'compile',
@@ -408,6 +409,12 @@ export const StateTreeTransitionSelectorSchema = z.object({
408
409
  transitionId: z.string().optional(),
409
410
  id: z.string().optional(),
410
411
  }).passthrough();
412
+ export const StateTreeBindingSchema = z.object({
413
+ sourceTask: z.string().describe('Name of the source task whose output provides the value'),
414
+ sourceProperty: z.string().describe('Property name on the source task to read from'),
415
+ targetTask: z.string().describe('Name of the target task whose input receives the value'),
416
+ targetProperty: z.string().describe('Property name on the target task to write to'),
417
+ }).describe('Task output-to-input binding (requires C++ plugin support — not yet implemented)');
411
418
  export const AnimationNotifySelectorSchema = z.object({
412
419
  notifyId: z.string().optional(),
413
420
  notifyGuid: z.string().optional(),
@@ -304,6 +304,8 @@ export function registerProjectControlTools({ server, client, projectController,
304
304
  openWorldHint: false,
305
305
  },
306
306
  }, async ({ changed_paths, force_rebuild, engine_root, project_path, target, platform, configuration, save_dirty_assets, save_asset_paths, build_timeout_seconds, disconnect_timeout_seconds, reconnect_timeout_seconds, include_output, clear_uht_cache, restart_first, }) => {
307
+ const stepErrors = {};
308
+ let currentStep = 'init';
307
309
  try {
308
310
  const resolvedProjectInputs = await resolveProjectInputs({ engine_root, project_path, target });
309
311
  // Normalize changed_paths to absolute paths
@@ -325,7 +327,6 @@ export function registerProjectControlTools({ server, client, projectController,
325
327
  pathWarnings.push(`Normalized relative path: "${original}" → "${normalizedPaths[i]}"`);
326
328
  }
327
329
  });
328
- const stepErrors = {};
329
330
  const plan = projectController.classifyChangedPaths(normalizedPaths, force_rebuild);
330
331
  const structuredResult = {
331
332
  success: false,
@@ -337,6 +338,7 @@ export function registerProjectControlTools({ server, client, projectController,
337
338
  if (pathWarnings.length > 0) {
338
339
  structuredResult.pathWarnings = pathWarnings;
339
340
  }
341
+ currentStep = 'live_coding';
340
342
  if (plan.strategy === 'live_coding') {
341
343
  if (!projectController.liveCodingSupported) {
342
344
  structuredResult.plan = {
@@ -365,6 +367,7 @@ export function registerProjectControlTools({ server, client, projectController,
365
367
  changedPaths: normalizedPaths,
366
368
  plan,
367
369
  liveCoding,
370
+ failedStep: liveCoding.success !== true ? currentStep : undefined,
368
371
  };
369
372
  if (pathWarnings.length > 0)
370
373
  lcResult.pathWarnings = pathWarnings;
@@ -381,6 +384,7 @@ export function registerProjectControlTools({ server, client, projectController,
381
384
  }
382
385
  }
383
386
  if (restart_first) {
387
+ currentStep = 'pre_save';
384
388
  if (Array.isArray(save_asset_paths) && save_asset_paths.length > 0) {
385
389
  try {
386
390
  const preSave = await callSubsystemJson('SaveAssets', {
@@ -393,6 +397,7 @@ export function registerProjectControlTools({ server, client, projectController,
393
397
  // Pre-save is non-critical — continue to restart
394
398
  }
395
399
  }
400
+ currentStep = 'pre_restart';
396
401
  try {
397
402
  const preRestart = await callSubsystemJson('RestartEditor', {
398
403
  bWarn: false,
@@ -404,6 +409,7 @@ export function registerProjectControlTools({ server, client, projectController,
404
409
  if (preRestart.success === false) {
405
410
  stepErrors.preRestart = 'RestartEditor returned success=false';
406
411
  structuredResult.strategy = 'restart_first';
412
+ structuredResult.failedStep = currentStep;
407
413
  if (Object.keys(stepErrors).length > 0)
408
414
  structuredResult.stepErrors = stepErrors;
409
415
  return jsonToolSuccess(structuredResult);
@@ -413,6 +419,7 @@ export function registerProjectControlTools({ server, client, projectController,
413
419
  stepErrors.preRestart = preRestartError instanceof Error ? preRestartError.message : String(preRestartError);
414
420
  structuredResult.strategy = 'restart_first';
415
421
  structuredResult.success = false;
422
+ structuredResult.failedStep = currentStep;
416
423
  if (Object.keys(stepErrors).length > 0)
417
424
  structuredResult.stepErrors = stepErrors;
418
425
  return jsonToolSuccess(structuredResult);
@@ -427,6 +434,7 @@ export function registerProjectControlTools({ server, client, projectController,
427
434
  if (!preDisconnect.success) {
428
435
  stepErrors.preDisconnect = 'Editor did not disconnect within timeout';
429
436
  structuredResult.strategy = 'restart_first';
437
+ structuredResult.failedStep = currentStep;
430
438
  if (Object.keys(stepErrors).length > 0)
431
439
  structuredResult.stepErrors = stepErrors;
432
440
  return jsonToolSuccess(structuredResult);
@@ -436,11 +444,13 @@ export function registerProjectControlTools({ server, client, projectController,
436
444
  stepErrors.preDisconnect = preDisconnectError instanceof Error ? preDisconnectError.message : String(preDisconnectError);
437
445
  structuredResult.strategy = 'restart_first';
438
446
  structuredResult.success = false;
447
+ structuredResult.failedStep = currentStep;
439
448
  if (Object.keys(stepErrors).length > 0)
440
449
  structuredResult.stepErrors = stepErrors;
441
450
  return jsonToolSuccess(structuredResult);
442
451
  }
443
452
  }
453
+ currentStep = 'build';
444
454
  let build;
445
455
  try {
446
456
  build = await projectController.compileProjectCode({
@@ -459,6 +469,7 @@ export function registerProjectControlTools({ server, client, projectController,
459
469
  stepErrors.build = buildError instanceof Error ? buildError.message : String(buildError);
460
470
  structuredResult.strategy = restart_first ? 'restart_first' : 'build_and_restart';
461
471
  structuredResult.success = false;
472
+ structuredResult.failedStep = currentStep;
462
473
  if (Object.keys(stepErrors).length > 0)
463
474
  structuredResult.stepErrors = stepErrors;
464
475
  return jsonToolSuccess(structuredResult);
@@ -466,10 +477,12 @@ export function registerProjectControlTools({ server, client, projectController,
466
477
  structuredResult.strategy = restart_first ? 'restart_first' : 'build_and_restart';
467
478
  structuredResult.build = build;
468
479
  if (!build.success) {
480
+ structuredResult.failedStep = currentStep;
469
481
  if (Object.keys(stepErrors).length > 0)
470
482
  structuredResult.stepErrors = stepErrors;
471
483
  return jsonToolSuccess(structuredResult);
472
484
  }
485
+ currentStep = 'save';
473
486
  if (!restart_first && Array.isArray(save_asset_paths) && save_asset_paths.length > 0) {
474
487
  try {
475
488
  const saveResult = await callSubsystemJson('SaveAssets', {
@@ -478,6 +491,7 @@ export function registerProjectControlTools({ server, client, projectController,
478
491
  structuredResult.save = saveResult;
479
492
  if (saveResult.success === false) {
480
493
  stepErrors.save = 'SaveAssets returned success=false';
494
+ structuredResult.failedStep = currentStep;
481
495
  if (Object.keys(stepErrors).length > 0)
482
496
  structuredResult.stepErrors = stepErrors;
483
497
  return jsonToolSuccess(structuredResult);
@@ -488,6 +502,7 @@ export function registerProjectControlTools({ server, client, projectController,
488
502
  // Save is non-critical before restart — continue
489
503
  }
490
504
  }
505
+ currentStep = 'restart';
491
506
  let reconnect;
492
507
  if (restart_first) {
493
508
  try {
@@ -499,6 +514,7 @@ export function registerProjectControlTools({ server, client, projectController,
499
514
  structuredResult.editorLaunch = launch;
500
515
  if (!launch.success) {
501
516
  stepErrors.editorLaunch = 'launchEditor returned success=false';
517
+ structuredResult.failedStep = currentStep;
502
518
  if (Object.keys(stepErrors).length > 0)
503
519
  structuredResult.stepErrors = stepErrors;
504
520
  return jsonToolSuccess(structuredResult);
@@ -507,10 +523,12 @@ export function registerProjectControlTools({ server, client, projectController,
507
523
  catch (launchError) {
508
524
  stepErrors.editorLaunch = launchError instanceof Error ? launchError.message : String(launchError);
509
525
  structuredResult.success = false;
526
+ structuredResult.failedStep = currentStep;
510
527
  if (Object.keys(stepErrors).length > 0)
511
528
  structuredResult.stepErrors = stepErrors;
512
529
  return jsonToolSuccess(structuredResult);
513
530
  }
531
+ currentStep = 'reconnect';
514
532
  try {
515
533
  reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
516
534
  disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
@@ -534,6 +552,7 @@ export function registerProjectControlTools({ server, client, projectController,
534
552
  structuredResult.restartRequestSaveDirtyAssetsAccepted = save_dirty_assets;
535
553
  if (restartRequest.success === false) {
536
554
  stepErrors.restart = 'RestartEditor returned success=false';
555
+ structuredResult.failedStep = currentStep;
537
556
  if (Object.keys(stepErrors).length > 0)
538
557
  structuredResult.stepErrors = stepErrors;
539
558
  return jsonToolSuccess(structuredResult);
@@ -542,10 +561,12 @@ export function registerProjectControlTools({ server, client, projectController,
542
561
  catch (restartError) {
543
562
  stepErrors.restart = restartError instanceof Error ? restartError.message : String(restartError);
544
563
  structuredResult.success = false;
564
+ structuredResult.failedStep = currentStep;
545
565
  if (Object.keys(stepErrors).length > 0)
546
566
  structuredResult.stepErrors = stepErrors;
547
567
  return jsonToolSuccess(structuredResult);
548
568
  }
569
+ currentStep = 'reconnect';
549
570
  try {
550
571
  reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
551
572
  disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
@@ -572,9 +593,21 @@ export function registerProjectControlTools({ server, client, projectController,
572
593
  return jsonToolSuccess(structuredResult);
573
594
  }
574
595
  catch (error) {
575
- const resolved = await resolveProjectInputs({ engine_root, project_path, target });
596
+ // Preserve accumulated step context even in unhandled exception path.
597
+ const resolved = await resolveProjectInputs({ engine_root, project_path, target }).catch(() => ({}));
576
598
  const errorMessage = error instanceof Error ? error.message : String(error);
577
- return jsonToolError(explainProjectResolutionFailure(errorMessage, resolved));
599
+ const failurePayload = {
600
+ success: false,
601
+ operation: 'sync_project_code',
602
+ message: String(explainProjectResolutionFailure(errorMessage, resolved)),
603
+ failedStep: currentStep,
604
+ stepErrors,
605
+ changedPaths: changed_paths,
606
+ };
607
+ if (error instanceof Error && error.stack) {
608
+ failurePayload.stack = error.stack;
609
+ }
610
+ return jsonToolSuccess(failurePayload);
578
611
  }
579
612
  });
580
613
  }
@@ -19,6 +19,7 @@ type RegisterSchemaAndAiAuthoringToolsOptions = {
19
19
  stateTreeStateSelectorSchema: z.ZodTypeAny;
20
20
  stateTreeEditorNodeSelectorSchema: z.ZodTypeAny;
21
21
  stateTreeTransitionSelectorSchema: z.ZodTypeAny;
22
+ stateTreeBindingSchema: z.ZodTypeAny;
22
23
  };
23
- export declare function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, jsonObjectSchema, userDefinedStructMutationOperationSchema, userDefinedStructFieldSchema, userDefinedEnumMutationOperationSchema, userDefinedEnumEntrySchema, blackboardMutationOperationSchema, blackboardKeySchema, behaviorTreeMutationOperationSchema, behaviorTreeNodeSelectorSchema, stateTreeMutationOperationSchema, stateTreeStateSelectorSchema, stateTreeEditorNodeSelectorSchema, stateTreeTransitionSelectorSchema, }: RegisterSchemaAndAiAuthoringToolsOptions): void;
24
+ export declare function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, jsonObjectSchema, userDefinedStructMutationOperationSchema, userDefinedStructFieldSchema, userDefinedEnumMutationOperationSchema, userDefinedEnumEntrySchema, blackboardMutationOperationSchema, blackboardKeySchema, behaviorTreeMutationOperationSchema, behaviorTreeNodeSelectorSchema, stateTreeMutationOperationSchema, stateTreeStateSelectorSchema, stateTreeEditorNodeSelectorSchema, stateTreeTransitionSelectorSchema, stateTreeBindingSchema, }: RegisterSchemaAndAiAuthoringToolsOptions): void;
24
25
  export {};
@@ -3,6 +3,7 @@ import { jsonToolError, jsonToolSuccess, normalizeUStructPath } from '../helpers
3
3
  /**
4
4
  * Recursively walks a payload object and normalizes all `nodeStructType` values
5
5
  * by stripping the C++ F-prefix from USTRUCT script paths.
6
+ * Also validates that nodeStructType paths match the expected `/Script/...` pattern.
6
7
  */
7
8
  function normalizePayloadPaths(obj, warnings) {
8
9
  if (Array.isArray(obj))
@@ -26,6 +27,47 @@ function normalizePayloadPaths(obj, warnings) {
26
27
  }
27
28
  return obj;
28
29
  }
30
+ /**
31
+ * Validates that all `nodeStructType` values in the payload use the correct
32
+ * `/Script/Module.ClassName` path format. Collects warnings for paths that
33
+ * look like Blueprint asset paths, raw class names, or other incorrect formats.
34
+ *
35
+ * This runs AFTER normalizePayloadPaths so F-prefix issues are already resolved.
36
+ */
37
+ function validateNodeStructTypes(obj, warnings) {
38
+ if (Array.isArray(obj)) {
39
+ for (const item of obj)
40
+ validateNodeStructTypes(item, warnings);
41
+ return;
42
+ }
43
+ if (obj && typeof obj === 'object') {
44
+ const record = obj;
45
+ for (const [key, value] of Object.entries(record)) {
46
+ if (key === 'nodeStructType' && typeof value === 'string') {
47
+ if (!value.startsWith('/Script/')) {
48
+ if (value.startsWith('/Game/') || value.startsWith('/Content/')) {
49
+ warnings.push(`Warning: nodeStructType "${value}" looks like a Blueprint asset path. `
50
+ + 'nodeStructType should be a C++ USTRUCT script path (e.g., "/Script/Module.ClassName")');
51
+ }
52
+ else if (!value.includes('/')) {
53
+ warnings.push(`Warning: nodeStructType "${value}" is a bare class name. `
54
+ + 'Use the full script path format: "/Script/ModuleName.ClassName"');
55
+ }
56
+ else {
57
+ warnings.push(`Warning: nodeStructType "${value}" does not match the expected /Script/Module.ClassName pattern`);
58
+ }
59
+ }
60
+ else if (!value.includes('.')) {
61
+ warnings.push(`Warning: nodeStructType "${value}" is missing the class name after the module path. `
62
+ + 'Expected format: "/Script/Module.ClassName"');
63
+ }
64
+ }
65
+ else {
66
+ validateNodeStructTypes(value, warnings);
67
+ }
68
+ }
69
+ }
70
+ }
29
71
  /**
30
72
  * Checks transition targetState.stateName references against the flat list of
31
73
  * state names in the payload. Collects warnings for unresolvable targets.
@@ -60,7 +102,56 @@ function validateTransitionTargets(payload, warnings) {
60
102
  }
61
103
  }
62
104
  }
63
- export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, jsonObjectSchema, userDefinedStructMutationOperationSchema, userDefinedStructFieldSchema, userDefinedEnumMutationOperationSchema, userDefinedEnumEntrySchema, blackboardMutationOperationSchema, blackboardKeySchema, behaviorTreeMutationOperationSchema, behaviorTreeNodeSelectorSchema, stateTreeMutationOperationSchema, stateTreeStateSelectorSchema, stateTreeEditorNodeSelectorSchema, stateTreeTransitionSelectorSchema, }) {
105
+ /**
106
+ * Collects all task names from a states array, recursively walking children.
107
+ */
108
+ function collectTaskNames(states) {
109
+ const taskNames = new Set();
110
+ function walkStates(stateList) {
111
+ for (const state of stateList) {
112
+ const tasks = state.tasks;
113
+ if (Array.isArray(tasks)) {
114
+ for (const task of tasks) {
115
+ if (typeof task.name === 'string')
116
+ taskNames.add(task.name);
117
+ }
118
+ }
119
+ const children = state.children;
120
+ if (Array.isArray(children))
121
+ walkStates(children);
122
+ }
123
+ }
124
+ walkStates(states);
125
+ return taskNames;
126
+ }
127
+ /**
128
+ * Validates that binding sourceTask and targetTask references point to task
129
+ * names that exist in the payload's states. Collects warnings for unresolvable references.
130
+ */
131
+ function validateBindingTaskReferences(payload, warnings) {
132
+ if (!payload)
133
+ return;
134
+ const bindings = (payload.bindings ?? payload.stateTree?.bindings);
135
+ if (!Array.isArray(bindings) || bindings.length === 0)
136
+ return;
137
+ const states = (payload.states ?? payload.stateTree?.states);
138
+ if (!Array.isArray(states) || states.length === 0)
139
+ return;
140
+ const taskNames = collectTaskNames(states);
141
+ if (taskNames.size === 0)
142
+ return;
143
+ for (const binding of bindings) {
144
+ const source = binding.sourceTask;
145
+ const target = binding.targetTask;
146
+ if (typeof source === 'string' && source.length > 0 && !taskNames.has(source)) {
147
+ warnings.push(`Warning: binding references sourceTask "${source}" which is not found in the states' tasks`);
148
+ }
149
+ if (typeof target === 'string' && target.length > 0 && !taskNames.has(target)) {
150
+ warnings.push(`Warning: binding references targetTask "${target}" which is not found in the states' tasks`);
151
+ }
152
+ }
153
+ }
154
+ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, jsonObjectSchema, userDefinedStructMutationOperationSchema, userDefinedStructFieldSchema, userDefinedEnumMutationOperationSchema, userDefinedEnumEntrySchema, blackboardMutationOperationSchema, blackboardKeySchema, behaviorTreeMutationOperationSchema, behaviorTreeNodeSelectorSchema, stateTreeMutationOperationSchema, stateTreeStateSelectorSchema, stateTreeEditorNodeSelectorSchema, stateTreeTransitionSelectorSchema, stateTreeBindingSchema, }) {
64
155
  server.registerTool('create_user_defined_struct', {
65
156
  title: 'Create UserDefinedStruct',
66
157
  description: 'Create a UE5 UserDefinedStruct asset from extractor-shaped field definitions.',
@@ -367,6 +458,7 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
367
458
  states: z.array(jsonObjectSchema).optional(),
368
459
  evaluators: z.array(jsonObjectSchema).optional(),
369
460
  globalTasks: z.array(jsonObjectSchema).optional(),
461
+ bindings: z.array(stateTreeBindingSchema).optional().describe('Task output-to-input bindings. Declares data flow between tasks (requires C++ plugin support — not yet implemented).'),
370
462
  }).passthrough().default({}).describe('Extractor-shaped StateTree payload.'),
371
463
  validate_only: z.boolean().default(false).describe('Validate and compile without creating the asset.'),
372
464
  timeout_seconds: z.number().positive().optional().describe('Timeout in seconds for the subsystem call. Default 120. Complex payloads may need more time.'),
@@ -393,9 +485,12 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
393
485
  warnings.push(`Warning: schema path "${schema}" does not match expected /Script/... pattern`);
394
486
  }
395
487
  validateTransitionTargets(payload, warnings);
488
+ validateBindingTaskReferences(payload, warnings);
396
489
  const normWarnings = [];
397
490
  const normalizedPayload = normalizePayloadPaths(payload ?? {}, normWarnings);
398
491
  warnings.push(...normWarnings);
492
+ // Validate nodeStructType path formats after normalization
493
+ validateNodeStructTypes(normalizedPayload, warnings);
399
494
  const timeoutMs = (timeout_seconds ?? 120) * 1000;
400
495
  const parsed = await callSubsystemJson('CreateStateTree', {
401
496
  AssetPath: asset_path,
@@ -405,8 +500,9 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
405
500
  const result = warnings.length > 0
406
501
  ? { ...parsed, warnings }
407
502
  : parsed;
408
- const extraContent = normWarnings.length > 0
409
- ? [{ type: 'text', text: normWarnings.join('\n') }]
503
+ const allWarnings = [...normWarnings, ...warnings.filter(w => !normWarnings.includes(w))];
504
+ const extraContent = allWarnings.length > 0
505
+ ? [{ type: 'text', text: allWarnings.join('\n') }]
410
506
  : undefined;
411
507
  return jsonToolSuccess(result, { extraContent });
412
508
  }
@@ -435,6 +531,7 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
435
531
  statePath: z.string().optional(),
436
532
  editorNodeId: z.string().optional(),
437
533
  transitionId: z.string().optional(),
534
+ bindings: z.array(stateTreeBindingSchema).optional().describe('Task output-to-input bindings. Declares data flow between tasks (requires C++ plugin support — not yet implemented).'),
438
535
  }).passthrough().default({}).describe('Operation payload. Selectors support stateId/statePath, editorNodeId, and transitionId.'),
439
536
  validate_only: z.boolean().default(false).describe('Validate and compile without changing the asset.'),
440
537
  timeout_seconds: z.number().positive().optional().describe('Timeout in seconds for the subsystem call. Default 90. Complex payloads may need more time.'),
@@ -451,9 +548,12 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
451
548
  const { asset_path, operation, payload, validate_only, timeout_seconds } = args;
452
549
  const warnings = [];
453
550
  validateTransitionTargets(payload, warnings);
551
+ validateBindingTaskReferences(payload, warnings);
454
552
  const normWarnings = [];
455
553
  const normalizedPayload = normalizePayloadPaths(payload ?? {}, normWarnings);
456
554
  warnings.push(...normWarnings);
555
+ // Validate nodeStructType path formats after normalization
556
+ validateNodeStructTypes(normalizedPayload, warnings);
457
557
  const timeoutMs = (timeout_seconds ?? 90) * 1000;
458
558
  const parsed = await callSubsystemJson('ModifyStateTree', {
459
559
  AssetPath: asset_path,
@@ -464,8 +564,9 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
464
564
  const result = warnings.length > 0
465
565
  ? { ...parsed, warnings }
466
566
  : parsed;
467
- const extraContent = normWarnings.length > 0
468
- ? [{ type: 'text', text: normWarnings.join('\n') }]
567
+ const allWarnings = [...normWarnings, ...warnings.filter(w => !normWarnings.includes(w))];
568
+ const extraContent = allWarnings.length > 0
569
+ ? [{ type: 'text', text: allWarnings.join('\n') }]
469
570
  : undefined;
470
571
  return jsonToolSuccess(result, { extraContent });
471
572
  }
package/dist/ue-client.js CHANGED
@@ -133,6 +133,12 @@ export class UEClient {
133
133
  if (result.timedOut) {
134
134
  details.push(`timeoutMs=${this.timeoutMs}`);
135
135
  }
136
+ if (typeof result.responseBody === 'string' && result.responseBody.length > 0) {
137
+ const truncated = result.responseBody.length > 500
138
+ ? result.responseBody.slice(0, 500) + '…'
139
+ : result.responseBody;
140
+ details.push(`body=${truncated}`);
141
+ }
136
142
  return `Failed to call ${method} on BlueprintExtractorSubsystem (${details.join(', ')})`;
137
143
  }
138
144
  async rawCall(objectPath, functionName, parameters, timeoutMs) {
@@ -169,10 +175,13 @@ export class UEClient {
169
175
  response: null,
170
176
  status: res.status,
171
177
  error: errorDetail,
178
+ responseBody,
172
179
  };
173
180
  }
181
+ const text = await res.text();
174
182
  return {
175
- response: await res.json(),
183
+ response: JSON.parse(text),
184
+ responseBody: text,
176
185
  };
177
186
  }
178
187
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blueprint-extractor-mcp",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "MCP server for the Unreal Engine BlueprintExtractor plugin over Remote Control",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "build": "tsc",
40
40
  "start": "node dist/index.js",
41
41
  "test": "npm run test:unit && npm run test:stdio",
42
- "test:unit": "vitest run tests/ue-client.test.ts tests/project-controller.test.ts tests/animation-authoring.test.ts tests/automation-controller.test.ts tests/automation-runs.test.ts tests/blueprint-authoring.test.ts tests/capture.test.ts tests/commonui-button-style.test.ts tests/compactor.test.ts tests/data-and-input.test.ts tests/extraction-tools.test.ts tests/formatting.test.ts tests/helper-utils.test.ts tests/import-jobs.test.ts tests/material-authoring.test.ts tests/material-instance.test.ts tests/project-control.test.ts tests/schema-and-ai-authoring.test.ts tests/tables-and-curves.test.ts tests/tool-results.test.ts tests/utility-tools.test.ts tests/verification.test.ts tests/widget-animation-authoring.test.ts tests/widget-extraction.test.ts tests/widget-structure.test.ts tests/widget-verification.test.ts tests/window-ui.test.ts tests/server-bootstrap.test.ts tests/server-contract.test.ts tests/schema-validation.test.ts tests/server-config.test.ts tests/subsystem.test.ts",
42
+ "test:unit": "vitest run tests/ue-client.test.ts tests/project-controller.test.ts tests/animation-authoring.test.ts tests/automation-controller.test.ts tests/automation-runs.test.ts tests/blueprint-authoring.test.ts tests/capture.test.ts tests/commonui-button-style.test.ts tests/compactor.test.ts tests/data-and-input.test.ts tests/extraction-tools.test.ts tests/formatting.test.ts tests/helper-utils.test.ts tests/import-jobs.test.ts tests/live-coding.test.ts tests/material-authoring.test.ts tests/material-instance.test.ts tests/project-control.test.ts tests/project-resolution.test.ts tests/schema-and-ai-authoring.test.ts tests/tables-and-curves.test.ts tests/tool-help.test.ts tests/tool-results.test.ts tests/utility-tools.test.ts tests/verification.test.ts tests/widget-animation-authoring.test.ts tests/widget-extraction.test.ts tests/widget-structure.test.ts tests/widget-verification.test.ts tests/window-ui.test.ts tests/server-bootstrap.test.ts tests/server-contract.test.ts tests/schema-validation.test.ts tests/server-config.test.ts tests/subsystem.test.ts",
43
43
  "test:stdio": "npm run build && vitest run tests/stdio.integration.test.ts",
44
44
  "test:live": "npm run build && vitest run tests/live.e2e.test.ts",
45
45
  "test:pack-smoke": "node tests/pack-smoke.mjs",