blueprint-extractor-mcp 3.0.0 → 3.1.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,10 +1,29 @@
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
9
  throw new Error(parsed.error);
7
10
  }
11
+ // Check for error-only failure responses (success: false with an explicit error message
12
+ // but no business-level fields). Structured responses with success: false are passed
13
+ // through so tool code can inspect them for orchestration (e.g., fallback strategies).
14
+ if (parsed.success === false) {
15
+ const explicitMessage = parsed.message ?? parsed.errorMessage;
16
+ if (typeof explicitMessage === 'string' && explicitMessage.length > 0) {
17
+ throw new Error(explicitMessage);
18
+ }
19
+ }
20
+ if (Array.isArray(parsed.errors) && parsed.errors.length > 0) {
21
+ const firstError = parsed.errors[0];
22
+ throw new Error(typeof firstError === 'string' ? firstError : JSON.stringify(firstError));
23
+ }
24
+ if (Object.keys(parsed).length === 0) {
25
+ throw new Error('Empty response from subsystem');
26
+ }
8
27
  return parsed;
9
28
  }
10
29
  export function jsonToolSuccess(parsed, options = {}) {
@@ -14,6 +33,28 @@ export function jsonToolSuccess(parsed, options = {}) {
14
33
  structuredContent,
15
34
  };
16
35
  }
36
+ /**
37
+ * Strips the C++ 'F' prefix from USTRUCT script paths.
38
+ * UE registers USTRUCTs without the F prefix in script paths.
39
+ * e.g., /Script/Module.FSTCFoo → /Script/Module.STCFoo
40
+ *
41
+ * Only strips when the class name starts with F followed by an uppercase letter,
42
+ * matching the UE USTRUCT naming convention.
43
+ */
44
+ export function normalizeUStructPath(path) {
45
+ return path.replace(/^(\/Script\/[^.]+\.)F([A-Z])/, '$1$2');
46
+ }
47
+ export function normalizeUStructPaths(paths) {
48
+ const warnings = [];
49
+ const normalized = paths.map(p => {
50
+ const result = normalizeUStructPath(p);
51
+ if (result !== p) {
52
+ warnings.push(`Auto-normalized F-prefix: "${p}" → "${result}"`);
53
+ }
54
+ return result;
55
+ });
56
+ return { normalized, warnings };
57
+ }
17
58
  export function jsonToolError(e) {
18
59
  return {
19
60
  content: [{ type: 'text', text: `Error: ${e instanceof Error ? e.message : String(e)}` }],
@@ -68,8 +68,15 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
68
68
  const firstDiagnostic = diagnostics.find((candidate) => (isRecord(candidate)
69
69
  && typeof candidate.message === 'string'
70
70
  && candidate.message.length > 0));
71
+ const existingContentText = existingResult
72
+ && Array.isArray(existingResult.content)
73
+ && isRecord(existingResult.content[0])
74
+ && existingResult.content[0].type === 'text'
75
+ && typeof existingResult.content[0].text === 'string'
76
+ ? existingResult.content[0].text.replace(/^Error:\s*/, '')
77
+ : undefined;
71
78
  const message = typeof payload.message === 'string'
72
- ? payload.message
79
+ ? payload.message.replace(/^Error:\s*/, '')
73
80
  : typeof payload.error === 'string'
74
81
  ? payload.error
75
82
  : (isRecord(firstDiagnostic) && typeof firstDiagnostic.message === 'string')
@@ -78,7 +85,26 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
78
85
  ? payloadOrError.message
79
86
  : typeof payloadOrError === 'string'
80
87
  ? payloadOrError.replace(/^Error:\s*/, '')
81
- : `Tool '${toolName}' failed.`;
88
+ : typeof existingContentText === 'string'
89
+ ? existingContentText
90
+ : payloadOrError == null
91
+ ? `Tool '${toolName}' failed with no error details (received ${String(payloadOrError)})`
92
+ : (() => {
93
+ const type = payloadOrError?.constructor?.name ?? typeof payloadOrError;
94
+ const keys = isRecord(payloadOrError) ? Object.keys(payloadOrError).join(', ') : '';
95
+ const truncatedJson = (() => {
96
+ try {
97
+ const s = JSON.stringify(payloadOrError);
98
+ return s.length > 500 ? s.slice(0, 500) + '…' : s;
99
+ }
100
+ catch {
101
+ return '[unserializable]';
102
+ }
103
+ })();
104
+ return keys
105
+ ? `Tool '${toolName}' failed — received ${type} with keys [${keys}]: ${truncatedJson}`
106
+ : `Tool '${toolName}' failed — received ${type}: ${truncatedJson}`;
107
+ })();
82
108
  const classification = classifyRecoverableToolFailure(toolName, message);
83
109
  const envelope = {
84
110
  ...payload,
@@ -109,7 +135,10 @@ export function createToolResultNormalizers({ taskAwareTools, classifyRecoverabl
109
135
  }
110
136
  return {
111
137
  ...(existingResult ?? {}),
112
- content: extractNonTextContent(existingResult),
138
+ content: [
139
+ { type: 'text', text: message },
140
+ ...extractNonTextContent(existingResult),
141
+ ],
113
142
  structuredContent: envelope,
114
143
  isError: true,
115
144
  };
@@ -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;
@@ -6,4 +6,9 @@ export declare function classifyRecoverableToolFailure(toolName: string, message
6
6
  recoverable: boolean;
7
7
  retry_after_ms: number;
8
8
  next_steps: string[];
9
+ } | {
10
+ code: string;
11
+ recoverable: boolean;
12
+ next_steps: string[];
13
+ retry_after_ms?: undefined;
9
14
  } | null;
@@ -59,5 +59,60 @@ export function classifyRecoverableToolFailure(toolName, message) {
59
59
  ],
60
60
  };
61
61
  }
62
+ if (message.includes('requires engine_root or UE_ENGINE_ROOT')) {
63
+ return {
64
+ code: 'engine_root_missing',
65
+ recoverable: false,
66
+ next_steps: [
67
+ 'Pass engine_root explicitly: { "engine_root": "C:/Program Files/Epic Games/UE_5.6" }',
68
+ 'Or set the UE_ENGINE_ROOT environment variable to your Unreal Engine root directory.',
69
+ ],
70
+ };
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
+ }
62
117
  return null;
63
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.0',
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;
@@ -1,3 +1,4 @@
1
+ import * as path from 'path';
1
2
  import { z } from 'zod';
2
3
  import { sleep } from '../helpers/formatting.js';
3
4
  import { canFallbackFromLiveCoding, enrichLiveCodingResult, } from '../helpers/live-coding.js';
@@ -205,11 +206,52 @@ export function registerProjectControlTools({ server, client, projectController,
205
206
  },
206
207
  }, async ({ save_dirty_assets, wait_for_reconnect, disconnect_timeout_seconds, reconnect_timeout_seconds }) => {
207
208
  try {
208
- const restartRequest = await callSubsystemJson('RestartEditor', {
209
- bWarn: false,
210
- bSaveDirtyAssets: save_dirty_assets,
211
- bRelaunch: true,
212
- });
209
+ // Pre-flight: verify editor is connected before attempting restart
210
+ const probe = supportsConnectionProbe(client);
211
+ if (probe) {
212
+ try {
213
+ const connected = await probe();
214
+ if (!connected) {
215
+ return jsonToolError(new Error('Editor is not connected. Cannot restart.'));
216
+ }
217
+ }
218
+ catch {
219
+ return jsonToolError(new Error('Editor is not connected. Cannot restart.'));
220
+ }
221
+ }
222
+ // Pre-flight: reject restart if editor is in PIE mode
223
+ try {
224
+ const ctx = await getProjectAutomationContext(true);
225
+ if (ctx.isPlayingInEditor === true) {
226
+ return jsonToolError(new Error('Cannot restart during Play-In-Editor session. Stop PIE first.'));
227
+ }
228
+ }
229
+ catch {
230
+ // Context query failed — proceed with restart attempt anyway
231
+ }
232
+ // Attempt restart with one retry on transient failure
233
+ let restartRequest;
234
+ try {
235
+ restartRequest = await callSubsystemJson('RestartEditor', {
236
+ bWarn: false,
237
+ bSaveDirtyAssets: save_dirty_assets,
238
+ bRelaunch: true,
239
+ });
240
+ }
241
+ catch (firstError) {
242
+ await sleep(2000);
243
+ try {
244
+ restartRequest = await callSubsystemJson('RestartEditor', {
245
+ bWarn: false,
246
+ bSaveDirtyAssets: save_dirty_assets,
247
+ bRelaunch: true,
248
+ });
249
+ }
250
+ catch (retryError) {
251
+ return jsonToolError(new Error(`restart_editor failed after retry: ${retryError instanceof Error ? retryError.message : String(retryError)}` +
252
+ ` (first attempt: ${firstError instanceof Error ? firstError.message : String(firstError)})`));
253
+ }
254
+ }
213
255
  clearProjectAutomationContext();
214
256
  if (!wait_for_reconnect || restartRequest.success === false) {
215
257
  return jsonToolSuccess({
@@ -218,7 +260,7 @@ export function registerProjectControlTools({ server, client, projectController,
218
260
  saveDirtyAssetsAppliedByEditor: save_dirty_assets,
219
261
  });
220
262
  }
221
- const reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
263
+ const reconnect = await projectController.waitForEditorRestart(probe, {
222
264
  disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
223
265
  reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
224
266
  });
@@ -263,15 +305,38 @@ export function registerProjectControlTools({ server, client, projectController,
263
305
  },
264
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, }) => {
265
307
  try {
266
- const plan = projectController.classifyChangedPaths(changed_paths, force_rebuild);
267
308
  const resolvedProjectInputs = await resolveProjectInputs({ engine_root, project_path, target });
309
+ // Normalize changed_paths to absolute paths
310
+ const projectRoot = resolvedProjectInputs.projectPath
311
+ ? path.dirname(resolvedProjectInputs.projectPath)
312
+ : '';
313
+ const pathWarnings = [];
314
+ const normalizedPaths = changed_paths.map((p) => {
315
+ if (path.isAbsolute(p))
316
+ return p;
317
+ if (projectRoot)
318
+ return path.resolve(projectRoot, p);
319
+ // No project root available — keep as-is but warn
320
+ pathWarnings.push(`Cannot resolve relative path without project root: "${p}"`);
321
+ return p;
322
+ });
323
+ changed_paths.forEach((original, i) => {
324
+ if (normalizedPaths[i] !== original) {
325
+ pathWarnings.push(`Normalized relative path: "${original}" → "${normalizedPaths[i]}"`);
326
+ }
327
+ });
328
+ const stepErrors = {};
329
+ const plan = projectController.classifyChangedPaths(normalizedPaths, force_rebuild);
268
330
  const structuredResult = {
269
331
  success: false,
270
332
  operation: 'sync_project_code',
271
- changedPaths: changed_paths,
333
+ changedPaths: normalizedPaths,
272
334
  plan,
273
335
  inputResolution: buildInputResolution(resolvedProjectInputs),
274
336
  };
337
+ if (pathWarnings.length > 0) {
338
+ structuredResult.pathWarnings = pathWarnings;
339
+ }
275
340
  if (plan.strategy === 'live_coding') {
276
341
  if (!projectController.liveCodingSupported) {
277
342
  structuredResult.plan = {
@@ -281,19 +346,31 @@ export function registerProjectControlTools({ server, client, projectController,
281
346
  };
282
347
  }
283
348
  else {
284
- const liveCoding = enrichLiveCodingResult(await callSubsystemJson('TriggerLiveCoding', {
285
- bEnableForSession: true,
286
- bWaitForCompletion: true,
287
- }), changed_paths, getLastExternalBuildContext());
349
+ let liveCoding;
350
+ try {
351
+ liveCoding = enrichLiveCodingResult(await callSubsystemJson('TriggerLiveCoding', {
352
+ bEnableForSession: true,
353
+ bWaitForCompletion: true,
354
+ }), normalizedPaths, getLastExternalBuildContext());
355
+ }
356
+ catch (lcError) {
357
+ stepErrors.liveCoding = lcError instanceof Error ? lcError.message : String(lcError);
358
+ liveCoding = { success: false, error: stepErrors.liveCoding };
359
+ }
288
360
  if (!canFallbackFromLiveCoding(liveCoding)) {
289
- return jsonToolSuccess({
361
+ const lcResult = {
290
362
  success: liveCoding.success === true,
291
363
  operation: 'sync_project_code',
292
364
  strategy: 'live_coding',
293
- changedPaths: changed_paths,
365
+ changedPaths: normalizedPaths,
294
366
  plan,
295
367
  liveCoding,
296
- });
368
+ };
369
+ if (pathWarnings.length > 0)
370
+ lcResult.pathWarnings = pathWarnings;
371
+ if (Object.keys(stepErrors).length > 0)
372
+ lcResult.stepErrors = stepErrors;
373
+ return jsonToolSuccess(lcResult);
297
374
  }
298
375
  structuredResult.liveCoding = liveCoding;
299
376
  structuredResult.plan = {
@@ -305,102 +382,199 @@ export function registerProjectControlTools({ server, client, projectController,
305
382
  }
306
383
  if (restart_first) {
307
384
  if (Array.isArray(save_asset_paths) && save_asset_paths.length > 0) {
308
- const preSave = await callSubsystemJson('SaveAssets', {
309
- AssetPathsJson: JSON.stringify(save_asset_paths),
385
+ try {
386
+ const preSave = await callSubsystemJson('SaveAssets', {
387
+ AssetPathsJson: JSON.stringify(save_asset_paths),
388
+ });
389
+ structuredResult.preSave = preSave;
390
+ }
391
+ catch (preSaveError) {
392
+ stepErrors.preSave = preSaveError instanceof Error ? preSaveError.message : String(preSaveError);
393
+ // Pre-save is non-critical — continue to restart
394
+ }
395
+ }
396
+ try {
397
+ const preRestart = await callSubsystemJson('RestartEditor', {
398
+ bWarn: false,
399
+ bSaveDirtyAssets: save_dirty_assets,
400
+ bRelaunch: false,
310
401
  });
311
- structuredResult.preSave = preSave;
402
+ clearProjectAutomationContext();
403
+ structuredResult.preRestart = preRestart;
404
+ if (preRestart.success === false) {
405
+ stepErrors.preRestart = 'RestartEditor returned success=false';
406
+ structuredResult.strategy = 'restart_first';
407
+ if (Object.keys(stepErrors).length > 0)
408
+ structuredResult.stepErrors = stepErrors;
409
+ return jsonToolSuccess(structuredResult);
410
+ }
312
411
  }
313
- const preRestart = await callSubsystemJson('RestartEditor', {
314
- bWarn: false,
315
- bSaveDirtyAssets: save_dirty_assets,
316
- bRelaunch: false,
317
- });
318
- clearProjectAutomationContext();
319
- structuredResult.preRestart = preRestart;
320
- if (preRestart.success === false) {
412
+ catch (preRestartError) {
413
+ stepErrors.preRestart = preRestartError instanceof Error ? preRestartError.message : String(preRestartError);
321
414
  structuredResult.strategy = 'restart_first';
415
+ structuredResult.success = false;
416
+ if (Object.keys(stepErrors).length > 0)
417
+ structuredResult.stepErrors = stepErrors;
322
418
  return jsonToolSuccess(structuredResult);
323
419
  }
324
- const preDisconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
325
- disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
326
- reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
327
- waitForReconnect: false,
328
- });
329
- structuredResult.preDisconnect = preDisconnect;
330
- if (!preDisconnect.success) {
420
+ try {
421
+ const preDisconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
422
+ disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
423
+ reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
424
+ waitForReconnect: false,
425
+ });
426
+ structuredResult.preDisconnect = preDisconnect;
427
+ if (!preDisconnect.success) {
428
+ stepErrors.preDisconnect = 'Editor did not disconnect within timeout';
429
+ structuredResult.strategy = 'restart_first';
430
+ if (Object.keys(stepErrors).length > 0)
431
+ structuredResult.stepErrors = stepErrors;
432
+ return jsonToolSuccess(structuredResult);
433
+ }
434
+ }
435
+ catch (preDisconnectError) {
436
+ stepErrors.preDisconnect = preDisconnectError instanceof Error ? preDisconnectError.message : String(preDisconnectError);
331
437
  structuredResult.strategy = 'restart_first';
438
+ structuredResult.success = false;
439
+ if (Object.keys(stepErrors).length > 0)
440
+ structuredResult.stepErrors = stepErrors;
332
441
  return jsonToolSuccess(structuredResult);
333
442
  }
334
443
  }
335
- const build = await projectController.compileProjectCode({
336
- engineRoot: resolvedProjectInputs.engineRoot,
337
- projectPath: resolvedProjectInputs.projectPath,
338
- target: resolvedProjectInputs.target,
339
- platform: platform,
340
- configuration: configuration,
341
- buildTimeoutMs: typeof build_timeout_seconds === 'number' ? build_timeout_seconds * 1000 : undefined,
342
- includeOutput: include_output,
343
- clearUhtCache: clear_uht_cache,
344
- });
345
- rememberExternalBuild(build);
444
+ let build;
445
+ try {
446
+ build = await projectController.compileProjectCode({
447
+ engineRoot: resolvedProjectInputs.engineRoot,
448
+ projectPath: resolvedProjectInputs.projectPath,
449
+ target: resolvedProjectInputs.target,
450
+ platform: platform,
451
+ configuration: configuration,
452
+ buildTimeoutMs: typeof build_timeout_seconds === 'number' ? build_timeout_seconds * 1000 : undefined,
453
+ includeOutput: include_output,
454
+ clearUhtCache: clear_uht_cache,
455
+ });
456
+ rememberExternalBuild(build);
457
+ }
458
+ catch (buildError) {
459
+ stepErrors.build = buildError instanceof Error ? buildError.message : String(buildError);
460
+ structuredResult.strategy = restart_first ? 'restart_first' : 'build_and_restart';
461
+ structuredResult.success = false;
462
+ if (Object.keys(stepErrors).length > 0)
463
+ structuredResult.stepErrors = stepErrors;
464
+ return jsonToolSuccess(structuredResult);
465
+ }
346
466
  structuredResult.strategy = restart_first ? 'restart_first' : 'build_and_restart';
347
467
  structuredResult.build = build;
348
468
  if (!build.success) {
469
+ if (Object.keys(stepErrors).length > 0)
470
+ structuredResult.stepErrors = stepErrors;
349
471
  return jsonToolSuccess(structuredResult);
350
472
  }
351
473
  if (!restart_first && Array.isArray(save_asset_paths) && save_asset_paths.length > 0) {
352
- const saveResult = await callSubsystemJson('SaveAssets', {
353
- AssetPathsJson: JSON.stringify(save_asset_paths),
354
- });
355
- structuredResult.save = saveResult;
356
- if (saveResult.success === false) {
357
- return jsonToolSuccess(structuredResult);
474
+ try {
475
+ const saveResult = await callSubsystemJson('SaveAssets', {
476
+ AssetPathsJson: JSON.stringify(save_asset_paths),
477
+ });
478
+ structuredResult.save = saveResult;
479
+ if (saveResult.success === false) {
480
+ stepErrors.save = 'SaveAssets returned success=false';
481
+ if (Object.keys(stepErrors).length > 0)
482
+ structuredResult.stepErrors = stepErrors;
483
+ return jsonToolSuccess(structuredResult);
484
+ }
485
+ }
486
+ catch (saveError) {
487
+ stepErrors.save = saveError instanceof Error ? saveError.message : String(saveError);
488
+ // Save is non-critical before restart — continue
358
489
  }
359
490
  }
360
491
  let reconnect;
361
492
  if (restart_first) {
362
- const launch = await projectController.launchEditor({
363
- engineRoot: resolvedProjectInputs.engineRoot,
364
- projectPath: resolvedProjectInputs.projectPath,
365
- });
366
- clearProjectAutomationContext();
367
- structuredResult.editorLaunch = launch;
368
- if (!launch.success) {
493
+ try {
494
+ const launch = await projectController.launchEditor({
495
+ engineRoot: resolvedProjectInputs.engineRoot,
496
+ projectPath: resolvedProjectInputs.projectPath,
497
+ });
498
+ clearProjectAutomationContext();
499
+ structuredResult.editorLaunch = launch;
500
+ if (!launch.success) {
501
+ stepErrors.editorLaunch = 'launchEditor returned success=false';
502
+ if (Object.keys(stepErrors).length > 0)
503
+ structuredResult.stepErrors = stepErrors;
504
+ return jsonToolSuccess(structuredResult);
505
+ }
506
+ }
507
+ catch (launchError) {
508
+ stepErrors.editorLaunch = launchError instanceof Error ? launchError.message : String(launchError);
509
+ structuredResult.success = false;
510
+ if (Object.keys(stepErrors).length > 0)
511
+ structuredResult.stepErrors = stepErrors;
369
512
  return jsonToolSuccess(structuredResult);
370
513
  }
371
- reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
372
- disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
373
- reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
374
- waitForDisconnect: false,
375
- });
514
+ try {
515
+ reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
516
+ disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
517
+ reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
518
+ waitForDisconnect: false,
519
+ });
520
+ }
521
+ catch (reconnectError) {
522
+ stepErrors.reconnect = reconnectError instanceof Error ? reconnectError.message : String(reconnectError);
523
+ }
376
524
  }
377
525
  else {
378
- const restartRequest = await callSubsystemJson('RestartEditor', {
379
- bWarn: false,
380
- bSaveDirtyAssets: save_dirty_assets,
381
- bRelaunch: true,
382
- });
383
- clearProjectAutomationContext();
384
- structuredResult.restartRequest = restartRequest;
385
- structuredResult.restartRequestSaveDirtyAssetsAccepted = save_dirty_assets;
386
- if (restartRequest.success === false) {
526
+ try {
527
+ const restartRequest = await callSubsystemJson('RestartEditor', {
528
+ bWarn: false,
529
+ bSaveDirtyAssets: save_dirty_assets,
530
+ bRelaunch: true,
531
+ });
532
+ clearProjectAutomationContext();
533
+ structuredResult.restartRequest = restartRequest;
534
+ structuredResult.restartRequestSaveDirtyAssetsAccepted = save_dirty_assets;
535
+ if (restartRequest.success === false) {
536
+ stepErrors.restart = 'RestartEditor returned success=false';
537
+ if (Object.keys(stepErrors).length > 0)
538
+ structuredResult.stepErrors = stepErrors;
539
+ return jsonToolSuccess(structuredResult);
540
+ }
541
+ }
542
+ catch (restartError) {
543
+ stepErrors.restart = restartError instanceof Error ? restartError.message : String(restartError);
544
+ structuredResult.success = false;
545
+ if (Object.keys(stepErrors).length > 0)
546
+ structuredResult.stepErrors = stepErrors;
387
547
  return jsonToolSuccess(structuredResult);
388
548
  }
389
- reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
390
- disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
391
- reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
392
- });
549
+ try {
550
+ reconnect = await projectController.waitForEditorRestart(supportsConnectionProbe(client), {
551
+ disconnectTimeoutMs: disconnect_timeout_seconds * 1000,
552
+ reconnectTimeoutMs: reconnect_timeout_seconds * 1000,
553
+ });
554
+ }
555
+ catch (reconnectError) {
556
+ stepErrors.reconnect = reconnectError instanceof Error ? reconnectError.message : String(reconnectError);
557
+ }
558
+ }
559
+ if (reconnect) {
560
+ structuredResult.reconnect = reconnect;
561
+ structuredResult.success = reconnect.success === true;
562
+ }
563
+ else {
564
+ structuredResult.success = false;
393
565
  }
394
- structuredResult.reconnect = reconnect;
395
- structuredResult.success = reconnect.success === true;
396
566
  if (structuredResult.success && structuredResult.build) {
397
567
  structuredResult.build = trimBuildOutput(structuredResult.build);
398
568
  }
569
+ if (Object.keys(stepErrors).length > 0) {
570
+ structuredResult.stepErrors = stepErrors;
571
+ }
399
572
  return jsonToolSuccess(structuredResult);
400
573
  }
401
574
  catch (error) {
402
575
  const resolved = await resolveProjectInputs({ engine_root, project_path, target });
403
- return jsonToolError(explainProjectResolutionFailure(error instanceof Error ? error.message : String(error), resolved));
576
+ const errorMessage = error instanceof Error ? error.message : String(error);
577
+ return jsonToolError(explainProjectResolutionFailure(errorMessage, resolved));
404
578
  }
405
579
  });
406
580
  }
@@ -1,6 +1,8 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- type JsonSubsystemCaller = (method: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>;
3
+ type JsonSubsystemCaller = (method: string, params: Record<string, unknown>, options?: {
4
+ timeoutMs?: number;
5
+ }) => Promise<Record<string, unknown>>;
4
6
  type RegisterSchemaAndAiAuthoringToolsOptions = {
5
7
  server: Pick<McpServer, 'registerTool'>;
6
8
  callSubsystemJson: JsonSubsystemCaller;
@@ -1,5 +1,65 @@
1
1
  import { z } from 'zod';
2
- import { jsonToolError, jsonToolSuccess } from '../helpers/subsystem.js';
2
+ import { jsonToolError, jsonToolSuccess, normalizeUStructPath } from '../helpers/subsystem.js';
3
+ /**
4
+ * Recursively walks a payload object and normalizes all `nodeStructType` values
5
+ * by stripping the C++ F-prefix from USTRUCT script paths.
6
+ */
7
+ function normalizePayloadPaths(obj, warnings) {
8
+ if (Array.isArray(obj))
9
+ return obj.map(item => normalizePayloadPaths(item, warnings));
10
+ if (obj && typeof obj === 'object') {
11
+ const record = obj;
12
+ const result = {};
13
+ for (const [key, value] of Object.entries(record)) {
14
+ if (key === 'nodeStructType' && typeof value === 'string') {
15
+ const normalized = normalizeUStructPath(value);
16
+ if (normalized !== value) {
17
+ warnings.push(`Auto-normalized F-prefix in nodeStructType: "${value}" → "${normalized}"`);
18
+ }
19
+ result[key] = normalized;
20
+ }
21
+ else {
22
+ result[key] = normalizePayloadPaths(value, warnings);
23
+ }
24
+ }
25
+ return result;
26
+ }
27
+ return obj;
28
+ }
29
+ /**
30
+ * Checks transition targetState.stateName references against the flat list of
31
+ * state names in the payload. Collects warnings for unresolvable targets.
32
+ */
33
+ function validateTransitionTargets(payload, warnings) {
34
+ if (!payload)
35
+ return;
36
+ const states = (payload.states ?? payload.stateTree?.states);
37
+ if (!Array.isArray(states) || states.length === 0)
38
+ return;
39
+ const stateNames = new Set();
40
+ for (const s of states) {
41
+ if (typeof s.stateName === 'string')
42
+ stateNames.add(s.stateName);
43
+ if (typeof s.name === 'string')
44
+ stateNames.add(s.name);
45
+ }
46
+ if (stateNames.size === 0)
47
+ return;
48
+ for (const s of states) {
49
+ const transitions = (s.transitions ?? []);
50
+ if (!Array.isArray(transitions))
51
+ continue;
52
+ for (const t of transitions) {
53
+ const target = t.targetState;
54
+ if (!target)
55
+ continue;
56
+ const targetName = (target.stateName ?? target.name);
57
+ if (typeof targetName === 'string' && targetName.length > 0 && !stateNames.has(targetName)) {
58
+ warnings.push(`Warning: transition references targetState "${targetName}" which is not in the states list`);
59
+ }
60
+ }
61
+ }
62
+ }
3
63
  export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, jsonObjectSchema, userDefinedStructMutationOperationSchema, userDefinedStructFieldSchema, userDefinedEnumMutationOperationSchema, userDefinedEnumEntrySchema, blackboardMutationOperationSchema, blackboardKeySchema, behaviorTreeMutationOperationSchema, behaviorTreeNodeSelectorSchema, stateTreeMutationOperationSchema, stateTreeStateSelectorSchema, stateTreeEditorNodeSelectorSchema, stateTreeTransitionSelectorSchema, }) {
4
64
  server.registerTool('create_user_defined_struct', {
5
65
  title: 'Create UserDefinedStruct',
@@ -303,12 +363,13 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
303
363
  asset_path: z.string().describe('UE content path for the new StateTree asset.'),
304
364
  payload: z.object({
305
365
  stateTree: jsonObjectSchema.optional(),
306
- schema: z.string().optional(),
366
+ schema: z.string().optional().describe('UE class path of the StateTree schema. Required by the editor. E.g. /Script/GameplayStateTreeModule.StateTreeComponentSchema'),
307
367
  states: z.array(jsonObjectSchema).optional(),
308
368
  evaluators: z.array(jsonObjectSchema).optional(),
309
369
  globalTasks: z.array(jsonObjectSchema).optional(),
310
370
  }).passthrough().default({}).describe('Extractor-shaped StateTree payload.'),
311
371
  validate_only: z.boolean().default(false).describe('Validate and compile without creating the asset.'),
372
+ timeout_seconds: z.number().positive().optional().describe('Timeout in seconds for the subsystem call. Default 120. Complex payloads may need more time.'),
312
373
  },
313
374
  annotations: {
314
375
  title: 'Create StateTree',
@@ -319,13 +380,35 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
319
380
  },
320
381
  }, async (args) => {
321
382
  try {
322
- const { asset_path, payload, validate_only } = args;
383
+ const { asset_path, payload, validate_only, timeout_seconds } = args;
384
+ // Validate schema is present
385
+ if (!payload?.schema && !payload?.stateTree?.schema) {
386
+ return jsonToolError(new Error('schema is required for create_state_tree. Provide it at payload.schema or payload.stateTree.schema '
387
+ + '(e.g., "/Script/GameplayStateTreeModule.StateTreeComponentSchema")'));
388
+ }
389
+ const warnings = [];
390
+ // Lightweight payload validation (non-blocking)
391
+ const schema = (payload?.schema ?? payload?.stateTree?.schema);
392
+ if (typeof schema === 'string' && !schema.startsWith('/Script/')) {
393
+ warnings.push(`Warning: schema path "${schema}" does not match expected /Script/... pattern`);
394
+ }
395
+ validateTransitionTargets(payload, warnings);
396
+ const normWarnings = [];
397
+ const normalizedPayload = normalizePayloadPaths(payload ?? {}, normWarnings);
398
+ warnings.push(...normWarnings);
399
+ const timeoutMs = (timeout_seconds ?? 120) * 1000;
323
400
  const parsed = await callSubsystemJson('CreateStateTree', {
324
401
  AssetPath: asset_path,
325
- PayloadJson: JSON.stringify(payload ?? {}),
402
+ PayloadJson: JSON.stringify(normalizedPayload),
326
403
  bValidateOnly: validate_only,
327
- });
328
- return jsonToolSuccess(parsed);
404
+ }, { timeoutMs });
405
+ const result = warnings.length > 0
406
+ ? { ...parsed, warnings }
407
+ : parsed;
408
+ const extraContent = normWarnings.length > 0
409
+ ? [{ type: 'text', text: normWarnings.join('\n') }]
410
+ : undefined;
411
+ return jsonToolSuccess(result, { extraContent });
329
412
  }
330
413
  catch (error) {
331
414
  return jsonToolError(error);
@@ -354,6 +437,7 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
354
437
  transitionId: z.string().optional(),
355
438
  }).passthrough().default({}).describe('Operation payload. Selectors support stateId/statePath, editorNodeId, and transitionId.'),
356
439
  validate_only: z.boolean().default(false).describe('Validate and compile without changing the asset.'),
440
+ timeout_seconds: z.number().positive().optional().describe('Timeout in seconds for the subsystem call. Default 90. Complex payloads may need more time.'),
357
441
  },
358
442
  annotations: {
359
443
  title: 'Modify StateTree',
@@ -364,14 +448,26 @@ export function registerSchemaAndAiAuthoringTools({ server, callSubsystemJson, j
364
448
  },
365
449
  }, async (args) => {
366
450
  try {
367
- const { asset_path, operation, payload, validate_only } = args;
451
+ const { asset_path, operation, payload, validate_only, timeout_seconds } = args;
452
+ const warnings = [];
453
+ validateTransitionTargets(payload, warnings);
454
+ const normWarnings = [];
455
+ const normalizedPayload = normalizePayloadPaths(payload ?? {}, normWarnings);
456
+ warnings.push(...normWarnings);
457
+ const timeoutMs = (timeout_seconds ?? 90) * 1000;
368
458
  const parsed = await callSubsystemJson('ModifyStateTree', {
369
459
  AssetPath: asset_path,
370
460
  Operation: operation,
371
- PayloadJson: JSON.stringify(payload ?? {}),
461
+ PayloadJson: JSON.stringify(normalizedPayload),
372
462
  bValidateOnly: validate_only,
373
- });
374
- return jsonToolSuccess(parsed);
463
+ }, { timeoutMs });
464
+ const result = warnings.length > 0
465
+ ? { ...parsed, warnings }
466
+ : parsed;
467
+ const extraContent = normWarnings.length > 0
468
+ ? [{ type: 'text', text: normWarnings.join('\n') }]
469
+ : undefined;
470
+ return jsonToolSuccess(result, { extraContent });
375
471
  }
376
472
  catch (error) {
377
473
  return jsonToolError(error);
@@ -35,6 +35,8 @@ export declare class UEClient {
35
35
  private clearDiscoveredSubsystemPath;
36
36
  private formatCallFailure;
37
37
  private rawCall;
38
- callSubsystem(method: string, params: Record<string, unknown>): Promise<string>;
38
+ callSubsystem(method: string, params: Record<string, unknown>, options?: {
39
+ timeoutMs?: number;
40
+ }): Promise<string>;
39
41
  }
40
42
  export {};
package/dist/ue-client.js CHANGED
@@ -135,7 +135,8 @@ export class UEClient {
135
135
  }
136
136
  return `Failed to call ${method} on BlueprintExtractorSubsystem (${details.join(', ')})`;
137
137
  }
138
- async rawCall(objectPath, functionName, parameters) {
138
+ async rawCall(objectPath, functionName, parameters, timeoutMs) {
139
+ const effectiveTimeout = timeoutMs ?? this.timeoutMs;
139
140
  const body = {
140
141
  objectPath,
141
142
  functionName,
@@ -143,7 +144,7 @@ export class UEClient {
143
144
  generateTransaction: false,
144
145
  };
145
146
  const controller = new AbortController();
146
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
147
+ const timeout = setTimeout(() => controller.abort(), effectiveTimeout);
147
148
  try {
148
149
  const res = await this.fetchImpl(`${this.baseUrl}/remote/object/call`, {
149
150
  method: 'PUT',
@@ -153,16 +154,17 @@ export class UEClient {
153
154
  });
154
155
  clearTimeout(timeout);
155
156
  if (!res.ok) {
156
- let errorDetail = res.statusText || `HTTP ${res.status}`;
157
+ let responseBody = '';
157
158
  try {
158
- const responseText = await res.text();
159
- if (responseText.trim().length > 0) {
160
- errorDetail = responseText;
161
- }
159
+ responseBody = await res.text();
162
160
  }
163
161
  catch {
164
- // Keep the HTTP status text when the body cannot be read.
162
+ // Body cannot be read leave empty.
165
163
  }
164
+ const truncatedBody = responseBody.length > 500 ? responseBody.slice(0, 500) + '...' : responseBody;
165
+ const errorDetail = truncatedBody.trim().length > 0
166
+ ? `UE editor returned HTTP ${res.status}: ${truncatedBody}`
167
+ : `UE editor returned HTTP ${res.status}: ${res.statusText || 'no response body'}`;
166
168
  return {
167
169
  response: null,
168
170
  status: res.status,
@@ -178,27 +180,28 @@ export class UEClient {
178
180
  if (error instanceof Error && error.name === 'AbortError') {
179
181
  return {
180
182
  response: null,
181
- error: `Request timed out after ${this.timeoutMs}ms`,
183
+ error: `Request timed out after ${effectiveTimeout}ms`,
182
184
  timedOut: true,
183
185
  };
184
186
  }
187
+ const networkMsg = error instanceof Error ? error.message : String(error);
185
188
  return {
186
189
  response: null,
187
- error: error instanceof Error ? error.message : String(error),
190
+ error: `Network error connecting to UE editor: ${networkMsg}`,
188
191
  };
189
192
  }
190
193
  }
191
- async callSubsystem(method, params) {
194
+ async callSubsystem(method, params, options) {
192
195
  const connected = await this.checkConnection();
193
196
  if (!connected) {
194
197
  throw new Error(`UE Editor not running or Remote Control not available on ${this.host}:${this.port}`);
195
198
  }
196
199
  let objectPath = await this.discoverSubsystem();
197
- let res = await this.rawCall(objectPath, method, params);
200
+ let res = await this.rawCall(objectPath, method, params, options?.timeoutMs);
198
201
  if (res.response === null && this.subsystemPathSource === 'discovered') {
199
202
  this.clearDiscoveredSubsystemPath();
200
203
  objectPath = await this.discoverSubsystem();
201
- res = await this.rawCall(objectPath, method, params);
204
+ res = await this.rawCall(objectPath, method, params, options?.timeoutMs);
202
205
  }
203
206
  if (res.response === null) {
204
207
  this.invalidateConnectionStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blueprint-extractor-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.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",
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",
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",