unreal-engine-mcp-server 0.5.1 → 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/.github/workflows/publish-mcp.yml +1 -4
- package/.github/workflows/release-drafter.yml +2 -1
- package/CHANGELOG.md +38 -0
- 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/tools/actors.d.ts +19 -1
- package/dist/tools/actors.js +15 -5
- package/dist/tools/assets.js +1 -1
- package/dist/tools/blueprint.d.ts +12 -0
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/editor.js +3 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -8
- 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/level.js +3 -3
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +4 -4
- package/dist/tools/materials.d.ts +1 -1
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +6 -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/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/handler-mapping.md +4 -2
- package/package.json +1 -1
- 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/server.json +3 -3
- 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/server/tool-registry.ts +3 -3
- package/src/tools/actors.ts +44 -19
- package/src/tools/assets.ts +3 -3
- package/src/tools/blueprint.ts +115 -49
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/editor.ts +4 -3
- package/src/tools/handlers/actor-handlers.ts +14 -9
- package/src/tools/handlers/sequence-handlers.ts +86 -63
- package/src/tools/introspection.ts +7 -7
- package/src/tools/level.ts +6 -6
- package/src/tools/lighting.ts +19 -19
- package/src/tools/materials.ts +1 -1
- package/src/tools/sequence.ts +1 -1
- package/src/tools/ui.ts +1 -1
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +46 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.ts +38 -16
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/unreal-command-queue.ts +13 -1
package/src/unreal-bridge.ts
CHANGED
|
@@ -5,14 +5,59 @@ import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT } from './constants.js
|
|
|
5
5
|
import { UnrealCommandQueue } from './utils/unreal-command-queue.js';
|
|
6
6
|
import { CommandValidator } from './utils/command-validator.js';
|
|
7
7
|
|
|
8
|
+
/** Connection event payload for automation bridge events */
|
|
9
|
+
interface ConnectionEventInfo {
|
|
10
|
+
host?: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
reason?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Result object from automation requests */
|
|
18
|
+
interface AutomationResult {
|
|
19
|
+
value?: unknown;
|
|
20
|
+
propertyValue?: unknown;
|
|
21
|
+
message?: string;
|
|
22
|
+
warnings?: string[];
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Subsystems feature flags */
|
|
27
|
+
interface SubsystemFlags {
|
|
28
|
+
unrealEditor?: boolean;
|
|
29
|
+
levelEditor?: boolean;
|
|
30
|
+
editorActor?: boolean;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Engine version result */
|
|
35
|
+
interface EngineVersionResult {
|
|
36
|
+
version?: string;
|
|
37
|
+
major?: number;
|
|
38
|
+
minor?: number;
|
|
39
|
+
patch?: number;
|
|
40
|
+
isUE56OrAbove?: boolean;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Console command response */
|
|
45
|
+
interface ConsoleCommandResponse {
|
|
46
|
+
success?: boolean;
|
|
47
|
+
message?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
transport?: string;
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
8
53
|
export class UnrealBridge {
|
|
9
54
|
private log = new Logger('UnrealBridge');
|
|
10
55
|
private connected = false;
|
|
11
56
|
private automationBridge?: AutomationBridge;
|
|
12
57
|
private automationBridgeListeners?: {
|
|
13
|
-
connected: (info:
|
|
14
|
-
disconnected: (info:
|
|
15
|
-
handshakeFailed: (info:
|
|
58
|
+
connected: (info: ConnectionEventInfo) => void;
|
|
59
|
+
disconnected: (info: ConnectionEventInfo) => void;
|
|
60
|
+
handshakeFailed: (info: ConnectionEventInfo) => void;
|
|
16
61
|
};
|
|
17
62
|
|
|
18
63
|
// Command queue for throttling
|
|
@@ -35,17 +80,17 @@ export class UnrealBridge {
|
|
|
35
80
|
return;
|
|
36
81
|
}
|
|
37
82
|
|
|
38
|
-
const onConnected = (info:
|
|
83
|
+
const onConnected = (info: ConnectionEventInfo) => {
|
|
39
84
|
this.connected = true;
|
|
40
85
|
this.log.debug('Automation bridge connected', info);
|
|
41
86
|
};
|
|
42
87
|
|
|
43
|
-
const onDisconnected = (info:
|
|
88
|
+
const onDisconnected = (info: ConnectionEventInfo) => {
|
|
44
89
|
this.connected = false;
|
|
45
90
|
this.log.debug('Automation bridge disconnected', info);
|
|
46
91
|
};
|
|
47
92
|
|
|
48
|
-
const onHandshakeFailed = (info:
|
|
93
|
+
const onHandshakeFailed = (info: ConnectionEventInfo) => {
|
|
49
94
|
this.connected = false;
|
|
50
95
|
this.log.warn('Automation bridge handshake failed', info);
|
|
51
96
|
};
|
|
@@ -279,13 +324,13 @@ export class UnrealBridge {
|
|
|
279
324
|
);
|
|
280
325
|
|
|
281
326
|
const success = response.success !== false;
|
|
282
|
-
const rawResult =
|
|
327
|
+
const rawResult: AutomationResult | undefined =
|
|
283
328
|
response.result && typeof response.result === 'object'
|
|
284
329
|
? { ...(response.result as Record<string, unknown>) }
|
|
285
|
-
:
|
|
330
|
+
: undefined;
|
|
286
331
|
const value =
|
|
287
|
-
|
|
288
|
-
|
|
332
|
+
rawResult?.value ??
|
|
333
|
+
rawResult?.propertyValue ??
|
|
289
334
|
(success ? rawResult : undefined);
|
|
290
335
|
|
|
291
336
|
if (success) {
|
|
@@ -297,8 +342,8 @@ export class UnrealBridge {
|
|
|
297
342
|
propertyValue: value,
|
|
298
343
|
transport: 'automation_bridge',
|
|
299
344
|
message: response.message,
|
|
300
|
-
warnings: Array.isArray(
|
|
301
|
-
?
|
|
345
|
+
warnings: Array.isArray(rawResult?.warnings)
|
|
346
|
+
? rawResult.warnings
|
|
302
347
|
: undefined,
|
|
303
348
|
raw: rawResult,
|
|
304
349
|
bridge: {
|
|
@@ -389,10 +434,10 @@ export class UnrealBridge {
|
|
|
389
434
|
);
|
|
390
435
|
|
|
391
436
|
const success = response.success !== false;
|
|
392
|
-
const rawResult =
|
|
437
|
+
const rawResult: AutomationResult | undefined =
|
|
393
438
|
response.result && typeof response.result === 'object'
|
|
394
439
|
? { ...(response.result as Record<string, unknown>) }
|
|
395
|
-
:
|
|
440
|
+
: undefined;
|
|
396
441
|
|
|
397
442
|
if (success) {
|
|
398
443
|
return {
|
|
@@ -401,7 +446,7 @@ export class UnrealBridge {
|
|
|
401
446
|
propertyName,
|
|
402
447
|
message:
|
|
403
448
|
response.message ||
|
|
404
|
-
(typeof
|
|
449
|
+
(typeof rawResult?.message === 'string' ? rawResult.message : undefined),
|
|
405
450
|
transport: 'automation_bridge',
|
|
406
451
|
raw: rawResult,
|
|
407
452
|
bridge: {
|
|
@@ -469,14 +514,14 @@ export class UnrealBridge {
|
|
|
469
514
|
throw new Error('Automation bridge not connected');
|
|
470
515
|
}
|
|
471
516
|
|
|
472
|
-
const pluginResp:
|
|
517
|
+
const pluginResp: ConsoleCommandResponse = await this.automationBridge.sendAutomationRequest(
|
|
473
518
|
'console_command',
|
|
474
519
|
{ command: cmdTrimmed },
|
|
475
520
|
{ timeoutMs: 30000 }
|
|
476
521
|
);
|
|
477
522
|
|
|
478
523
|
if (pluginResp && pluginResp.success) {
|
|
479
|
-
return { ...
|
|
524
|
+
return { ...pluginResp, transport: 'automation_bridge' };
|
|
480
525
|
}
|
|
481
526
|
|
|
482
527
|
const errMsg = pluginResp?.message || pluginResp?.error || 'Plugin execution failed';
|
|
@@ -553,14 +598,14 @@ export class UnrealBridge {
|
|
|
553
598
|
|
|
554
599
|
const bridge = this.getAutomationBridge();
|
|
555
600
|
try {
|
|
556
|
-
const resp
|
|
601
|
+
const resp = await bridge.sendAutomationRequest(
|
|
557
602
|
'system_control',
|
|
558
603
|
{ action: 'get_engine_version' },
|
|
559
604
|
{ timeoutMs: 15000 }
|
|
560
605
|
);
|
|
561
|
-
const raw = resp && typeof resp.result === 'object'
|
|
562
|
-
? (resp.result as
|
|
563
|
-
: (resp?.result ?? resp ?? {}
|
|
606
|
+
const raw: EngineVersionResult = resp && typeof resp.result === 'object'
|
|
607
|
+
? (resp.result as Record<string, unknown>)
|
|
608
|
+
: (resp?.result as Record<string, unknown>) ?? resp ?? {};
|
|
564
609
|
const version = typeof raw.version === 'string' ? raw.version : 'unknown';
|
|
565
610
|
const major = typeof raw.major === 'number' ? raw.major : 0;
|
|
566
611
|
const minor = typeof raw.minor === 'number' ? raw.minor : 0;
|
|
@@ -596,16 +641,16 @@ export class UnrealBridge {
|
|
|
596
641
|
|
|
597
642
|
const bridge = this.getAutomationBridge();
|
|
598
643
|
try {
|
|
599
|
-
const resp
|
|
644
|
+
const resp = await bridge.sendAutomationRequest(
|
|
600
645
|
'system_control',
|
|
601
646
|
{ action: 'get_feature_flags' },
|
|
602
647
|
{ timeoutMs: 15000 }
|
|
603
648
|
);
|
|
604
649
|
const raw = resp && typeof resp.result === 'object'
|
|
605
|
-
? (resp.result as
|
|
606
|
-
: (resp?.result ?? resp ?? {}
|
|
607
|
-
const subs = raw && typeof raw.subsystems === 'object'
|
|
608
|
-
? (raw.subsystems as
|
|
650
|
+
? (resp.result as Record<string, unknown>)
|
|
651
|
+
: (resp?.result as Record<string, unknown>) ?? resp ?? {};
|
|
652
|
+
const subs: SubsystemFlags = raw && typeof raw.subsystems === 'object'
|
|
653
|
+
? (raw.subsystems as SubsystemFlags)
|
|
609
654
|
: {};
|
|
610
655
|
return {
|
|
611
656
|
subsystems: {
|
|
@@ -1,28 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates console commands before execution to prevent dangerous operations.
|
|
3
|
+
* Blocks crash-inducing commands, shell injection, and Python execution.
|
|
4
|
+
*/
|
|
1
5
|
export class CommandValidator {
|
|
6
|
+
/**
|
|
7
|
+
* Commands that can crash the engine or cause severe instability.
|
|
8
|
+
* These are blocked unconditionally.
|
|
9
|
+
*/
|
|
2
10
|
private static readonly DANGEROUS_COMMANDS = [
|
|
3
|
-
|
|
11
|
+
// Engine termination commands
|
|
12
|
+
'quit', 'exit', 'kill', 'crash',
|
|
13
|
+
// Crash-inducing commands
|
|
14
|
+
'r.gpucrash', 'r.crash', 'debug crash', 'forcecrash', 'debug break',
|
|
15
|
+
'assert false', 'check(false)',
|
|
16
|
+
// View buffer commands that can crash on some hardware
|
|
4
17
|
'viewmode visualizebuffer basecolor',
|
|
5
18
|
'viewmode visualizebuffer worldnormal',
|
|
6
|
-
|
|
7
|
-
'buildpaths',
|
|
8
|
-
|
|
9
|
-
'obj garbage', 'obj list', 'memreport'
|
|
19
|
+
// Heavy operations that can cause access violations if systems not initialized
|
|
20
|
+
'buildpaths', 'rebuildnavigation',
|
|
21
|
+
// Heavy debug commands that can stall or crash
|
|
22
|
+
'obj garbage', 'obj list', 'memreport',
|
|
23
|
+
// Potentially destructive without proper setup
|
|
24
|
+
'delete', 'destroy'
|
|
10
25
|
];
|
|
11
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Tokens that indicate shell injection or external system access attempts.
|
|
29
|
+
* Any command containing these is blocked.
|
|
30
|
+
*/
|
|
12
31
|
private static readonly FORBIDDEN_TOKENS = [
|
|
32
|
+
// Shell commands (Windows/Unix)
|
|
13
33
|
'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
|
|
14
34
|
'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
|
|
35
|
+
// Python injection attempts
|
|
15
36
|
'import os', 'import subprocess', 'subprocess.', 'os.system',
|
|
16
37
|
'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
|
|
17
38
|
'with open', 'open(', 'write(', 'read('
|
|
18
39
|
];
|
|
19
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Patterns that indicate obviously invalid commands.
|
|
43
|
+
* Used to warn about likely typos or invalid input.
|
|
44
|
+
*/
|
|
20
45
|
private static readonly INVALID_PATTERNS = [
|
|
21
46
|
/^\d+$/, // Just numbers
|
|
22
47
|
/^invalid_command/i,
|
|
23
48
|
/^this_is_not_a_valid/i,
|
|
24
49
|
];
|
|
25
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Validates a console command for safety before execution.
|
|
53
|
+
* @param command - The console command string to validate
|
|
54
|
+
* @throws Error if the command is dangerous, contains forbidden tokens, or is invalid
|
|
55
|
+
*/
|
|
26
56
|
static validate(command: string): void {
|
|
27
57
|
if (!command || typeof command !== 'string') {
|
|
28
58
|
throw new Error('Invalid command: must be a non-empty string');
|
|
@@ -56,11 +86,22 @@ export class CommandValidator {
|
|
|
56
86
|
}
|
|
57
87
|
}
|
|
58
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Check if a command looks like an obviously invalid or mistyped command.
|
|
91
|
+
* @param command - The command to check
|
|
92
|
+
* @returns true if the command matches known invalid patterns
|
|
93
|
+
*/
|
|
59
94
|
static isLikelyInvalid(command: string): boolean {
|
|
60
95
|
const cmdTrimmed = command.trim();
|
|
61
96
|
return this.INVALID_PATTERNS.some(pattern => pattern.test(cmdTrimmed));
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Get the priority level of a command for throttling purposes.
|
|
101
|
+
* Lower numbers indicate heavier operations that need more throttling.
|
|
102
|
+
* @param command - The command to evaluate
|
|
103
|
+
* @returns Priority level (1=heavy, 5=medium, 7=default, 8-9=light)
|
|
104
|
+
*/
|
|
64
105
|
static getPriority(command: string): number {
|
|
65
106
|
if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
|
|
66
107
|
return 1; // Heavy operation
|
|
@@ -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
|
}
|
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;
|