unreal-engine-mcp-server 0.4.0 → 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.
Files changed (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. 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
- if (error instanceof ToolError) {
116
- return error.type;
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.message?.toLowerCase() || String(error).toLowerCase();
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, AxiosRequestConfig, AxiosError } from 'axios';
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
- // Connection pooling configuration for better performance
6
+ // Enhanced connection pooling configuration to prevent socket failures
7
7
  const httpAgent = new http.Agent({
8
8
  keepAlive: true,
9
- keepAliveMsecs: 30000,
10
- maxSockets: 10,
11
- maxFreeSockets: 5,
12
- timeout: 30000
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: 30000,
18
- maxSockets: 10,
19
- maxFreeSockets: 5,
20
- timeout: 30000
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 retry logic
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
- * Execute request with retry logic for resilience
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.
@@ -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 toRotArray(input: any): Rot3Array | null {
52
- const obj = toRotObject(input);
53
- return obj ? [obj.pitch, obj.yaw, obj.roll] : null;
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
+