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.
- package/dist/helpers/project-resolution.js +51 -2
- package/dist/helpers/subsystem.d.ts +18 -2
- package/dist/helpers/subsystem.js +43 -2
- package/dist/helpers/tool-results.js +32 -3
- package/dist/register-server-tools.d.ts +3 -1
- package/dist/server-config.d.ts +5 -0
- package/dist/server-config.js +55 -0
- package/dist/server-factory.js +2 -2
- package/dist/tool-context.d.ts +1 -1
- package/dist/tools/project-control.js +252 -78
- package/dist/tools/schema-and-ai-authoring.d.ts +3 -1
- package/dist/tools/schema-and-ai-authoring.js +106 -10
- package/dist/ue-client.d.ts +3 -1
- package/dist/ue-client.js +16 -13
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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:
|
|
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
|
|
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;
|
package/dist/server-config.d.ts
CHANGED
|
@@ -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;
|
package/dist/server-config.js
CHANGED
|
@@ -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
|
}
|
package/dist/server-factory.js
CHANGED
|
@@ -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.
|
|
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
|
}
|
package/dist/tool-context.d.ts
CHANGED
|
@@ -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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
361
|
+
const lcResult = {
|
|
290
362
|
success: liveCoding.success === true,
|
|
291
363
|
operation: 'sync_project_code',
|
|
292
364
|
strategy: 'live_coding',
|
|
293
|
-
changedPaths:
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
402
|
+
PayloadJson: JSON.stringify(normalizedPayload),
|
|
326
403
|
bValidateOnly: validate_only,
|
|
327
|
-
});
|
|
328
|
-
|
|
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(
|
|
461
|
+
PayloadJson: JSON.stringify(normalizedPayload),
|
|
372
462
|
bValidateOnly: validate_only,
|
|
373
|
-
});
|
|
374
|
-
|
|
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);
|
package/dist/ue-client.d.ts
CHANGED
|
@@ -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
|
|
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(),
|
|
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
|
|
157
|
+
let responseBody = '';
|
|
157
158
|
try {
|
|
158
|
-
|
|
159
|
-
if (responseText.trim().length > 0) {
|
|
160
|
-
errorDetail = responseText;
|
|
161
|
-
}
|
|
159
|
+
responseBody = await res.text();
|
|
162
160
|
}
|
|
163
161
|
catch {
|
|
164
|
-
//
|
|
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 ${
|
|
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
|
|
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.
|
|
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",
|