unreal-engine-mcp-server 0.3.1 → 0.4.3
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.production +1 -1
- package/.github/copilot-instructions.md +45 -0
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +22 -7
- package/dist/index.js +137 -46
- package/dist/prompts/index.d.ts +10 -3
- package/dist/prompts/index.js +186 -7
- package/dist/resources/actors.d.ts +19 -1
- package/dist/resources/actors.js +55 -64
- package/dist/resources/assets.d.ts +3 -2
- package/dist/resources/assets.js +117 -109
- package/dist/resources/levels.d.ts +21 -3
- package/dist/resources/levels.js +31 -56
- package/dist/tools/actors.d.ts +3 -14
- package/dist/tools/actors.js +246 -302
- package/dist/tools/animation.d.ts +57 -102
- package/dist/tools/animation.js +429 -450
- package/dist/tools/assets.d.ts +13 -2
- package/dist/tools/assets.js +58 -46
- package/dist/tools/audio.d.ts +22 -13
- package/dist/tools/audio.js +467 -121
- package/dist/tools/blueprint.d.ts +32 -13
- package/dist/tools/blueprint.js +699 -448
- package/dist/tools/build_environment_advanced.d.ts +0 -1
- package/dist/tools/build_environment_advanced.js +236 -87
- package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
- package/dist/tools/consolidated-tool-definitions.js +124 -255
- package/dist/tools/consolidated-tool-handlers.js +749 -766
- package/dist/tools/debug.d.ts +72 -10
- package/dist/tools/debug.js +170 -36
- package/dist/tools/editor.d.ts +9 -2
- package/dist/tools/editor.js +30 -44
- package/dist/tools/foliage.d.ts +34 -15
- package/dist/tools/foliage.js +97 -107
- package/dist/tools/introspection.js +19 -21
- package/dist/tools/landscape.d.ts +1 -2
- package/dist/tools/landscape.js +311 -168
- package/dist/tools/level.d.ts +3 -28
- package/dist/tools/level.js +642 -192
- package/dist/tools/lighting.d.ts +14 -3
- package/dist/tools/lighting.js +236 -123
- package/dist/tools/materials.d.ts +25 -7
- package/dist/tools/materials.js +102 -79
- package/dist/tools/niagara.d.ts +10 -12
- package/dist/tools/niagara.js +74 -94
- package/dist/tools/performance.d.ts +12 -4
- package/dist/tools/performance.js +38 -79
- package/dist/tools/physics.d.ts +34 -10
- package/dist/tools/physics.js +364 -292
- package/dist/tools/rc.js +98 -24
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +146 -24
- package/dist/tools/ui.d.ts +31 -4
- package/dist/tools/ui.js +83 -66
- package/dist/tools/visual.d.ts +11 -0
- package/dist/tools/visual.js +245 -30
- package/dist/types/tool-types.d.ts +0 -6
- package/dist/types/tool-types.js +1 -8
- package/dist/unreal-bridge.d.ts +32 -2
- package/dist/unreal-bridge.js +621 -127
- package/dist/utils/elicitation.d.ts +57 -0
- package/dist/utils/elicitation.js +104 -0
- package/dist/utils/error-handler.d.ts +0 -33
- package/dist/utils/error-handler.js +4 -111
- package/dist/utils/http.d.ts +2 -22
- package/dist/utils/http.js +12 -75
- package/dist/utils/normalize.d.ts +4 -4
- package/dist/utils/normalize.js +15 -7
- package/dist/utils/python-output.d.ts +18 -0
- package/dist/utils/python-output.js +290 -0
- package/dist/utils/python.d.ts +2 -0
- package/dist/utils/python.js +4 -0
- package/dist/utils/response-validator.d.ts +6 -1
- package/dist/utils/response-validator.js +66 -13
- package/dist/utils/result-helpers.d.ts +27 -0
- package/dist/utils/result-helpers.js +147 -0
- package/dist/utils/safe-json.d.ts +0 -2
- package/dist/utils/safe-json.js +0 -43
- package/dist/utils/validation.d.ts +16 -0
- package/dist/utils/validation.js +70 -7
- package/mcp-config-example.json +2 -2
- package/package.json +11 -10
- package/server.json +37 -14
- package/src/index.ts +146 -50
- package/src/prompts/index.ts +211 -13
- package/src/resources/actors.ts +59 -44
- package/src/resources/assets.ts +123 -102
- package/src/resources/levels.ts +37 -47
- package/src/tools/actors.ts +269 -313
- package/src/tools/animation.ts +556 -539
- package/src/tools/assets.ts +59 -45
- package/src/tools/audio.ts +507 -113
- package/src/tools/blueprint.ts +778 -462
- package/src/tools/build_environment_advanced.ts +312 -106
- package/src/tools/consolidated-tool-definitions.ts +136 -267
- package/src/tools/consolidated-tool-handlers.ts +871 -795
- package/src/tools/debug.ts +179 -38
- package/src/tools/editor.ts +35 -37
- package/src/tools/foliage.ts +110 -104
- package/src/tools/introspection.ts +24 -22
- package/src/tools/landscape.ts +334 -181
- package/src/tools/level.ts +683 -182
- package/src/tools/lighting.ts +244 -123
- package/src/tools/materials.ts +114 -83
- package/src/tools/niagara.ts +87 -81
- package/src/tools/performance.ts +49 -88
- package/src/tools/physics.ts +393 -299
- package/src/tools/rc.ts +103 -25
- package/src/tools/sequence.ts +157 -30
- package/src/tools/ui.ts +101 -70
- package/src/tools/visual.ts +250 -29
- package/src/types/tool-types.ts +0 -9
- package/src/unreal-bridge.ts +658 -140
- package/src/utils/elicitation.ts +129 -0
- package/src/utils/error-handler.ts +4 -159
- package/src/utils/http.ts +16 -115
- package/src/utils/normalize.ts +20 -10
- package/src/utils/python-output.ts +351 -0
- package/src/utils/python.ts +3 -0
- package/src/utils/response-validator.ts +68 -17
- package/src/utils/result-helpers.ts +193 -0
- package/src/utils/safe-json.ts +0 -50
- package/src/utils/validation.ts +94 -7
- package/tests/run-unreal-tool-tests.mjs +720 -0
- package/tsconfig.json +2 -2
- package/dist/python-utils.d.ts +0 -29
- package/dist/python-utils.js +0 -54
- package/dist/tools/tool-definitions.d.ts +0 -4919
- package/dist/tools/tool-definitions.js +0 -1065
- package/dist/tools/tool-handlers.d.ts +0 -47
- package/dist/tools/tool-handlers.js +0 -863
- package/dist/types/index.d.ts +0 -323
- package/dist/types/index.js +0 -28
- package/dist/utils/cache-manager.d.ts +0 -64
- package/dist/utils/cache-manager.js +0 -176
- package/dist/utils/errors.d.ts +0 -133
- package/dist/utils/errors.js +0 -256
- package/src/python/editor_compat.py +0 -181
- package/src/python-utils.ts +0 -57
- package/src/tools/tool-definitions.ts +0 -1081
- package/src/tools/tool-handlers.ts +0 -973
- package/src/types/index.ts +0 -414
- package/src/utils/cache-manager.ts +0 -213
- package/src/utils/errors.ts +0 -312
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { Logger } from './logger.js';
|
|
3
|
+
|
|
4
|
+
// Minimal helper to opportunistically use MCP Elicitation when available.
|
|
5
|
+
// Safe across clients: validates schema shape, handles timeouts and -32601 fallbacks.
|
|
6
|
+
export type PrimitiveSchema =
|
|
7
|
+
| { type: 'string'; title?: string; description?: string; minLength?: number; maxLength?: number; pattern?: string; format?: 'email'|'uri'|'date'|'date-time'; default?: string }
|
|
8
|
+
| { type: 'number'|'integer'; title?: string; description?: string; minimum?: number; maximum?: number; default?: number }
|
|
9
|
+
| { type: 'boolean'; title?: string; description?: string; default?: boolean }
|
|
10
|
+
| { type: 'string'; enum: string[]; enumNames?: string[]; title?: string; description?: string; default?: string };
|
|
11
|
+
|
|
12
|
+
export interface ElicitSchema {
|
|
13
|
+
type: 'object';
|
|
14
|
+
properties: Record<string, PrimitiveSchema>;
|
|
15
|
+
required?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ElicitOptions {
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
fallback?: () => Promise<{ ok: boolean; value?: any; error?: string }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createElicitationHelper(server: Server, log: Logger) {
|
|
24
|
+
// We do not require explicit capability detection: we optimistically try once
|
|
25
|
+
// and disable on a Method-not-found (-32601) error for the session.
|
|
26
|
+
let supported = true; // optimistic; will be set false on first failure
|
|
27
|
+
|
|
28
|
+
const MIN_TIMEOUT_MS = 30_000;
|
|
29
|
+
const MAX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
30
|
+
const DEFAULT_TIMEOUT_MS = 3 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
const timeoutEnvRaw = process.env.MCP_ELICITATION_TIMEOUT_MS ?? process.env.ELICITATION_TIMEOUT_MS ?? '';
|
|
33
|
+
const parsedEnvTimeout = Number.parseInt(timeoutEnvRaw, 10);
|
|
34
|
+
const defaultTimeoutMs = Number.isFinite(parsedEnvTimeout) && parsedEnvTimeout > 0
|
|
35
|
+
? Math.min(Math.max(parsedEnvTimeout, MIN_TIMEOUT_MS), MAX_TIMEOUT_MS)
|
|
36
|
+
: DEFAULT_TIMEOUT_MS;
|
|
37
|
+
|
|
38
|
+
if (timeoutEnvRaw) {
|
|
39
|
+
log.debug('Configured elicitation timeout override detected', {
|
|
40
|
+
defaultTimeoutMs,
|
|
41
|
+
fromEnv: timeoutEnvRaw
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isSafeSchema(schema: ElicitSchema): boolean {
|
|
46
|
+
if (!schema || schema.type !== 'object' || typeof schema.properties !== 'object') return false;
|
|
47
|
+
|
|
48
|
+
const propertyEntries = Object.entries(schema.properties ?? {});
|
|
49
|
+
const propertyKeys = propertyEntries.map(([key]) => key);
|
|
50
|
+
|
|
51
|
+
if (schema.required) {
|
|
52
|
+
if (!Array.isArray(schema.required)) return false;
|
|
53
|
+
const invalidRequired = schema.required.some((key) => typeof key !== 'string' || !propertyKeys.includes(key));
|
|
54
|
+
if (invalidRequired) return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return propertyEntries.every(([, rawSchema]) => {
|
|
58
|
+
if (!rawSchema || typeof rawSchema !== 'object') return false;
|
|
59
|
+
const primitive = rawSchema as PrimitiveSchema & { properties?: unknown; items?: unknown }; // narrow for guards
|
|
60
|
+
|
|
61
|
+
if ('properties' in primitive || 'items' in primitive) return false; // nested schemas unsupported
|
|
62
|
+
|
|
63
|
+
if (Array.isArray((primitive as any).enum)) {
|
|
64
|
+
const enumValues = (primitive as any).enum;
|
|
65
|
+
const allStrings = enumValues.every((value: unknown) => typeof value === 'string');
|
|
66
|
+
if (!allStrings) return false;
|
|
67
|
+
return !('type' in primitive) || (primitive as any).type === 'string';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ((primitive as any).type === 'string') return true;
|
|
71
|
+
if ((primitive as any).type === 'number' || (primitive as any).type === 'integer') return true;
|
|
72
|
+
if ((primitive as any).type === 'boolean') return true;
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function elicit(message: string, requestedSchema: ElicitSchema, opts: ElicitOptions = {}) {
|
|
79
|
+
if (!supported || !isSafeSchema(requestedSchema)) {
|
|
80
|
+
if (opts.fallback) return opts.fallback();
|
|
81
|
+
return { ok: false, error: 'elicitation-unsupported' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const params = { message, requestedSchema } as any;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const elicitMethod = (server as any)?.elicitInput;
|
|
88
|
+
if (typeof elicitMethod !== 'function') {
|
|
89
|
+
supported = false;
|
|
90
|
+
throw new Error('elicitInput-not-available');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const requestedTimeout = opts.timeoutMs;
|
|
94
|
+
const timeoutMs = Math.max(MIN_TIMEOUT_MS, requestedTimeout ?? defaultTimeoutMs);
|
|
95
|
+
const res: any = await elicitMethod.call(server, params, { timeout: timeoutMs });
|
|
96
|
+
const action = res?.action;
|
|
97
|
+
const content = res?.content;
|
|
98
|
+
|
|
99
|
+
if (action === 'accept') return { ok: true, value: content };
|
|
100
|
+
if (action === 'decline' || action === 'cancel') {
|
|
101
|
+
if (opts.fallback) return opts.fallback();
|
|
102
|
+
return { ok: false, error: action };
|
|
103
|
+
}
|
|
104
|
+
if (opts.fallback) return opts.fallback();
|
|
105
|
+
return { ok: false, error: 'unexpected-response' };
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
const msg = String(e?.message || e);
|
|
108
|
+
const code = (e as any)?.code ?? (e as any)?.error?.code;
|
|
109
|
+
// If client doesn't support it, don’t try again this session
|
|
110
|
+
if (
|
|
111
|
+
msg.includes('Method not found') ||
|
|
112
|
+
msg.includes('elicitInput-not-available') ||
|
|
113
|
+
msg.includes('request-not-available') ||
|
|
114
|
+
String(code) === '-32601'
|
|
115
|
+
) {
|
|
116
|
+
supported = false;
|
|
117
|
+
}
|
|
118
|
+
log.debug('Elicitation failed; falling back', { error: msg, code });
|
|
119
|
+
if (opts.fallback) return opts.fallback();
|
|
120
|
+
return { ok: false, error: msg.includes('timeout') ? 'timeout' : 'rpc-failed' };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
supports: () => supported,
|
|
126
|
+
elicit,
|
|
127
|
+
getDefaultTimeoutMs: () => defaultTimeoutMs
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -16,21 +16,6 @@ export enum ErrorType {
|
|
|
16
16
|
UNKNOWN = 'UNKNOWN'
|
|
17
17
|
}
|
|
18
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
19
|
/**
|
|
35
20
|
* Consistent error handling for all tools
|
|
36
21
|
*/
|
|
@@ -76,47 +61,16 @@ export class ErrorHandler {
|
|
|
76
61
|
} as any;
|
|
77
62
|
}
|
|
78
63
|
|
|
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
64
|
/**
|
|
112
65
|
* Categorize error by type
|
|
113
66
|
*/
|
|
114
67
|
private static categorizeError(error: any): ErrorType {
|
|
115
|
-
|
|
116
|
-
|
|
68
|
+
const explicitType = (error?.type || error?.errorType || '').toString().toUpperCase();
|
|
69
|
+
if (explicitType && Object.values(ErrorType).includes(explicitType as ErrorType)) {
|
|
70
|
+
return explicitType as ErrorType;
|
|
117
71
|
}
|
|
118
72
|
|
|
119
|
-
const errorMessage = error
|
|
73
|
+
const errorMessage = error?.message?.toLowerCase() || String(error).toLowerCase();
|
|
120
74
|
|
|
121
75
|
// Connection errors
|
|
122
76
|
if (
|
|
@@ -208,113 +162,4 @@ export class ErrorHandler {
|
|
|
208
162
|
} catch {}
|
|
209
163
|
return false;
|
|
210
164
|
}
|
|
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
165
|
}
|
package/src/utils/http.ts
CHANGED
|
@@ -1,65 +1,34 @@
|
|
|
1
|
-
import axios, { AxiosInstance
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Enhanced connection pooling configuration to prevent socket failures
|
|
7
7
|
const httpAgent = new http.Agent({
|
|
8
8
|
keepAlive: true,
|
|
9
|
-
keepAliveMsecs:
|
|
10
|
-
maxSockets:
|
|
11
|
-
maxFreeSockets:
|
|
12
|
-
timeout:
|
|
9
|
+
keepAliveMsecs: 60000, // Increased keep-alive time
|
|
10
|
+
maxSockets: 20, // Increased socket pool
|
|
11
|
+
maxFreeSockets: 10, // More free sockets
|
|
12
|
+
timeout: 60000, // Longer timeout
|
|
13
13
|
});
|
|
14
14
|
|
|
15
15
|
const httpsAgent = new https.Agent({
|
|
16
16
|
keepAlive: true,
|
|
17
|
-
keepAliveMsecs:
|
|
18
|
-
maxSockets:
|
|
19
|
-
maxFreeSockets:
|
|
20
|
-
timeout:
|
|
17
|
+
keepAliveMsecs: 60000, // Increased keep-alive time
|
|
18
|
+
maxSockets: 20, // Increased socket pool
|
|
19
|
+
maxFreeSockets: 10, // More free sockets
|
|
20
|
+
timeout: 60000, // Longer timeout
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
// Retry configuration interface
|
|
24
|
-
interface RetryConfig {
|
|
25
|
-
maxRetries: number;
|
|
26
|
-
initialDelay: number;
|
|
27
|
-
maxDelay: number;
|
|
28
|
-
backoffMultiplier: number;
|
|
29
|
-
retryableStatuses: number[];
|
|
30
|
-
retryableErrors: string[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
23
|
const log = new Logger('HTTP');
|
|
34
24
|
|
|
35
|
-
const defaultRetryConfig: RetryConfig = {
|
|
36
|
-
maxRetries: 3,
|
|
37
|
-
initialDelay: 1000,
|
|
38
|
-
maxDelay: 10000,
|
|
39
|
-
backoffMultiplier: 2,
|
|
40
|
-
retryableStatuses: [408, 429, 500, 502, 503, 504],
|
|
41
|
-
retryableErrors: ['ECONNABORTED', 'ETIMEDOUT', 'ECONNRESET', 'ENOTFOUND']
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Calculate exponential backoff delay with jitter
|
|
46
|
-
*/
|
|
47
|
-
function calculateBackoff(attempt: number, config: RetryConfig): number {
|
|
48
|
-
const delay = Math.min(
|
|
49
|
-
config.initialDelay * Math.pow(config.backoffMultiplier, attempt - 1),
|
|
50
|
-
config.maxDelay
|
|
51
|
-
);
|
|
52
|
-
// Add jitter to prevent thundering herd
|
|
53
|
-
return delay + Math.random() * 1000;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
25
|
/**
|
|
57
|
-
* Enhanced HTTP client factory with connection pooling and
|
|
26
|
+
* Enhanced HTTP client factory with connection pooling and request timing
|
|
58
27
|
*/
|
|
59
28
|
export function createHttpClient(baseURL: string): AxiosInstance {
|
|
60
29
|
const client = axios.create({
|
|
61
30
|
baseURL,
|
|
62
|
-
headers: {
|
|
31
|
+
headers: {
|
|
63
32
|
'Content-Type': 'application/json',
|
|
64
33
|
'Accept': 'application/json'
|
|
65
34
|
},
|
|
@@ -71,7 +40,7 @@ export function createHttpClient(baseURL: string): AxiosInstance {
|
|
|
71
40
|
// Remove Content-Length if it's set incorrectly
|
|
72
41
|
delete headers['Content-Length'];
|
|
73
42
|
delete headers['content-length'];
|
|
74
|
-
|
|
43
|
+
|
|
75
44
|
// Properly stringify JSON data
|
|
76
45
|
if (data && typeof data === 'object') {
|
|
77
46
|
const jsonStr = JSON.stringify(data);
|
|
@@ -102,7 +71,7 @@ export function createHttpClient(baseURL: string): AxiosInstance {
|
|
|
102
71
|
client.interceptors.response.use(
|
|
103
72
|
(response) => {
|
|
104
73
|
const duration = Date.now() - ((response.config as any).metadata?.startTime || 0);
|
|
105
|
-
if (duration > 5000) {
|
|
74
|
+
if (duration > 5000) {
|
|
106
75
|
log.warn(`[HTTP] Slow request: ${response.config.url} took ${duration}ms`);
|
|
107
76
|
}
|
|
108
77
|
return response;
|
|
@@ -115,73 +84,5 @@ if (duration > 5000) {
|
|
|
115
84
|
return client;
|
|
116
85
|
}
|
|
117
86
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
export async function requestWithRetry<T = any>(
|
|
122
|
-
client: AxiosInstance,
|
|
123
|
-
config: AxiosRequestConfig,
|
|
124
|
-
retryConfig: Partial<RetryConfig> = {}
|
|
125
|
-
): Promise<T> {
|
|
126
|
-
const retry = { ...defaultRetryConfig, ...retryConfig };
|
|
127
|
-
let lastError: Error | null = null;
|
|
128
|
-
|
|
129
|
-
for (let attempt = 1; attempt <= retry.maxRetries; attempt++) {
|
|
130
|
-
try {
|
|
131
|
-
const response = await client.request<T>(config);
|
|
132
|
-
|
|
133
|
-
// Check if we should retry based on status
|
|
134
|
-
if (retry.retryableStatuses.includes(response.status)) {
|
|
135
|
-
throw new Error(`Retryable status: ${response.status}`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return response.data;
|
|
139
|
-
} catch (error) {
|
|
140
|
-
lastError = error as Error;
|
|
141
|
-
const axiosError = error as AxiosError;
|
|
142
|
-
|
|
143
|
-
// Check if error is retryable
|
|
144
|
-
const isRetryable =
|
|
145
|
-
retry.retryableErrors.includes(axiosError.code || '') ||
|
|
146
|
-
(axiosError.response && retry.retryableStatuses.includes(axiosError.response.status));
|
|
147
|
-
|
|
148
|
-
if (!isRetryable || attempt === retry.maxRetries) {
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Calculate delay and wait
|
|
153
|
-
const delay = calculateBackoff(attempt, retry);
|
|
154
|
-
log.debug(`[HTTP] Retry attempt ${attempt}/${retry.maxRetries} after ${Math.round(delay)}ms`);
|
|
155
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
throw lastError || new Error('Request failed after retries');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Batch multiple requests for efficiency
|
|
164
|
-
*/
|
|
165
|
-
export async function batchRequests<T = any>(
|
|
166
|
-
client: AxiosInstance,
|
|
167
|
-
requests: AxiosRequestConfig[],
|
|
168
|
-
options: { concurrency?: number; throwOnError?: boolean } = {}
|
|
169
|
-
): Promise<(T | Error)[]> {
|
|
170
|
-
const { concurrency = 5, throwOnError = false } = options;
|
|
171
|
-
const results: (T | Error)[] = [];
|
|
172
|
-
|
|
173
|
-
// Process requests in batches
|
|
174
|
-
for (let i = 0; i < requests.length; i += concurrency) {
|
|
175
|
-
const batch = requests.slice(i, i + concurrency);
|
|
176
|
-
const batchPromises = batch.map(req =>
|
|
177
|
-
client.request<T>(req)
|
|
178
|
-
.then(res => res.data)
|
|
179
|
-
.catch(err => throwOnError ? Promise.reject(err) : err)
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
const batchResults = await Promise.all(batchPromises);
|
|
183
|
-
results.push(...batchResults);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return results;
|
|
187
|
-
}
|
|
87
|
+
// No retry helpers are exported; consolidated command flows rely on
|
|
88
|
+
// Unreal's own retry/backoff semantics to avoid duplicate side effects.
|
package/src/utils/normalize.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export type Vec3Array = [number, number, number];
|
|
2
|
-
export type Rot3Array = [number, number, number];
|
|
3
1
|
export interface Vec3Obj { x: number; y: number; z: number; }
|
|
4
2
|
export interface Rot3Obj { pitch: number; yaw: number; roll: number; }
|
|
5
3
|
|
|
4
|
+
export type Vec3Tuple = [number, number, number];
|
|
5
|
+
export type Rot3Tuple = [number, number, number];
|
|
6
|
+
|
|
6
7
|
export function toVec3Object(input: any): Vec3Obj | null {
|
|
7
8
|
try {
|
|
8
9
|
if (Array.isArray(input) && input.length === 3) {
|
|
@@ -23,11 +24,6 @@ export function toVec3Object(input: any): Vec3Obj | null {
|
|
|
23
24
|
return null;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function toVec3Array(input: any): Vec3Array | null {
|
|
27
|
-
const obj = toVec3Object(input);
|
|
28
|
-
return obj ? [obj.x, obj.y, obj.z] : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
27
|
export function toRotObject(input: any): Rot3Obj | null {
|
|
32
28
|
try {
|
|
33
29
|
if (Array.isArray(input) && input.length === 3) {
|
|
@@ -48,7 +44,21 @@ export function toRotObject(input: any): Rot3Obj | null {
|
|
|
48
44
|
return null;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
export function
|
|
52
|
-
const
|
|
53
|
-
|
|
47
|
+
export function toVec3Tuple(input: any): Vec3Tuple | null {
|
|
48
|
+
const vec = toVec3Object(input);
|
|
49
|
+
if (!vec) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const { x, y, z } = vec;
|
|
53
|
+
return [x, y, z];
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
export function toRotTuple(input: any): Rot3Tuple | null {
|
|
57
|
+
const rot = toRotObject(input);
|
|
58
|
+
if (!rot) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const { pitch, yaw, roll } = rot;
|
|
62
|
+
return [pitch, yaw, roll];
|
|
63
|
+
}
|
|
64
|
+
|