blueprint-extractor-mcp 3.0.1 → 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.
@@ -1,3 +1,6 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import { resolve } from 'node:path';
1
4
  export function rememberExternalBuild(result) {
2
5
  return {
3
6
  success: result.success === true,
@@ -27,6 +30,31 @@ export async function getProjectAutomationContext(deps) {
27
30
  setCachedContext(nextContext);
28
31
  return nextContext;
29
32
  }
33
+ let cachedHeuristicEngineRoot;
34
+ const HEURISTIC_ENGINE_CANDIDATES = [
35
+ 'C:/Program Files/Epic Games/UE_5.6',
36
+ 'C:/Program Files/Epic Games/UE_5.5',
37
+ 'C:/Program Files/Epic Games/UE_5.4',
38
+ 'C:/Program Files/Epic Games/UE_5.3',
39
+ ];
40
+ const ENGINE_MARKER = 'Engine/Build/BatchFiles/Build.bat';
41
+ async function probeEngineRootHeuristic() {
42
+ if (cachedHeuristicEngineRoot !== undefined) {
43
+ return cachedHeuristicEngineRoot || undefined;
44
+ }
45
+ for (const candidate of HEURISTIC_ENGINE_CANDIDATES) {
46
+ try {
47
+ await access(resolve(candidate, ENGINE_MARKER), fsConstants.F_OK);
48
+ cachedHeuristicEngineRoot = candidate;
49
+ return candidate;
50
+ }
51
+ catch {
52
+ // not found, try next
53
+ }
54
+ }
55
+ cachedHeuristicEngineRoot = '';
56
+ return undefined;
57
+ }
30
58
  export async function resolveProjectInputs(request, deps) {
31
59
  const { getProjectAutomationContext, firstDefinedString, env = process.env, } = deps;
32
60
  let context = null;
@@ -45,7 +73,28 @@ export async function resolveProjectInputs(request, deps) {
45
73
  const engineRootFromEnv = firstDefinedString(env.UE_ENGINE_ROOT);
46
74
  const projectPathFromEnv = firstDefinedString(env.UE_PROJECT_PATH);
47
75
  const targetFromEnv = firstDefinedString(env.UE_PROJECT_TARGET, env.UE_EDITOR_TARGET);
48
- const engineRoot = firstDefinedString(request.engine_root, engineRootFromContext, engineRootFromEnv);
76
+ let engineRoot = firstDefinedString(request.engine_root, engineRootFromContext, engineRootFromEnv);
77
+ let engineRootSource;
78
+ if (request.engine_root) {
79
+ engineRootSource = 'explicit';
80
+ }
81
+ else if (engineRootFromContext) {
82
+ engineRootSource = 'editor_context';
83
+ }
84
+ else if (engineRootFromEnv) {
85
+ engineRootSource = 'environment';
86
+ }
87
+ else {
88
+ // Filesystem heuristic: probe common UE installation paths
89
+ const heuristicRoot = await probeEngineRootHeuristic();
90
+ if (heuristicRoot) {
91
+ engineRoot = heuristicRoot;
92
+ engineRootSource = 'filesystem_heuristic';
93
+ }
94
+ else {
95
+ engineRootSource = 'missing';
96
+ }
97
+ }
49
98
  const projectPath = firstDefinedString(request.project_path, projectPathFromContext, projectPathFromEnv);
50
99
  const target = firstDefinedString(request.target, targetFromContext, targetFromEnv);
51
100
  return {
@@ -55,7 +104,7 @@ export async function resolveProjectInputs(request, deps) {
55
104
  context,
56
105
  contextError,
57
106
  sources: {
58
- engineRoot: request.engine_root ? 'explicit' : engineRootFromContext ? 'editor_context' : engineRootFromEnv ? 'environment' : 'missing',
107
+ engineRoot: engineRootSource,
59
108
  projectPath: request.project_path ? 'explicit' : projectPathFromContext ? 'editor_context' : projectPathFromEnv ? 'environment' : 'missing',
60
109
  target: request.target ? 'explicit' : targetFromContext ? 'editor_context' : targetFromEnv ? 'environment' : 'missing',
61
110
  },
@@ -1,13 +1,29 @@
1
1
  import type { CallToolResult, ContentBlock } from '@modelcontextprotocol/sdk/types.js';
2
+ export interface SubsystemCallOptions {
3
+ timeoutMs?: number;
4
+ }
2
5
  type SubsystemClientLike = {
3
- callSubsystem(method: string, params: Record<string, unknown>): Promise<string>;
6
+ callSubsystem(method: string, params: Record<string, unknown>, options?: SubsystemCallOptions): Promise<string>;
4
7
  };
5
- export declare function callSubsystemJson(client: SubsystemClientLike, method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
8
+ export declare function callSubsystemJson(client: SubsystemClientLike, method: string, params: Record<string, unknown>, options?: SubsystemCallOptions): Promise<Record<string, unknown>>;
6
9
  export declare function jsonToolSuccess(parsed: unknown, options?: {
7
10
  extraContent?: ContentBlock[];
8
11
  }): CallToolResult & {
9
12
  structuredContent: Record<string, unknown>;
10
13
  };
14
+ /**
15
+ * Strips the C++ 'F' prefix from USTRUCT script paths.
16
+ * UE registers USTRUCTs without the F prefix in script paths.
17
+ * e.g., /Script/Module.FSTCFoo → /Script/Module.STCFoo
18
+ *
19
+ * Only strips when the class name starts with F followed by an uppercase letter,
20
+ * matching the UE USTRUCT naming convention.
21
+ */
22
+ export declare function normalizeUStructPath(path: string): string;
23
+ export declare function normalizeUStructPaths(paths: string[]): {
24
+ normalized: string[];
25
+ warnings: string[];
26
+ };
11
27
  export declare function jsonToolError(e: unknown): {
12
28
  content: {
13
29
  type: "text";
@@ -1,19 +1,90 @@
1
1
  import { isRecord } from './formatting.js';
2
- export async function callSubsystemJson(client, method, params) {
3
- const result = await client.callSubsystem(method, params);
2
+ export async function callSubsystemJson(client, method, params, options) {
3
+ const result = await client.callSubsystem(method, params, options);
4
+ if (process.env.MCP_DEBUG_RESPONSES) {
5
+ process.stderr.write(`[MCP_DEBUG] ${method} raw response: ${result}\n`);
6
+ }
4
7
  const parsed = JSON.parse(result);
5
8
  if (typeof parsed.error === 'string' && parsed.error.length > 0) {
6
- throw new Error(parsed.error);
9
+ const err = new Error(parsed.error);
10
+ err.ueResponse = parsed;
11
+ throw err;
12
+ }
13
+ // Check for error-only failure responses (success: false with an explicit error message
14
+ // but no business-level fields). Structured responses with success: false are passed
15
+ // through so tool code can inspect them for orchestration (e.g., fallback strategies).
16
+ if (parsed.success === false) {
17
+ const explicitMessage = parsed.message ?? parsed.errorMessage;
18
+ if (typeof explicitMessage === 'string' && explicitMessage.length > 0) {
19
+ const err = new Error(explicitMessage);
20
+ err.ueResponse = parsed;
21
+ throw err;
22
+ }
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
+ }
39
+ if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
40
+ const firstError = parsed.errors[0];
41
+ const err = new Error(typeof firstError === 'string' ? firstError : JSON.stringify(firstError));
42
+ err.ueResponse = parsed;
43
+ throw err;
44
+ }
45
+ if (Object.keys(parsed).length === 0) {
46
+ throw new Error('Empty response from subsystem');
7
47
  }
8
48
  return parsed;
9
49
  }
10
50
  export function jsonToolSuccess(parsed, options = {}) {
11
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
+ }
12
61
  return {
13
62
  content: options.extraContent ? [...options.extraContent] : [],
14
63
  structuredContent,
15
64
  };
16
65
  }
66
+ /**
67
+ * Strips the C++ 'F' prefix from USTRUCT script paths.
68
+ * UE registers USTRUCTs without the F prefix in script paths.
69
+ * e.g., /Script/Module.FSTCFoo → /Script/Module.STCFoo
70
+ *
71
+ * Only strips when the class name starts with F followed by an uppercase letter,
72
+ * matching the UE USTRUCT naming convention.
73
+ */
74
+ export function normalizeUStructPath(path) {
75
+ return path.replace(/^(\/Script\/[^.]+\.)F([A-Z])/, '$1$2');
76
+ }
77
+ export function normalizeUStructPaths(paths) {
78
+ const warnings = [];
79
+ const normalized = paths.map(p => {
80
+ const result = normalizeUStructPath(p);
81
+ if (result !== p) {
82
+ warnings.push(`Auto-normalized F-prefix: "${p}" → "${result}"`);
83
+ }
84
+ return result;
85
+ });
86
+ return { normalized, warnings };
87
+ }
17
88
  export function jsonToolError(e) {
18
89
  return {
19
90
  content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
@@ -65,9 +65,25 @@ 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));
80
+ const existingContentText = existingResult
81
+ && Array.isArray(existingResult.content)
82
+ && isRecord(existingResult.content[0])
83
+ && existingResult.content[0].type === 'text'
84
+ && typeof existingResult.content[0].text === 'string'
85
+ ? existingResult.content[0].text.replace(/^Error:\s*/, '')
86
+ : undefined;
71
87
  const message = typeof payload.message === 'string'
72
88
  ? payload.message.replace(/^Error:\s*/, '')
73
89
  : typeof payload.error === 'string'
@@ -78,7 +94,26 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
78
94
  ? payloadOrError.message
79
95
  : typeof payloadOrError === 'string'
80
96
  ? payloadOrError.replace(/^Error:\s*/, '')
81
- : `Tool '${toolName}' failed.`;
97
+ : typeof existingContentText === 'string'
98
+ ? existingContentText
99
+ : payloadOrError == null
100
+ ? `Tool '${toolName}' failed with no error details (received ${String(payloadOrError)})`
101
+ : (() => {
102
+ const type = payloadOrError?.constructor?.name ?? typeof payloadOrError;
103
+ const keys = isRecord(payloadOrError) ? Object.keys(payloadOrError).join(', ') : '';
104
+ const truncatedJson = (() => {
105
+ try {
106
+ const s = JSON.stringify(payloadOrError);
107
+ return s.length > 500 ? s.slice(0, 500) + '…' : s;
108
+ }
109
+ catch {
110
+ return '[unserializable]';
111
+ }
112
+ })();
113
+ return keys
114
+ ? `Tool '${toolName}' failed — received ${type} with keys [${keys}]: ${truncatedJson}`
115
+ : `Tool '${toolName}' failed — received ${type}: ${truncatedJson}`;
116
+ })();
82
117
  const classification = classifyRecoverableToolFailure(toolName, message);
83
118
  const envelope = {
84
119
  ...payload,
@@ -4,7 +4,9 @@ import { type ToolHelpEntry } from './helpers/tool-help.js';
4
4
  import type { ProjectControllerLike, CompileProjectCodeResult } from './project-controller.js';
5
5
  import type { ProjectAutomationContext, ResolvedProjectInputs } from './tool-context.js';
6
6
  import { UEClient } from './ue-client.js';
7
- type JsonSubsystemCaller = (method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>;
7
+ type JsonSubsystemCaller = (method: string, params: Record<string, unknown>, options?: {
8
+ timeoutMs?: number;
9
+ }) => Promise<Record<string, unknown>>;
8
10
  type UEClientLike = Pick<UEClient, 'callSubsystem'> & Partial<Pick<UEClient, 'checkConnection'>>;
9
11
  type ResolveProjectInputsFn = (request: {
10
12
  engine_root?: string;
@@ -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(),
@@ -69,5 +69,50 @@ export function classifyRecoverableToolFailure(toolName, message) {
69
69
  ],
70
70
  };
71
71
  }
72
+ if (message.includes('timed out') || message.includes('timeout') || message.includes('ETIMEDOUT') || message.includes('ESOCKETTIMEDOUT')) {
73
+ return {
74
+ code: 'timeout',
75
+ recoverable: true,
76
+ retry_after_ms: 5000,
77
+ next_steps: [
78
+ 'Retry with simpler payload or increase timeout',
79
+ 'Check if UE editor is responding (call wait_for_editor)',
80
+ 'For StateTree tools, try splitting complex payloads into multiple calls',
81
+ ],
82
+ };
83
+ }
84
+ if (message.includes('JSON') || message.includes('Unexpected token') || message.includes('SyntaxError')) {
85
+ return {
86
+ code: 'invalid_response',
87
+ recoverable: true,
88
+ next_steps: [
89
+ 'Check UE editor output log for errors',
90
+ 'The editor may have returned an HTML error page instead of JSON',
91
+ 'Retry the operation — this may be a transient serialization issue',
92
+ ],
93
+ };
94
+ }
95
+ if (message.includes('locked by another process') || message.includes('locked file') || message.includes('cannot access the file')) {
96
+ return {
97
+ code: 'locked_file',
98
+ recoverable: true,
99
+ next_steps: [
100
+ 'Close UE editor to release DLL locks',
101
+ 'Call restart_editor, then retry the build',
102
+ 'If editor was just restarted, the cached build may apply automatically',
103
+ ],
104
+ };
105
+ }
106
+ if (message.includes('Empty response') || message.includes('empty response') || message === '') {
107
+ return {
108
+ code: 'empty_response',
109
+ recoverable: true,
110
+ next_steps: [
111
+ 'Verify the asset path exists',
112
+ 'Check editor connection with wait_for_editor',
113
+ 'The UE subsystem may have crashed — check editor logs',
114
+ ],
115
+ };
116
+ }
72
117
  return null;
73
118
  }
@@ -17,7 +17,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
17
17
  const toolHelpRegistry = new Map();
18
18
  const server = new McpServer({
19
19
  name: 'blueprint-extractor',
20
- version: '3.0.1',
20
+ version: '3.1.0',
21
21
  }, {
22
22
  instructions: serverInstructions,
23
23
  });
@@ -32,7 +32,7 @@ export function createBlueprintExtractorServer(client = new UEClient(), projectC
32
32
  normalizeToolError,
33
33
  normalizeToolSuccess,
34
34
  });
35
- const callSubsystemJson = (method, params) => (callSubsystemJsonWithClient(client, method, params));
35
+ const callSubsystemJson = (method, params, options) => (callSubsystemJsonWithClient(client, method, params, options));
36
36
  function rememberExternalBuild(result) {
37
37
  lastExternalBuildContext = buildExternalBuildContext(result);
38
38
  }
@@ -14,7 +14,7 @@ export type ProjectAutomationContext = {
14
14
  liveCodingStarted?: boolean;
15
15
  liveCodingError?: string;
16
16
  };
17
- export type ProjectInputSource = 'explicit' | 'editor_context' | 'environment' | 'missing';
17
+ export type ProjectInputSource = 'explicit' | 'editor_context' | 'environment' | 'filesystem_heuristic' | 'missing';
18
18
  export type ResolvedProjectInputs = {
19
19
  engineRoot?: string;
20
20
  projectPath?: string;