unreal-engine-mcp-server 0.2.1
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/.dockerignore +57 -0
- package/.env.production +25 -0
- package/.eslintrc.json +54 -0
- package/.github/workflows/publish-mcp.yml +75 -0
- package/Dockerfile +54 -0
- package/LICENSE +21 -0
- package/Public/icon.png +0 -0
- package/README.md +209 -0
- package/claude_desktop_config_example.json +13 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +484 -0
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.js +38 -0
- package/dist/python-utils.d.ts +29 -0
- package/dist/python-utils.js +54 -0
- package/dist/resources/actors.d.ts +13 -0
- package/dist/resources/actors.js +83 -0
- package/dist/resources/assets.d.ts +23 -0
- package/dist/resources/assets.js +245 -0
- package/dist/resources/levels.d.ts +17 -0
- package/dist/resources/levels.js +94 -0
- package/dist/tools/actors.d.ts +51 -0
- package/dist/tools/actors.js +459 -0
- package/dist/tools/animation.d.ts +196 -0
- package/dist/tools/animation.js +579 -0
- package/dist/tools/assets.d.ts +21 -0
- package/dist/tools/assets.js +304 -0
- package/dist/tools/audio.d.ts +170 -0
- package/dist/tools/audio.js +416 -0
- package/dist/tools/blueprint.d.ts +144 -0
- package/dist/tools/blueprint.js +652 -0
- package/dist/tools/build_environment_advanced.d.ts +66 -0
- package/dist/tools/build_environment_advanced.js +484 -0
- package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
- package/dist/tools/consolidated-tool-definitions.js +607 -0
- package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
- package/dist/tools/consolidated-tool-handlers.js +1050 -0
- package/dist/tools/debug.d.ts +185 -0
- package/dist/tools/debug.js +265 -0
- package/dist/tools/editor.d.ts +88 -0
- package/dist/tools/editor.js +365 -0
- package/dist/tools/engine.d.ts +30 -0
- package/dist/tools/engine.js +36 -0
- package/dist/tools/foliage.d.ts +155 -0
- package/dist/tools/foliage.js +525 -0
- package/dist/tools/introspection.d.ts +98 -0
- package/dist/tools/introspection.js +683 -0
- package/dist/tools/landscape.d.ts +158 -0
- package/dist/tools/landscape.js +375 -0
- package/dist/tools/level.d.ts +110 -0
- package/dist/tools/level.js +362 -0
- package/dist/tools/lighting.d.ts +159 -0
- package/dist/tools/lighting.js +1179 -0
- package/dist/tools/materials.d.ts +34 -0
- package/dist/tools/materials.js +146 -0
- package/dist/tools/niagara.d.ts +145 -0
- package/dist/tools/niagara.js +289 -0
- package/dist/tools/performance.d.ts +163 -0
- package/dist/tools/performance.js +412 -0
- package/dist/tools/physics.d.ts +189 -0
- package/dist/tools/physics.js +784 -0
- package/dist/tools/rc.d.ts +110 -0
- package/dist/tools/rc.js +363 -0
- package/dist/tools/sequence.d.ts +112 -0
- package/dist/tools/sequence.js +675 -0
- package/dist/tools/tool-definitions.d.ts +4919 -0
- package/dist/tools/tool-definitions.js +891 -0
- package/dist/tools/tool-handlers.d.ts +47 -0
- package/dist/tools/tool-handlers.js +830 -0
- package/dist/tools/ui.d.ts +171 -0
- package/dist/tools/ui.js +337 -0
- package/dist/tools/visual.d.ts +29 -0
- package/dist/tools/visual.js +67 -0
- package/dist/types/env.d.ts +10 -0
- package/dist/types/env.js +18 -0
- package/dist/types/index.d.ts +323 -0
- package/dist/types/index.js +28 -0
- package/dist/types/tool-types.d.ts +274 -0
- package/dist/types/tool-types.js +13 -0
- package/dist/unreal-bridge.d.ts +126 -0
- package/dist/unreal-bridge.js +992 -0
- package/dist/utils/cache-manager.d.ts +64 -0
- package/dist/utils/cache-manager.js +176 -0
- package/dist/utils/error-handler.d.ts +66 -0
- package/dist/utils/error-handler.js +243 -0
- package/dist/utils/errors.d.ts +133 -0
- package/dist/utils/errors.js +256 -0
- package/dist/utils/http.d.ts +26 -0
- package/dist/utils/http.js +135 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +32 -0
- package/dist/utils/normalize.d.ts +17 -0
- package/dist/utils/normalize.js +49 -0
- package/dist/utils/response-validator.d.ts +34 -0
- package/dist/utils/response-validator.js +121 -0
- package/dist/utils/safe-json.d.ts +4 -0
- package/dist/utils/safe-json.js +97 -0
- package/dist/utils/stdio-redirect.d.ts +2 -0
- package/dist/utils/stdio-redirect.js +20 -0
- package/dist/utils/validation.d.ts +50 -0
- package/dist/utils/validation.js +173 -0
- package/mcp-config-example.json +14 -0
- package/package.json +63 -0
- package/server.json +60 -0
- package/src/cli.ts +7 -0
- package/src/index.ts +543 -0
- package/src/prompts/index.ts +51 -0
- package/src/python/editor_compat.py +181 -0
- package/src/python-utils.ts +57 -0
- package/src/resources/actors.ts +92 -0
- package/src/resources/assets.ts +251 -0
- package/src/resources/levels.ts +83 -0
- package/src/tools/actors.ts +480 -0
- package/src/tools/animation.ts +713 -0
- package/src/tools/assets.ts +305 -0
- package/src/tools/audio.ts +548 -0
- package/src/tools/blueprint.ts +736 -0
- package/src/tools/build_environment_advanced.ts +526 -0
- package/src/tools/consolidated-tool-definitions.ts +619 -0
- package/src/tools/consolidated-tool-handlers.ts +1093 -0
- package/src/tools/debug.ts +368 -0
- package/src/tools/editor.ts +360 -0
- package/src/tools/engine.ts +32 -0
- package/src/tools/foliage.ts +652 -0
- package/src/tools/introspection.ts +778 -0
- package/src/tools/landscape.ts +523 -0
- package/src/tools/level.ts +410 -0
- package/src/tools/lighting.ts +1316 -0
- package/src/tools/materials.ts +148 -0
- package/src/tools/niagara.ts +312 -0
- package/src/tools/performance.ts +549 -0
- package/src/tools/physics.ts +924 -0
- package/src/tools/rc.ts +437 -0
- package/src/tools/sequence.ts +791 -0
- package/src/tools/tool-definitions.ts +907 -0
- package/src/tools/tool-handlers.ts +941 -0
- package/src/tools/ui.ts +499 -0
- package/src/tools/visual.ts +60 -0
- package/src/types/env.ts +27 -0
- package/src/types/index.ts +414 -0
- package/src/types/tool-types.ts +343 -0
- package/src/unreal-bridge.ts +1118 -0
- package/src/utils/cache-manager.ts +213 -0
- package/src/utils/error-handler.ts +320 -0
- package/src/utils/errors.ts +312 -0
- package/src/utils/http.ts +184 -0
- package/src/utils/logger.ts +30 -0
- package/src/utils/normalize.ts +54 -0
- package/src/utils/response-validator.ts +145 -0
- package/src/utils/safe-json.ts +112 -0
- package/src/utils/stdio-redirect.ts +18 -0
- package/src/utils/validation.ts +212 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Manager for API responses
|
|
3
|
+
* Implements LRU cache with TTL support for optimizing repeated API calls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface CacheEntry<T> {
|
|
7
|
+
data: T;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
hits: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface CacheOptions {
|
|
13
|
+
maxSize?: number;
|
|
14
|
+
defaultTTL?: number; // in milliseconds
|
|
15
|
+
enableMetrics?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CacheMetrics {
|
|
19
|
+
hits: number;
|
|
20
|
+
misses: number;
|
|
21
|
+
evictions: number;
|
|
22
|
+
size: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CacheManager<T = any> {
|
|
26
|
+
private cache: Map<string, CacheEntry<T>>;
|
|
27
|
+
private readonly maxSize: number;
|
|
28
|
+
private readonly defaultTTL: number;
|
|
29
|
+
private readonly enableMetrics: boolean;
|
|
30
|
+
private metrics: CacheMetrics;
|
|
31
|
+
|
|
32
|
+
constructor(options: CacheOptions = {}) {
|
|
33
|
+
this.cache = new Map();
|
|
34
|
+
this.maxSize = options.maxSize || 100;
|
|
35
|
+
this.defaultTTL = options.defaultTTL || 60000; // 1 minute default
|
|
36
|
+
this.enableMetrics = options.enableMetrics || false;
|
|
37
|
+
this.metrics = {
|
|
38
|
+
hits: 0,
|
|
39
|
+
misses: 0,
|
|
40
|
+
evictions: 0,
|
|
41
|
+
size: 0
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get item from cache
|
|
47
|
+
*/
|
|
48
|
+
get(key: string): T | null {
|
|
49
|
+
const entry = this.cache.get(key);
|
|
50
|
+
|
|
51
|
+
if (!entry) {
|
|
52
|
+
if (this.enableMetrics) this.metrics.misses++;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if expired
|
|
57
|
+
if (Date.now() - entry.timestamp > this.defaultTTL) {
|
|
58
|
+
this.cache.delete(key);
|
|
59
|
+
if (this.enableMetrics) {
|
|
60
|
+
this.metrics.misses++;
|
|
61
|
+
this.metrics.size--;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Update hit count and move to end (LRU)
|
|
67
|
+
entry.hits++;
|
|
68
|
+
this.cache.delete(key);
|
|
69
|
+
this.cache.set(key, entry);
|
|
70
|
+
|
|
71
|
+
if (this.enableMetrics) this.metrics.hits++;
|
|
72
|
+
return entry.data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set item in cache
|
|
77
|
+
*/
|
|
78
|
+
set(key: string, data: T): void {
|
|
79
|
+
// Evict oldest if at max size
|
|
80
|
+
if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
|
|
81
|
+
const firstKey = this.cache.keys().next().value;
|
|
82
|
+
if (firstKey) {
|
|
83
|
+
this.cache.delete(firstKey);
|
|
84
|
+
if (this.enableMetrics) {
|
|
85
|
+
this.metrics.evictions++;
|
|
86
|
+
this.metrics.size--;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entry: CacheEntry<T> = {
|
|
92
|
+
data,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
hits: 0
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this.cache.set(key, entry);
|
|
98
|
+
if (this.enableMetrics) this.metrics.size = this.cache.size;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if key exists and is valid
|
|
103
|
+
*/
|
|
104
|
+
has(key: string): boolean {
|
|
105
|
+
const entry = this.cache.get(key);
|
|
106
|
+
if (!entry) return false;
|
|
107
|
+
|
|
108
|
+
// Check expiration
|
|
109
|
+
if (Date.now() - entry.timestamp > this.defaultTTL) {
|
|
110
|
+
this.cache.delete(key);
|
|
111
|
+
if (this.enableMetrics) this.metrics.size--;
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear specific key or all cache
|
|
120
|
+
*/
|
|
121
|
+
clear(key?: string): void {
|
|
122
|
+
if (key) {
|
|
123
|
+
this.cache.delete(key);
|
|
124
|
+
if (this.enableMetrics) this.metrics.size = this.cache.size;
|
|
125
|
+
} else {
|
|
126
|
+
this.cache.clear();
|
|
127
|
+
if (this.enableMetrics) {
|
|
128
|
+
this.metrics.size = 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get cache metrics
|
|
135
|
+
*/
|
|
136
|
+
getMetrics(): CacheMetrics {
|
|
137
|
+
return { ...this.metrics };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get cache hit rate
|
|
142
|
+
*/
|
|
143
|
+
getHitRate(): number {
|
|
144
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
145
|
+
return total > 0 ? this.metrics.hits / total : 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wrap async function with cache
|
|
150
|
+
*/
|
|
151
|
+
async wrap<R = T>(
|
|
152
|
+
key: string,
|
|
153
|
+
fn: () => Promise<R>
|
|
154
|
+
): Promise<R> {
|
|
155
|
+
// Check cache first
|
|
156
|
+
const cached = this.get(key) as R | null;
|
|
157
|
+
if (cached !== null) {
|
|
158
|
+
return cached;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Execute function and cache result
|
|
162
|
+
const result = await fn();
|
|
163
|
+
this.set(key, result as any);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Batch get multiple keys
|
|
169
|
+
*/
|
|
170
|
+
getBatch(keys: string[]): Map<string, T | null> {
|
|
171
|
+
const results = new Map<string, T | null>();
|
|
172
|
+
for (const key of keys) {
|
|
173
|
+
results.set(key, this.get(key));
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Invalidate cache entries by pattern
|
|
180
|
+
*/
|
|
181
|
+
invalidatePattern(pattern: RegExp): number {
|
|
182
|
+
let count = 0;
|
|
183
|
+
for (const key of this.cache.keys()) {
|
|
184
|
+
if (pattern.test(key)) {
|
|
185
|
+
this.cache.delete(key);
|
|
186
|
+
count++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (this.enableMetrics) {
|
|
190
|
+
this.metrics.size = this.cache.size;
|
|
191
|
+
}
|
|
192
|
+
return count;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Global cache instances for different purposes
|
|
197
|
+
export const assetCache = new CacheManager({
|
|
198
|
+
maxSize: 500,
|
|
199
|
+
defaultTTL: 300000, // 5 minutes for assets
|
|
200
|
+
enableMetrics: true
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const engineCache = new CacheManager({
|
|
204
|
+
maxSize: 50,
|
|
205
|
+
defaultTTL: 600000, // 10 minutes for engine info
|
|
206
|
+
enableMetrics: true
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
export const commandCache = new CacheManager({
|
|
210
|
+
maxSize: 100,
|
|
211
|
+
defaultTTL: 30000, // 30 seconds for commands
|
|
212
|
+
enableMetrics: true
|
|
213
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { Logger } from './logger.js';
|
|
2
|
+
import { BaseToolResponse } from '../types/tool-types.js';
|
|
3
|
+
|
|
4
|
+
const log = new Logger('ErrorHandler');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Error types for categorization
|
|
8
|
+
*/
|
|
9
|
+
export enum ErrorType {
|
|
10
|
+
VALIDATION = 'VALIDATION',
|
|
11
|
+
CONNECTION = 'CONNECTION',
|
|
12
|
+
UNREAL_ENGINE = 'UNREAL_ENGINE',
|
|
13
|
+
PARAMETER = 'PARAMETER',
|
|
14
|
+
EXECUTION = 'EXECUTION',
|
|
15
|
+
TIMEOUT = 'TIMEOUT',
|
|
16
|
+
UNKNOWN = 'UNKNOWN'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Custom error class for MCP tools
|
|
21
|
+
*/
|
|
22
|
+
export class ToolError extends Error {
|
|
23
|
+
constructor(
|
|
24
|
+
public type: ErrorType,
|
|
25
|
+
public toolName: string,
|
|
26
|
+
message: string,
|
|
27
|
+
public originalError?: any
|
|
28
|
+
) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = 'ToolError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Consistent error handling for all tools
|
|
36
|
+
*/
|
|
37
|
+
export class ErrorHandler {
|
|
38
|
+
/**
|
|
39
|
+
* Create a standardized error response
|
|
40
|
+
*/
|
|
41
|
+
static createErrorResponse(
|
|
42
|
+
error: any,
|
|
43
|
+
toolName: string,
|
|
44
|
+
context?: any
|
|
45
|
+
): BaseToolResponse {
|
|
46
|
+
const errorType = this.categorizeError(error);
|
|
47
|
+
const userMessage = this.getUserFriendlyMessage(errorType, error);
|
|
48
|
+
const retriable = this.isRetriable(error);
|
|
49
|
+
const scope = context?.scope || `tool-call/${toolName}`;
|
|
50
|
+
|
|
51
|
+
log.error(`Tool ${toolName} failed:`, {
|
|
52
|
+
type: errorType,
|
|
53
|
+
message: error.message || error,
|
|
54
|
+
retriable,
|
|
55
|
+
scope,
|
|
56
|
+
context
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
error: userMessage,
|
|
62
|
+
message: `Failed to execute ${toolName}: ${userMessage}`,
|
|
63
|
+
retriable: retriable as any,
|
|
64
|
+
scope: scope as any,
|
|
65
|
+
// Add debug info in development
|
|
66
|
+
...(process.env.NODE_ENV === 'development' && {
|
|
67
|
+
_debug: {
|
|
68
|
+
errorType,
|
|
69
|
+
originalError: error.message || String(error),
|
|
70
|
+
stack: error.stack,
|
|
71
|
+
context,
|
|
72
|
+
retriable,
|
|
73
|
+
scope
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
} as any;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a standardized warning response
|
|
81
|
+
*/
|
|
82
|
+
static createWarningResponse(
|
|
83
|
+
message: string,
|
|
84
|
+
result: any,
|
|
85
|
+
toolName: string
|
|
86
|
+
): BaseToolResponse {
|
|
87
|
+
log.warn(`Tool ${toolName} warning: ${message}`);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
warning: message,
|
|
92
|
+
message: `${toolName} completed with warnings`,
|
|
93
|
+
...result
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create a standardized success response
|
|
99
|
+
*/
|
|
100
|
+
static createSuccessResponse(
|
|
101
|
+
message: string,
|
|
102
|
+
data: any = {}
|
|
103
|
+
): BaseToolResponse {
|
|
104
|
+
return {
|
|
105
|
+
success: true,
|
|
106
|
+
message,
|
|
107
|
+
...data
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Categorize error by type
|
|
113
|
+
*/
|
|
114
|
+
private static categorizeError(error: any): ErrorType {
|
|
115
|
+
if (error instanceof ToolError) {
|
|
116
|
+
return error.type;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const errorMessage = error.message?.toLowerCase() || String(error).toLowerCase();
|
|
120
|
+
|
|
121
|
+
// Connection errors
|
|
122
|
+
if (
|
|
123
|
+
errorMessage.includes('econnrefused') ||
|
|
124
|
+
errorMessage.includes('timeout') ||
|
|
125
|
+
errorMessage.includes('connection') ||
|
|
126
|
+
errorMessage.includes('network')
|
|
127
|
+
) {
|
|
128
|
+
return ErrorType.CONNECTION;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validation errors
|
|
132
|
+
if (
|
|
133
|
+
errorMessage.includes('invalid') ||
|
|
134
|
+
errorMessage.includes('required') ||
|
|
135
|
+
errorMessage.includes('must be') ||
|
|
136
|
+
errorMessage.includes('validation')
|
|
137
|
+
) {
|
|
138
|
+
return ErrorType.VALIDATION;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Unreal Engine specific errors
|
|
142
|
+
if (
|
|
143
|
+
errorMessage.includes('unreal') ||
|
|
144
|
+
errorMessage.includes('remote control') ||
|
|
145
|
+
errorMessage.includes('blueprint') ||
|
|
146
|
+
errorMessage.includes('actor') ||
|
|
147
|
+
errorMessage.includes('asset')
|
|
148
|
+
) {
|
|
149
|
+
return ErrorType.UNREAL_ENGINE;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parameter errors
|
|
153
|
+
if (
|
|
154
|
+
errorMessage.includes('parameter') ||
|
|
155
|
+
errorMessage.includes('argument') ||
|
|
156
|
+
errorMessage.includes('missing')
|
|
157
|
+
) {
|
|
158
|
+
return ErrorType.PARAMETER;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Timeout errors
|
|
162
|
+
if (errorMessage.includes('timeout')) {
|
|
163
|
+
return ErrorType.TIMEOUT;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return ErrorType.UNKNOWN;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get user-friendly error message
|
|
171
|
+
*/
|
|
172
|
+
private static getUserFriendlyMessage(type: ErrorType, error: any): string {
|
|
173
|
+
const originalMessage = error.message || String(error);
|
|
174
|
+
|
|
175
|
+
switch (type) {
|
|
176
|
+
case ErrorType.CONNECTION:
|
|
177
|
+
return 'Failed to connect to Unreal Engine. Please ensure Remote Control is enabled and the engine is running.';
|
|
178
|
+
|
|
179
|
+
case ErrorType.VALIDATION:
|
|
180
|
+
return `Invalid input: ${originalMessage}`;
|
|
181
|
+
|
|
182
|
+
case ErrorType.UNREAL_ENGINE:
|
|
183
|
+
return `Unreal Engine error: ${originalMessage}`;
|
|
184
|
+
|
|
185
|
+
case ErrorType.PARAMETER:
|
|
186
|
+
return `Invalid parameters: ${originalMessage}`;
|
|
187
|
+
|
|
188
|
+
case ErrorType.TIMEOUT:
|
|
189
|
+
return 'Operation timed out. Unreal Engine may be busy or unresponsive.';
|
|
190
|
+
|
|
191
|
+
case ErrorType.EXECUTION:
|
|
192
|
+
return `Execution failed: ${originalMessage}`;
|
|
193
|
+
|
|
194
|
+
default:
|
|
195
|
+
return originalMessage;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Determine if an error is likely retriable */
|
|
200
|
+
private static isRetriable(error: any): boolean {
|
|
201
|
+
try {
|
|
202
|
+
const code = (error?.code || '').toString().toUpperCase();
|
|
203
|
+
const msg = (error?.message || String(error) || '').toLowerCase();
|
|
204
|
+
const status = Number((error?.response?.status));
|
|
205
|
+
if (['ECONNRESET','ECONNREFUSED','ETIMEDOUT','EPIPE'].includes(code)) return true;
|
|
206
|
+
if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
|
|
207
|
+
if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
|
|
208
|
+
} catch {}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Wrap async function with error handling
|
|
214
|
+
*/
|
|
215
|
+
static async wrapAsync<T extends BaseToolResponse>(
|
|
216
|
+
toolName: string,
|
|
217
|
+
fn: () => Promise<T>,
|
|
218
|
+
context?: any
|
|
219
|
+
): Promise<T> {
|
|
220
|
+
try {
|
|
221
|
+
const result = await fn();
|
|
222
|
+
|
|
223
|
+
// Ensure result has success field
|
|
224
|
+
if (typeof result === 'object' && result !== null) {
|
|
225
|
+
if (!('success' in result)) {
|
|
226
|
+
(result as any).success = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
} catch (error) {
|
|
232
|
+
return this.createErrorResponse(error, toolName, context) as T;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Validate required parameters
|
|
238
|
+
*/
|
|
239
|
+
static validateParams(
|
|
240
|
+
params: any,
|
|
241
|
+
required: string[],
|
|
242
|
+
toolName: string
|
|
243
|
+
): void {
|
|
244
|
+
if (!params || typeof params !== 'object') {
|
|
245
|
+
throw new ToolError(
|
|
246
|
+
ErrorType.PARAMETER,
|
|
247
|
+
toolName,
|
|
248
|
+
'Invalid parameters: expected object'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const field of required) {
|
|
253
|
+
if (!(field in params) || params[field] === undefined || params[field] === null) {
|
|
254
|
+
throw new ToolError(
|
|
255
|
+
ErrorType.PARAMETER,
|
|
256
|
+
toolName,
|
|
257
|
+
`Missing required parameter: ${field}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Additional validation for common types
|
|
262
|
+
if (field.includes('Path') || field.includes('Name')) {
|
|
263
|
+
if (typeof params[field] !== 'string' || params[field].trim() === '') {
|
|
264
|
+
throw new ToolError(
|
|
265
|
+
ErrorType.PARAMETER,
|
|
266
|
+
toolName,
|
|
267
|
+
`Invalid ${field}: must be a non-empty string`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Handle Unreal Engine specific errors
|
|
276
|
+
*/
|
|
277
|
+
static handleUnrealError(error: any, operation: string): string {
|
|
278
|
+
const errorStr = String(error.message || error).toLowerCase();
|
|
279
|
+
|
|
280
|
+
// Common Unreal errors
|
|
281
|
+
if (errorStr.includes('worldcontext')) {
|
|
282
|
+
return `${operation} completed (WorldContext warnings are normal)`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (errorStr.includes('does not exist')) {
|
|
286
|
+
return `Asset or object not found for ${operation}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (errorStr.includes('access denied') || errorStr.includes('read-only')) {
|
|
290
|
+
return `Permission denied for ${operation}. Check file permissions.`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (errorStr.includes('already exists')) {
|
|
294
|
+
return `Object already exists for ${operation}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return `Unreal Engine error during ${operation}: ${error.message || error}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create operation result with consistent structure
|
|
302
|
+
*/
|
|
303
|
+
static createResult<T extends BaseToolResponse>(
|
|
304
|
+
success: boolean,
|
|
305
|
+
message: string,
|
|
306
|
+
data?: Partial<T>
|
|
307
|
+
): T {
|
|
308
|
+
const result: BaseToolResponse = {
|
|
309
|
+
success,
|
|
310
|
+
message,
|
|
311
|
+
...(data || {})
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
if (!success && !result.error) {
|
|
315
|
+
result.error = message;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return result as T;
|
|
319
|
+
}
|
|
320
|
+
}
|