unreal-engine-mcp-server 0.5.0 → 0.5.2
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/.env.example +1 -1
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/greetings.yml +5 -1
- package/.github/workflows/labeler.yml +2 -1
- package/.github/workflows/publish-mcp.yml +2 -4
- package/.github/workflows/release-drafter.yml +3 -2
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +109 -0
- package/CONTRIBUTING.md +1 -1
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +166 -200
- package/dist/automation/bridge.d.ts +1 -2
- package/dist/automation/bridge.js +24 -23
- package/dist/automation/connection-manager.d.ts +1 -0
- package/dist/automation/connection-manager.js +10 -0
- package/dist/automation/message-handler.js +5 -4
- package/dist/automation/request-tracker.d.ts +4 -0
- package/dist/automation/request-tracker.js +11 -3
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +3 -3
- package/dist/graphql/resolvers.js +33 -30
- package/dist/graphql/server.js +3 -1
- package/dist/graphql/types.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -2
- package/dist/server-setup.d.ts +0 -1
- package/dist/server-setup.js +0 -40
- package/dist/tools/actors.d.ts +58 -24
- package/dist/tools/actors.js +22 -6
- package/dist/tools/assets.d.ts +19 -71
- package/dist/tools/assets.js +28 -22
- package/dist/tools/base-tool.d.ts +4 -4
- package/dist/tools/base-tool.js +1 -1
- package/dist/tools/blueprint.d.ts +45 -61
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/consolidated-tool-handlers.js +96 -110
- package/dist/tools/dynamic-handler-registry.d.ts +11 -9
- package/dist/tools/dynamic-handler-registry.js +17 -95
- package/dist/tools/editor.d.ts +19 -193
- package/dist/tools/editor.js +11 -2
- package/dist/tools/environment.d.ts +8 -14
- package/dist/tools/foliage.d.ts +18 -143
- package/dist/tools/foliage.js +4 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -13
- package/dist/tools/handlers/asset-handlers.js +454 -454
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/sequence-handlers.js +24 -13
- package/dist/tools/introspection.d.ts +1 -1
- package/dist/tools/introspection.js +1 -1
- package/dist/tools/landscape.d.ts +16 -116
- package/dist/tools/landscape.js +7 -3
- package/dist/tools/level.d.ts +22 -103
- package/dist/tools/level.js +26 -18
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +9 -5
- package/dist/tools/materials.d.ts +1 -1
- package/dist/tools/materials.js +5 -1
- package/dist/tools/niagara.js +37 -2
- package/dist/tools/performance.d.ts +0 -1
- package/dist/tools/performance.js +0 -1
- package/dist/tools/physics.js +5 -1
- package/dist/tools/sequence.d.ts +24 -24
- package/dist/tools/sequence.js +13 -0
- package/dist/tools/ui.d.ts +0 -2
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +135 -135
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +7 -5
- package/dist/utils/error-handler.d.ts +24 -2
- package/dist/utils/error-handler.js +58 -23
- package/dist/utils/normalize.d.ts +7 -4
- package/dist/utils/normalize.js +12 -10
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +4 -4
- package/dist/utils/response-factory.js +15 -21
- package/dist/utils/response-validator.js +88 -73
- package/dist/utils/unreal-command-queue.d.ts +2 -0
- package/dist/utils/unreal-command-queue.js +8 -1
- package/docs/Migration-Guide-v0.5.0.md +1 -9
- package/docs/handler-mapping.md +4 -2
- package/docs/testing-guide.md +2 -2
- package/package.json +12 -6
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
- package/scripts/run-all-tests.mjs +25 -20
- package/server.json +3 -2
- package/src/automation/bridge.ts +27 -25
- package/src/automation/connection-manager.ts +18 -0
- package/src/automation/message-handler.ts +33 -8
- package/src/automation/request-tracker.ts +39 -7
- package/src/config.ts +1 -1
- package/src/constants.ts +7 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +47 -49
- package/src/graphql/server.ts +3 -1
- package/src/graphql/types.ts +3 -0
- package/src/index.ts +15 -2
- package/src/resources/assets.ts +5 -4
- package/src/server/tool-registry.ts +3 -3
- package/src/server-setup.ts +3 -37
- package/src/tools/actors.ts +77 -44
- package/src/tools/animation.ts +1 -0
- package/src/tools/assets.ts +76 -65
- package/src/tools/base-tool.ts +3 -3
- package/src/tools/blueprint.ts +170 -104
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/consolidated-tool-handlers.ts +129 -150
- package/src/tools/dynamic-handler-registry.ts +22 -140
- package/src/tools/editor.ts +43 -29
- package/src/tools/environment.ts +21 -27
- package/src/tools/foliage.ts +28 -25
- package/src/tools/handlers/actor-handlers.ts +16 -17
- package/src/tools/handlers/asset-handlers.ts +484 -484
- package/src/tools/handlers/sequence-handlers.ts +85 -62
- package/src/tools/introspection.ts +7 -7
- package/src/tools/landscape.ts +34 -28
- package/src/tools/level.ts +100 -80
- package/src/tools/lighting.ts +25 -20
- package/src/tools/materials.ts +9 -3
- package/src/tools/niagara.ts +44 -2
- package/src/tools/performance.ts +1 -2
- package/src/tools/physics.ts +7 -1
- package/src/tools/sequence.ts +42 -26
- package/src/tools/ui.ts +1 -3
- package/src/types/automation-responses.ts +119 -0
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +135 -135
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +47 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +38 -16
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +29 -24
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/unreal-command-queue.ts +13 -1
- package/src/utils/validation.test.ts +184 -0
- package/tests/test-animation.mjs +358 -33
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +314 -116
- package/tests/test-behavior-tree.mjs +327 -144
- package/tests/test-blueprint-graph.mjs +343 -12
- package/tests/test-control-editor.mjs +85 -53
- package/tests/test-graphql.mjs +58 -8
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +291 -61
- package/tests/test-landscape.mjs +304 -48
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-level.mjs +70 -51
- package/tests/test-performance.mjs +539 -0
- package/tests/test-sequence.mjs +82 -46
- package/tests/test-system.mjs +72 -33
- package/tests/test-wasm.mjs +98 -8
- package/vitest.config.ts +35 -0
- package/.github/release-drafter.yml +0 -148
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/blueprint/helpers.d.ts +0 -29
- package/dist/tools/blueprint/helpers.js +0 -182
- package/src/prompts/index.ts +0 -249
- package/src/tools/blueprint/helpers.ts +0 -189
- package/tests/test-blueprint-events.mjs +0 -35
- package/tests/test-extra-tools.mjs +0 -38
- package/tests/test-render.mjs +0 -33
- package/tests/test-search-assets.mjs +0 -66
|
@@ -16,61 +16,134 @@ export enum ErrorType {
|
|
|
16
16
|
UNKNOWN = 'UNKNOWN'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Debug information attached to error responses in development mode
|
|
21
|
+
*/
|
|
22
|
+
interface ErrorResponseDebug {
|
|
23
|
+
errorType: ErrorType;
|
|
24
|
+
originalError: string;
|
|
25
|
+
stack?: string;
|
|
26
|
+
context?: Record<string, unknown>;
|
|
27
|
+
retriable: boolean;
|
|
28
|
+
scope: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extended error response with optional debug info
|
|
33
|
+
*/
|
|
34
|
+
interface ErrorToolResponse extends BaseToolResponse {
|
|
35
|
+
_debug?: ErrorResponseDebug;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Represents any error object with common properties
|
|
40
|
+
*/
|
|
41
|
+
interface ErrorLike {
|
|
42
|
+
message?: string;
|
|
43
|
+
code?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
errorType?: string;
|
|
46
|
+
stack?: string;
|
|
47
|
+
response?: { status?: number };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalize any error type to ErrorLike interface
|
|
52
|
+
*/
|
|
53
|
+
function normalizeErrorToLike(error: unknown): ErrorLike {
|
|
54
|
+
if (error instanceof Error) {
|
|
55
|
+
return {
|
|
56
|
+
message: error.message,
|
|
57
|
+
stack: error.stack,
|
|
58
|
+
code: (error as NodeJS.ErrnoException).code
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (typeof error === 'object' && error !== null) {
|
|
62
|
+
const obj = error as Record<string, unknown>;
|
|
63
|
+
return {
|
|
64
|
+
message: typeof obj.message === 'string' ? obj.message : undefined,
|
|
65
|
+
code: typeof obj.code === 'string' ? obj.code : undefined,
|
|
66
|
+
type: typeof obj.type === 'string' ? obj.type : undefined,
|
|
67
|
+
errorType: typeof obj.errorType === 'string' ? obj.errorType : undefined,
|
|
68
|
+
stack: typeof obj.stack === 'string' ? obj.stack : undefined,
|
|
69
|
+
response: typeof obj.response === 'object' && obj.response !== null
|
|
70
|
+
? {
|
|
71
|
+
status: typeof (obj.response as Record<string, unknown>).status === 'number'
|
|
72
|
+
? (obj.response as Record<string, unknown>).status as number
|
|
73
|
+
: undefined
|
|
74
|
+
}
|
|
75
|
+
: undefined
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return { message: String(error) };
|
|
79
|
+
}
|
|
80
|
+
|
|
19
81
|
/**
|
|
20
82
|
* Consistent error handling for all tools
|
|
21
83
|
*/
|
|
22
84
|
export class ErrorHandler {
|
|
23
85
|
/**
|
|
24
86
|
* Create a standardized error response
|
|
87
|
+
* @param error - The error object (can be Error, string, object with message, or unknown)
|
|
88
|
+
* @param toolName - Name of the tool that failed
|
|
89
|
+
* @param context - Optional additional context for debugging
|
|
25
90
|
*/
|
|
26
91
|
static createErrorResponse(
|
|
27
|
-
error:
|
|
92
|
+
error: unknown,
|
|
28
93
|
toolName: string,
|
|
29
|
-
context?:
|
|
30
|
-
):
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
94
|
+
context?: Record<string, unknown>
|
|
95
|
+
): ErrorToolResponse {
|
|
96
|
+
const errorObj = normalizeErrorToLike(error);
|
|
97
|
+
const errorType = this.categorizeError(errorObj);
|
|
98
|
+
const userMessage = this.getUserFriendlyMessage(errorType, errorObj);
|
|
99
|
+
const retriable = this.isRetriable(errorObj);
|
|
100
|
+
const scope = (context?.scope as string) || `tool-call/${toolName}`;
|
|
101
|
+
const errorMessage = errorObj.message || String(error);
|
|
102
|
+
const errorStack = errorObj.stack;
|
|
103
|
+
|
|
36
104
|
log.error(`Tool ${toolName} failed:`, {
|
|
37
105
|
type: errorType,
|
|
38
|
-
message:
|
|
106
|
+
message: errorMessage,
|
|
39
107
|
retriable,
|
|
40
108
|
scope,
|
|
41
109
|
context
|
|
42
110
|
});
|
|
43
111
|
|
|
44
|
-
|
|
112
|
+
const response: ErrorToolResponse = {
|
|
45
113
|
success: false,
|
|
46
114
|
error: userMessage,
|
|
47
115
|
message: `Failed to execute ${toolName}: ${userMessage}`,
|
|
48
|
-
retriable
|
|
49
|
-
scope
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
116
|
+
retriable,
|
|
117
|
+
scope
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Add debug info in development
|
|
121
|
+
if (process.env.NODE_ENV === 'development') {
|
|
122
|
+
response._debug = {
|
|
123
|
+
errorType,
|
|
124
|
+
originalError: errorMessage,
|
|
125
|
+
stack: errorStack,
|
|
126
|
+
context,
|
|
127
|
+
retriable,
|
|
128
|
+
scope
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return response;
|
|
62
133
|
}
|
|
63
134
|
|
|
64
135
|
/**
|
|
65
136
|
* Categorize error by type
|
|
137
|
+
* @param error - The error to categorize
|
|
66
138
|
*/
|
|
67
|
-
private static categorizeError(error:
|
|
68
|
-
const
|
|
139
|
+
private static categorizeError(error: ErrorLike | Error | string): ErrorType {
|
|
140
|
+
const errorObj = typeof error === 'object' ? error as ErrorLike : null;
|
|
141
|
+
const explicitType = (errorObj?.type || errorObj?.errorType || '').toString().toUpperCase();
|
|
69
142
|
if (explicitType && Object.values(ErrorType).includes(explicitType as ErrorType)) {
|
|
70
143
|
return explicitType as ErrorType;
|
|
71
144
|
}
|
|
72
145
|
|
|
73
|
-
const errorMessage =
|
|
146
|
+
const errorMessage = (errorObj?.message || String(error)).toLowerCase();
|
|
74
147
|
|
|
75
148
|
// Connection errors
|
|
76
149
|
if (
|
|
@@ -122,49 +195,59 @@ export class ErrorHandler {
|
|
|
122
195
|
|
|
123
196
|
/**
|
|
124
197
|
* Get user-friendly error message
|
|
198
|
+
* @param type - The categorized error type
|
|
199
|
+
* @param error - The original error
|
|
125
200
|
*/
|
|
126
|
-
private static getUserFriendlyMessage(type: ErrorType, error:
|
|
127
|
-
const originalMessage = error
|
|
201
|
+
private static getUserFriendlyMessage(type: ErrorType, error: ErrorLike | Error | string): string {
|
|
202
|
+
const originalMessage = (typeof error === 'object' && error !== null && 'message' in error)
|
|
203
|
+
? (error as { message?: string }).message || String(error)
|
|
204
|
+
: String(error);
|
|
128
205
|
|
|
129
206
|
switch (type) {
|
|
130
207
|
case ErrorType.CONNECTION:
|
|
131
208
|
return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
|
|
132
|
-
|
|
209
|
+
|
|
133
210
|
case ErrorType.VALIDATION:
|
|
134
211
|
return `Invalid input: ${originalMessage}`;
|
|
135
|
-
|
|
212
|
+
|
|
136
213
|
case ErrorType.UNREAL_ENGINE:
|
|
137
214
|
return `Unreal Engine error: ${originalMessage}`;
|
|
138
|
-
|
|
215
|
+
|
|
139
216
|
case ErrorType.PARAMETER:
|
|
140
217
|
return `Invalid parameters: ${originalMessage}`;
|
|
141
|
-
|
|
218
|
+
|
|
142
219
|
case ErrorType.TIMEOUT:
|
|
143
220
|
return 'Operation timed out. Unreal Engine may be busy or unresponsive.';
|
|
144
|
-
|
|
221
|
+
|
|
145
222
|
case ErrorType.EXECUTION:
|
|
146
223
|
return `Execution failed: ${originalMessage}`;
|
|
147
|
-
|
|
224
|
+
|
|
148
225
|
default:
|
|
149
226
|
return originalMessage;
|
|
150
227
|
}
|
|
151
228
|
}
|
|
152
229
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Determine if an error is likely retriable
|
|
232
|
+
* @param error - The error to check
|
|
233
|
+
*/
|
|
234
|
+
private static isRetriable(error: ErrorLike | Error | string): boolean {
|
|
155
235
|
try {
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
236
|
+
const errorObj = typeof error === 'object' ? error as ErrorLike : null;
|
|
237
|
+
const code = (errorObj?.code || '').toString().toUpperCase();
|
|
238
|
+
const msg = (errorObj?.message || String(error) || '').toLowerCase();
|
|
239
|
+
const status = Number(errorObj?.response?.status);
|
|
240
|
+
if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE'].includes(code)) return true;
|
|
160
241
|
if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
|
|
161
242
|
if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
|
|
162
|
-
} catch {}
|
|
243
|
+
} catch { }
|
|
163
244
|
return false;
|
|
164
245
|
}
|
|
165
246
|
|
|
166
247
|
/**
|
|
167
248
|
* Retry a function with exponential backoff
|
|
249
|
+
* @param fn - The async function to retry
|
|
250
|
+
* @param options - Retry configuration options
|
|
168
251
|
*/
|
|
169
252
|
static async retryWithBackoff<T>(
|
|
170
253
|
fn: () => Promise<T>,
|
|
@@ -173,14 +256,14 @@ export class ErrorHandler {
|
|
|
173
256
|
initialDelay?: number;
|
|
174
257
|
maxDelay?: number;
|
|
175
258
|
backoffMultiplier?: number;
|
|
176
|
-
shouldRetry?: (error:
|
|
259
|
+
shouldRetry?: (error: ErrorLike | Error | unknown) => boolean;
|
|
177
260
|
} = {}
|
|
178
261
|
): Promise<T> {
|
|
179
262
|
const maxRetries = options.maxRetries ?? 3;
|
|
180
263
|
const initialDelay = options.initialDelay ?? 1000;
|
|
181
264
|
const maxDelay = options.maxDelay ?? 10000;
|
|
182
265
|
const multiplier = options.backoffMultiplier ?? 2;
|
|
183
|
-
const shouldRetry = options.shouldRetry ?? ((err) => this.isRetriable(err));
|
|
266
|
+
const shouldRetry = options.shouldRetry ?? ((err: unknown) => this.isRetriable(err as ErrorLike));
|
|
184
267
|
|
|
185
268
|
let delay = initialDelay;
|
|
186
269
|
|
|
@@ -191,7 +274,7 @@ export class ErrorHandler {
|
|
|
191
274
|
if (attempt === maxRetries || !shouldRetry(error)) {
|
|
192
275
|
throw error;
|
|
193
276
|
}
|
|
194
|
-
|
|
277
|
+
|
|
195
278
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
196
279
|
delay = Math.min(delay * multiplier, maxDelay);
|
|
197
280
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for normalize utility functions
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
toVec3Object,
|
|
7
|
+
toRotObject,
|
|
8
|
+
toVec3Tuple,
|
|
9
|
+
toRotTuple,
|
|
10
|
+
toFiniteNumber,
|
|
11
|
+
normalizePartialVector,
|
|
12
|
+
normalizeTransformInput
|
|
13
|
+
} from './normalize.js';
|
|
14
|
+
|
|
15
|
+
describe('toVec3Object', () => {
|
|
16
|
+
it('converts object input to Vector3', () => {
|
|
17
|
+
const result = toVec3Object({ x: 1, y: 2, z: 3 });
|
|
18
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('converts array input to Vector3', () => {
|
|
22
|
+
const result = toVec3Object([1, 2, 3]);
|
|
23
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns null for invalid input', () => {
|
|
27
|
+
expect(toVec3Object('invalid')).toBeNull();
|
|
28
|
+
expect(toVec3Object(null)).toBeNull();
|
|
29
|
+
expect(toVec3Object(undefined)).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('returns null for incomplete object', () => {
|
|
33
|
+
expect(toVec3Object({ x: 1, y: 2 })).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('handles zero values', () => {
|
|
37
|
+
const result = toVec3Object({ x: 0, y: 0, z: 0 });
|
|
38
|
+
expect(result).toEqual({ x: 0, y: 0, z: 0 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('toRotObject', () => {
|
|
43
|
+
it('converts object input to Rotator', () => {
|
|
44
|
+
const result = toRotObject({ pitch: 10, yaw: 20, roll: 30 });
|
|
45
|
+
expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('converts array input to Rotator', () => {
|
|
49
|
+
const result = toRotObject([10, 20, 30]);
|
|
50
|
+
expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns null for invalid input', () => {
|
|
54
|
+
expect(toRotObject('invalid')).toBeNull();
|
|
55
|
+
expect(toRotObject(null)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('toVec3Tuple', () => {
|
|
60
|
+
it('converts object to tuple', () => {
|
|
61
|
+
const result = toVec3Tuple({ x: 1, y: 2, z: 3 });
|
|
62
|
+
expect(result).toEqual([1, 2, 3]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('passes through valid array', () => {
|
|
66
|
+
const result = toVec3Tuple([1, 2, 3]);
|
|
67
|
+
expect(result).toEqual([1, 2, 3]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns null for invalid input', () => {
|
|
71
|
+
expect(toVec3Tuple([1, 2])).toBeNull();
|
|
72
|
+
expect(toVec3Tuple(null)).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('toRotTuple', () => {
|
|
77
|
+
it('converts object to tuple', () => {
|
|
78
|
+
const result = toRotTuple({ pitch: 10, yaw: 20, roll: 30 });
|
|
79
|
+
expect(result).toEqual([10, 20, 30]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('passes through valid array', () => {
|
|
83
|
+
const result = toRotTuple([10, 20, 30]);
|
|
84
|
+
expect(result).toEqual([10, 20, 30]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('toFiniteNumber', () => {
|
|
89
|
+
it('accepts valid numbers', () => {
|
|
90
|
+
expect(toFiniteNumber(42)).toBe(42);
|
|
91
|
+
expect(toFiniteNumber(0)).toBe(0);
|
|
92
|
+
expect(toFiniteNumber(-5)).toBe(-5);
|
|
93
|
+
expect(toFiniteNumber(3.14)).toBe(3.14);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('parses string numbers', () => {
|
|
97
|
+
expect(toFiniteNumber('42')).toBe(42);
|
|
98
|
+
expect(toFiniteNumber('3.14')).toBe(3.14);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns undefined for invalid input', () => {
|
|
102
|
+
expect(toFiniteNumber('not a number')).toBeUndefined();
|
|
103
|
+
expect(toFiniteNumber(NaN)).toBeUndefined();
|
|
104
|
+
expect(toFiniteNumber(Infinity)).toBeUndefined();
|
|
105
|
+
expect(toFiniteNumber(null)).toBeUndefined();
|
|
106
|
+
expect(toFiniteNumber(undefined)).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('normalizePartialVector', () => {
|
|
111
|
+
it('normalizes complete vectors', () => {
|
|
112
|
+
const result = normalizePartialVector({ x: 1, y: 2, z: 3 });
|
|
113
|
+
expect(result).toEqual({ x: 1, y: 2, z: 3 });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('handles partial vectors', () => {
|
|
117
|
+
const result = normalizePartialVector({ x: 1 });
|
|
118
|
+
expect(result).toBeDefined();
|
|
119
|
+
expect(result?.x).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('uses alternate keys for reading input', () => {
|
|
123
|
+
const result = normalizePartialVector(
|
|
124
|
+
{ pitch: 10, yaw: 20, roll: 30 },
|
|
125
|
+
['pitch', 'yaw', 'roll']
|
|
126
|
+
);
|
|
127
|
+
// alternateKeys are used to READ from input, but output is still x/y/z
|
|
128
|
+
expect(result).toEqual({ x: 10, y: 20, z: 30 });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('returns undefined for invalid input', () => {
|
|
132
|
+
expect(normalizePartialVector(null)).toBeUndefined();
|
|
133
|
+
expect(normalizePartialVector('string')).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('normalizeTransformInput', () => {
|
|
138
|
+
it('normalizes complete transform', () => {
|
|
139
|
+
const result = normalizeTransformInput({
|
|
140
|
+
location: { x: 0, y: 0, z: 100 },
|
|
141
|
+
rotation: { pitch: 0, yaw: 90, roll: 0 },
|
|
142
|
+
scale: { x: 1, y: 1, z: 1 }
|
|
143
|
+
});
|
|
144
|
+
expect(result).toBeDefined();
|
|
145
|
+
expect(result?.location).toBeDefined();
|
|
146
|
+
expect(result?.rotation).toBeDefined();
|
|
147
|
+
expect(result?.scale).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles partial transforms', () => {
|
|
151
|
+
const result = normalizeTransformInput({
|
|
152
|
+
location: { x: 100, y: 200, z: 300 }
|
|
153
|
+
});
|
|
154
|
+
expect(result).toBeDefined();
|
|
155
|
+
expect(result?.location).toBeDefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns undefined for invalid input', () => {
|
|
159
|
+
expect(normalizeTransformInput(null)).toBeUndefined();
|
|
160
|
+
expect(normalizeTransformInput('invalid')).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
package/src/utils/normalize.ts
CHANGED
|
@@ -4,47 +4,66 @@ export interface Rot3Obj { pitch: number; yaw: number; roll: number; }
|
|
|
4
4
|
export type Vec3Tuple = [number, number, number];
|
|
5
5
|
export type Rot3Tuple = [number, number, number];
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/** Input that may represent a 3D vector */
|
|
8
|
+
type VectorInput = Vec3Obj | Vec3Tuple | Record<string, unknown> | unknown[];
|
|
9
|
+
|
|
10
|
+
/** Input that may represent a 3D rotation */
|
|
11
|
+
type RotationInput = Rot3Obj | Rot3Tuple | Record<string, unknown> | unknown[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert various input formats to a Vec3 object
|
|
15
|
+
* @param input - Array, object with x/y/z or X/Y/Z properties
|
|
16
|
+
*/
|
|
17
|
+
export function toVec3Object(input: VectorInput | unknown): Vec3Obj | null {
|
|
8
18
|
try {
|
|
9
19
|
if (Array.isArray(input) && input.length === 3) {
|
|
10
20
|
const [x, y, z] = input;
|
|
11
21
|
if ([x, y, z].every(v => typeof v === 'number' && isFinite(v))) {
|
|
12
|
-
return { x, y, z };
|
|
22
|
+
return { x: x as number, y: y as number, z: z as number };
|
|
13
23
|
}
|
|
14
24
|
}
|
|
15
|
-
if (input && typeof input === 'object') {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
25
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
26
|
+
const obj = input as Record<string, unknown>;
|
|
27
|
+
const x = Number(obj.x ?? obj.X);
|
|
28
|
+
const y = Number(obj.y ?? obj.Y);
|
|
29
|
+
const z = Number(obj.z ?? obj.Z);
|
|
19
30
|
if ([x, y, z].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
|
|
20
31
|
return { x, y, z };
|
|
21
32
|
}
|
|
22
33
|
}
|
|
23
|
-
} catch {}
|
|
34
|
+
} catch { }
|
|
24
35
|
return null;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Convert various input formats to a Rotation object
|
|
40
|
+
* @param input - Array, object with pitch/yaw/roll or Pitch/Yaw/Roll properties
|
|
41
|
+
*/
|
|
42
|
+
export function toRotObject(input: RotationInput | unknown): Rot3Obj | null {
|
|
28
43
|
try {
|
|
29
44
|
if (Array.isArray(input) && input.length === 3) {
|
|
30
45
|
const [pitch, yaw, roll] = input;
|
|
31
46
|
if ([pitch, yaw, roll].every(v => typeof v === 'number' && isFinite(v))) {
|
|
32
|
-
return { pitch, yaw, roll };
|
|
47
|
+
return { pitch: pitch as number, yaw: yaw as number, roll: roll as number };
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
|
-
if (input && typeof input === 'object') {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
50
|
+
if (input && typeof input === 'object' && !Array.isArray(input)) {
|
|
51
|
+
const obj = input as Record<string, unknown>;
|
|
52
|
+
const pitch = Number(obj.pitch ?? obj.Pitch);
|
|
53
|
+
const yaw = Number(obj.yaw ?? obj.Yaw);
|
|
54
|
+
const roll = Number(obj.roll ?? obj.Roll);
|
|
39
55
|
if ([pitch, yaw, roll].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
|
|
40
56
|
return { pitch, yaw, roll };
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
|
-
} catch {}
|
|
59
|
+
} catch { }
|
|
44
60
|
return null;
|
|
45
61
|
}
|
|
46
62
|
|
|
47
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Convert vector input to a tuple format [x, y, z]
|
|
65
|
+
*/
|
|
66
|
+
export function toVec3Tuple(input: VectorInput | unknown): Vec3Tuple | null {
|
|
48
67
|
const vec = toVec3Object(input);
|
|
49
68
|
if (!vec) {
|
|
50
69
|
return null;
|
|
@@ -53,7 +72,10 @@ export function toVec3Tuple(input: any): Vec3Tuple | null {
|
|
|
53
72
|
return [x, y, z];
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Convert rotation input to a tuple format [pitch, yaw, roll]
|
|
77
|
+
*/
|
|
78
|
+
export function toRotTuple(input: RotationInput | unknown): Rot3Tuple | null {
|
|
57
79
|
const rot = toRotObject(input);
|
|
58
80
|
if (!rot) {
|
|
59
81
|
return null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function sanitizePath(path: string, allowedRoots: string[] = ['/Game', '/Engine']): string {
|
|
2
|
+
if (!path || typeof path !== 'string') {
|
|
3
|
+
throw new Error('Invalid path: must be a non-empty string');
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const trimmed = path.trim();
|
|
7
|
+
if (trimmed.length === 0) {
|
|
8
|
+
throw new Error('Invalid path: cannot be empty');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Normalize separators
|
|
12
|
+
const normalized = trimmed.replace(/\\/g, '/');
|
|
13
|
+
|
|
14
|
+
// Prevent directory traversal
|
|
15
|
+
if (normalized.includes('..')) {
|
|
16
|
+
throw new Error('Invalid path: directory traversal (..) is not allowed');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Ensure path starts with a valid root
|
|
20
|
+
// We check case-insensitive for the root prefix to be user-friendly,
|
|
21
|
+
// but Unreal paths are typically case-insensitive anyway.
|
|
22
|
+
const isAllowed = allowedRoots.some(root =>
|
|
23
|
+
normalized.toLowerCase() === root.toLowerCase() ||
|
|
24
|
+
normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (!isAllowed) {
|
|
28
|
+
throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Basic character validation (Unreal strictness)
|
|
32
|
+
// Blocks: < > : " | ? * (Windows reserved) and control characters
|
|
33
|
+
// allowing spaces, dots, underscores, dashes, slashes
|
|
34
|
+
// Note: Unreal allows spaces in some contexts but it's often safer to restrict them if strict mode is desired.
|
|
35
|
+
// For now, we block the definitely invalid ones.
|
|
36
|
+
// eslint-disable-next-line no-control-regex
|
|
37
|
+
const invalidChars = /[<>:"|?*\x00-\x1f]/;
|
|
38
|
+
if (invalidChars.test(normalized)) {
|
|
39
|
+
throw new Error('Invalid path: contains illegal characters');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
@@ -1,39 +1,44 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { StandardActionResponse } from '../types/tool-interfaces.js';
|
|
2
|
+
import { cleanObject } from './safe-json.js';
|
|
2
3
|
|
|
3
4
|
export class ResponseFactory {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
content.push({
|
|
9
|
-
type: 'text',
|
|
10
|
-
text: JSON.stringify(data, null, 2)
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Create a standard success response
|
|
7
|
+
*/
|
|
8
|
+
static success(data: any, message: string = 'Operation successful'): StandardActionResponse {
|
|
14
9
|
return {
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
success: true,
|
|
11
|
+
message,
|
|
12
|
+
data: cleanObject(data)
|
|
17
13
|
};
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Create a standard error response
|
|
18
|
+
* @param error The error object or message
|
|
19
|
+
* @param defaultMessage Fallback message if error is not an Error object
|
|
20
|
+
*/
|
|
21
|
+
static error(error: any, defaultMessage: string = 'Operation failed'): StandardActionResponse {
|
|
22
|
+
const errorMessage = error instanceof Error ? error.message : String(error || defaultMessage);
|
|
23
|
+
|
|
24
|
+
// Log the full error for debugging (internal logs) but return a clean message to the client
|
|
25
|
+
console.error('[ResponseFactory] Error:', error);
|
|
23
26
|
|
|
24
27
|
return {
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
success: false,
|
|
29
|
+
message: errorMessage,
|
|
30
|
+
data: null
|
|
27
31
|
};
|
|
28
32
|
}
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Create a validation error response
|
|
36
|
+
*/
|
|
37
|
+
static validationError(message: string): StandardActionResponse {
|
|
31
38
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}],
|
|
36
|
-
isError: false
|
|
39
|
+
success: false,
|
|
40
|
+
message: `Validation Error: ${message}`,
|
|
41
|
+
data: null
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
}
|