unreal-engine-mcp-server 0.5.2 → 0.5.4

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 (98) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +9 -6
  3. package/dist/automation/bridge.d.ts +1 -0
  4. package/dist/automation/bridge.js +62 -4
  5. package/dist/automation/types.d.ts +1 -0
  6. package/dist/config/class-aliases.d.ts +5 -0
  7. package/dist/config/class-aliases.js +30 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/graphql/server.d.ts +0 -1
  11. package/dist/graphql/server.js +15 -16
  12. package/dist/index.js +1 -1
  13. package/dist/services/metrics-server.d.ts +2 -1
  14. package/dist/services/metrics-server.js +29 -4
  15. package/dist/tools/consolidated-tool-definitions.js +3 -3
  16. package/dist/tools/debug.d.ts +5 -0
  17. package/dist/tools/debug.js +7 -0
  18. package/dist/tools/handlers/actor-handlers.js +4 -27
  19. package/dist/tools/handlers/asset-handlers.js +13 -1
  20. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
  21. package/dist/tools/handlers/common-handlers.d.ts +11 -11
  22. package/dist/tools/handlers/common-handlers.js +6 -4
  23. package/dist/tools/handlers/editor-handlers.d.ts +2 -1
  24. package/dist/tools/handlers/editor-handlers.js +6 -6
  25. package/dist/tools/handlers/effect-handlers.js +3 -0
  26. package/dist/tools/handlers/graph-handlers.d.ts +2 -1
  27. package/dist/tools/handlers/graph-handlers.js +1 -1
  28. package/dist/tools/handlers/input-handlers.d.ts +5 -1
  29. package/dist/tools/handlers/level-handlers.d.ts +2 -1
  30. package/dist/tools/handlers/level-handlers.js +3 -3
  31. package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
  32. package/dist/tools/handlers/lighting-handlers.js +3 -0
  33. package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
  34. package/dist/tools/handlers/pipeline-handlers.js +64 -10
  35. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  36. package/dist/tools/handlers/system-handlers.d.ts +1 -1
  37. package/dist/tools/input.d.ts +5 -1
  38. package/dist/tools/input.js +37 -1
  39. package/dist/tools/lighting.d.ts +1 -0
  40. package/dist/tools/lighting.js +7 -0
  41. package/dist/tools/physics.d.ts +1 -1
  42. package/dist/tools/sequence.d.ts +1 -0
  43. package/dist/tools/sequence.js +7 -0
  44. package/dist/types/handler-types.d.ts +343 -0
  45. package/dist/types/handler-types.js +2 -0
  46. package/dist/unreal-bridge.d.ts +1 -1
  47. package/dist/unreal-bridge.js +8 -6
  48. package/dist/utils/command-validator.d.ts +1 -0
  49. package/dist/utils/command-validator.js +11 -1
  50. package/dist/utils/error-handler.js +3 -1
  51. package/dist/utils/response-validator.js +2 -2
  52. package/dist/utils/safe-json.d.ts +1 -1
  53. package/dist/utils/safe-json.js +3 -6
  54. package/dist/utils/unreal-command-queue.js +1 -1
  55. package/dist/utils/validation.js +6 -2
  56. package/docs/handler-mapping.md +6 -1
  57. package/package.json +2 -2
  58. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
  59. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
  60. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
  61. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
  62. package/server.json +2 -2
  63. package/src/automation/bridge.ts +80 -10
  64. package/src/automation/types.ts +1 -0
  65. package/src/config/class-aliases.ts +65 -0
  66. package/src/constants.ts +10 -0
  67. package/src/graphql/server.ts +23 -23
  68. package/src/index.ts +1 -1
  69. package/src/services/metrics-server.ts +40 -6
  70. package/src/tools/consolidated-tool-definitions.ts +3 -3
  71. package/src/tools/debug.ts +8 -0
  72. package/src/tools/handlers/actor-handlers.ts +5 -31
  73. package/src/tools/handlers/asset-handlers.ts +19 -1
  74. package/src/tools/handlers/blueprint-handlers.ts +1 -1
  75. package/src/tools/handlers/common-handlers.ts +32 -11
  76. package/src/tools/handlers/editor-handlers.ts +8 -7
  77. package/src/tools/handlers/effect-handlers.ts +4 -0
  78. package/src/tools/handlers/graph-handlers.ts +7 -6
  79. package/src/tools/handlers/level-handlers.ts +5 -4
  80. package/src/tools/handlers/lighting-handlers.ts +5 -1
  81. package/src/tools/handlers/pipeline-handlers.ts +83 -16
  82. package/src/tools/input.ts +60 -1
  83. package/src/tools/lighting.ts +11 -0
  84. package/src/tools/physics.ts +1 -1
  85. package/src/tools/sequence.ts +11 -0
  86. package/src/types/handler-types.ts +442 -0
  87. package/src/unreal-bridge.ts +8 -6
  88. package/src/utils/command-validator.ts +23 -1
  89. package/src/utils/error-handler.ts +4 -1
  90. package/src/utils/response-validator.ts +7 -9
  91. package/src/utils/safe-json.ts +20 -15
  92. package/src/utils/unreal-command-queue.ts +3 -1
  93. package/src/utils/validation.test.ts +3 -3
  94. package/src/utils/validation.ts +36 -26
  95. package/tests/test-console-command.mjs +1 -1
  96. package/tests/test-runner.mjs +63 -3
  97. package/tests/run-unreal-tool-tests.mjs +0 -948
  98. package/tests/test-asset-errors.mjs +0 -35
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Shared type definitions for handler arguments and responses.
3
+ * Used across all *-handlers.ts files to replace 'any' types.
4
+ */
5
+
6
+ // ============================================================================
7
+ // Common Geometry Types
8
+ // ============================================================================
9
+
10
+ /** 3D Vector - used for locations, forces, scales */
11
+ export interface Vector3 {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ }
16
+
17
+ /** Rotation in Unreal format (Pitch, Yaw, Roll in degrees) */
18
+ export interface Rotator {
19
+ pitch: number;
20
+ yaw: number;
21
+ roll: number;
22
+ }
23
+
24
+ /** Transform combining location, rotation, and scale */
25
+ export interface Transform {
26
+ location?: Vector3;
27
+ rotation?: Rotator;
28
+ scale?: Vector3;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Base Handler Types
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Base interface for handler arguments.
37
+ * All handler args should extend this or use it directly for loose typing.
38
+ */
39
+ export interface HandlerArgs {
40
+ action?: string;
41
+ subAction?: string;
42
+ [key: string]: unknown;
43
+ }
44
+
45
+ /**
46
+ * Standard response from automation bridge requests.
47
+ */
48
+ export interface AutomationResponse {
49
+ success: boolean;
50
+ message?: string;
51
+ error?: string;
52
+ result?: unknown;
53
+ [key: string]: unknown;
54
+ }
55
+
56
+ /**
57
+ * Component information returned from getComponents.
58
+ */
59
+ export interface ComponentInfo {
60
+ name: string;
61
+ class?: string;
62
+ objectPath?: string;
63
+ [key: string]: unknown;
64
+ }
65
+
66
+ // ============================================================================
67
+ // Actor Types
68
+ // ============================================================================
69
+
70
+ export interface ActorArgs extends HandlerArgs {
71
+ actorName?: string;
72
+ name?: string;
73
+ classPath?: string;
74
+ class?: string;
75
+ type?: string;
76
+ location?: Vector3;
77
+ rotation?: Rotator;
78
+ scale?: Vector3;
79
+ meshPath?: string;
80
+ timeoutMs?: number;
81
+ force?: Vector3;
82
+ parentActor?: string;
83
+ childActor?: string;
84
+ tag?: string;
85
+ newName?: string;
86
+ offset?: Vector3;
87
+ visible?: boolean;
88
+ componentName?: string;
89
+ componentType?: string;
90
+ properties?: Record<string, unknown>;
91
+ }
92
+
93
+ // ============================================================================
94
+ // Asset Types
95
+ // ============================================================================
96
+
97
+ export interface AssetArgs extends HandlerArgs {
98
+ assetPath?: string;
99
+ path?: string;
100
+ directory?: string;
101
+ directoryPath?: string;
102
+ sourcePath?: string;
103
+ destinationPath?: string;
104
+ newName?: string;
105
+ name?: string;
106
+ filter?: string;
107
+ recursive?: boolean;
108
+ overwrite?: boolean;
109
+ classNames?: string[];
110
+ packagePaths?: string[];
111
+ parentMaterial?: string;
112
+ parameters?: Record<string, unknown>;
113
+ assetPaths?: string[];
114
+ meshPath?: string;
115
+ }
116
+
117
+ // ============================================================================
118
+ // Blueprint Types
119
+ // ============================================================================
120
+
121
+ export interface BlueprintArgs extends HandlerArgs {
122
+ blueprintPath?: string;
123
+ name?: string;
124
+ savePath?: string;
125
+ blueprintType?: string;
126
+ componentType?: string;
127
+ componentName?: string;
128
+ attachTo?: string;
129
+ variableName?: string;
130
+ eventType?: string;
131
+ customEventName?: string;
132
+ nodeType?: string;
133
+ graphName?: string;
134
+ x?: number;
135
+ y?: number;
136
+ memberName?: string;
137
+ nodeId?: string;
138
+ pinName?: string;
139
+ linkedTo?: string;
140
+ fromNodeId?: string;
141
+ fromPin?: string;
142
+ fromPinName?: string;
143
+ toNodeId?: string;
144
+ toPin?: string;
145
+ toPinName?: string;
146
+ propertyName?: string;
147
+ value?: unknown;
148
+ properties?: Record<string, unknown>;
149
+ compile?: boolean;
150
+ save?: boolean;
151
+ metadata?: Record<string, unknown>;
152
+ }
153
+
154
+ // ============================================================================
155
+ // Editor Types
156
+ // ============================================================================
157
+
158
+ export interface EditorArgs extends HandlerArgs {
159
+ command?: string;
160
+ filename?: string;
161
+ resolution?: string;
162
+ location?: Vector3;
163
+ rotation?: Rotator;
164
+ fov?: number;
165
+ speed?: number;
166
+ viewMode?: string;
167
+ width?: number;
168
+ height?: number;
169
+ enabled?: boolean;
170
+ realtime?: boolean;
171
+ bookmarkName?: string;
172
+ assetPath?: string;
173
+ path?: string;
174
+ category?: string;
175
+ preferences?: Record<string, unknown>;
176
+ timeoutMs?: number;
177
+ }
178
+
179
+ // ============================================================================
180
+ // Level Types
181
+ // ============================================================================
182
+
183
+ export interface LevelArgs extends HandlerArgs {
184
+ levelPath?: string;
185
+ levelName?: string;
186
+ levelPaths?: string[];
187
+ destinationPath?: string;
188
+ savePath?: string;
189
+ subLevelPath?: string;
190
+ parentLevel?: string;
191
+ parentPath?: string;
192
+ streamingMethod?: 'Blueprint' | 'AlwaysLoaded';
193
+ exportPath?: string;
194
+ packagePath?: string;
195
+ sourcePath?: string;
196
+ lightType?: 'Directional' | 'Point' | 'Spot' | 'Rect';
197
+ name?: string;
198
+ location?: Vector3;
199
+ rotation?: Rotator;
200
+ intensity?: number;
201
+ color?: number[];
202
+ quality?: string;
203
+ streaming?: boolean;
204
+ shouldBeLoaded?: boolean;
205
+ shouldBeVisible?: boolean;
206
+ dataLayerLabel?: string;
207
+ dataLayerName?: string;
208
+ dataLayerState?: string;
209
+ actorPath?: string;
210
+ min?: number[];
211
+ max?: number[];
212
+ origin?: number[];
213
+ extent?: number[];
214
+ metadata?: Record<string, unknown>;
215
+ timeoutMs?: number;
216
+ }
217
+
218
+ // ============================================================================
219
+ // Sequence Types
220
+ // ============================================================================
221
+
222
+ export interface SequenceArgs extends HandlerArgs {
223
+ path?: string;
224
+ name?: string;
225
+ actorName?: string;
226
+ actorNames?: string[];
227
+ spawnable?: boolean;
228
+ trackName?: string;
229
+ trackType?: string;
230
+ property?: string;
231
+ frame?: number;
232
+ value?: unknown;
233
+ speed?: number;
234
+ lengthInFrames?: number;
235
+ start?: number;
236
+ end?: number;
237
+ startFrame?: number;
238
+ endFrame?: number;
239
+ assetPath?: string;
240
+ muted?: boolean;
241
+ solo?: boolean;
242
+ locked?: boolean;
243
+ }
244
+
245
+ // ============================================================================
246
+ // Effect Types
247
+ // ============================================================================
248
+
249
+ export interface EffectArgs extends HandlerArgs {
250
+ location?: Vector3;
251
+ rotation?: Rotator;
252
+ scale?: number;
253
+ preset?: string;
254
+ systemPath?: string;
255
+ shape?: string;
256
+ size?: number;
257
+ color?: number[];
258
+ name?: string;
259
+ emitterName?: string;
260
+ modulePath?: string;
261
+ parameterName?: string;
262
+ parameterType?: string;
263
+ type?: string;
264
+ filter?: string;
265
+ }
266
+
267
+ // ============================================================================
268
+ // Environment Types
269
+ // ============================================================================
270
+
271
+ export interface EnvironmentArgs extends HandlerArgs {
272
+ name?: string;
273
+ landscapeName?: string;
274
+ location?: Vector3;
275
+ scale?: Vector3;
276
+ componentCount?: { x: number; y: number };
277
+ sectionSize?: number;
278
+ sectionsPerComponent?: number;
279
+ materialPath?: string;
280
+ foliageType?: string;
281
+ foliageTypePath?: string;
282
+ meshPath?: string;
283
+ density?: number;
284
+ radius?: number;
285
+ minScale?: number;
286
+ maxScale?: number;
287
+ alignToNormal?: boolean;
288
+ randomYaw?: boolean;
289
+ cullDistance?: number;
290
+ transforms?: Transform[];
291
+ locations?: Vector3[];
292
+ bounds?: { min: Vector3; max: Vector3 };
293
+ seed?: number;
294
+ heightData?: number[];
295
+ layerName?: string;
296
+ }
297
+
298
+ // ============================================================================
299
+ // Lighting Types
300
+ // ============================================================================
301
+
302
+ export interface LightingArgs extends HandlerArgs {
303
+ lightType?: 'Directional' | 'Point' | 'Spot' | 'Rect';
304
+ name?: string;
305
+ location?: Vector3;
306
+ rotation?: Rotator;
307
+ intensity?: number;
308
+ color?: number[];
309
+ temperature?: number;
310
+ radius?: number;
311
+ falloffExponent?: number;
312
+ innerCone?: number;
313
+ outerCone?: number;
314
+ castShadows?: boolean;
315
+ method?: 'Lightmass' | 'LumenGI' | 'ScreenSpace' | 'None';
316
+ bounces?: number;
317
+ quality?: string;
318
+ enabled?: boolean;
319
+ density?: number;
320
+ fogHeight?: number;
321
+ cubemapPath?: string;
322
+ sourceType?: 'CapturedScene' | 'SpecifiedCubemap';
323
+ recapture?: boolean;
324
+ }
325
+
326
+ // ============================================================================
327
+ // Performance Types
328
+ // ============================================================================
329
+
330
+ export interface PerformanceArgs extends HandlerArgs {
331
+ type?: 'CPU' | 'GPU' | 'Memory' | 'RenderThread' | 'GameThread' | 'All';
332
+ category?: string;
333
+ duration?: number;
334
+ outputPath?: string;
335
+ level?: number;
336
+ scale?: number;
337
+ enabled?: boolean;
338
+ maxFPS?: number;
339
+ verbose?: boolean;
340
+ detailed?: boolean;
341
+ }
342
+
343
+ // ============================================================================
344
+ // Inspect Types
345
+ // ============================================================================
346
+
347
+ export interface InspectArgs extends HandlerArgs {
348
+ objectPath?: string;
349
+ name?: string;
350
+ actorName?: string;
351
+ componentName?: string;
352
+ propertyName?: string;
353
+ propertyPath?: string;
354
+ value?: unknown;
355
+ className?: string;
356
+ classPath?: string;
357
+ filter?: string;
358
+ tag?: string;
359
+ snapshotName?: string;
360
+ destinationPath?: string;
361
+ outputPath?: string;
362
+ format?: string;
363
+ }
364
+
365
+ // ============================================================================
366
+ // Graph Types (Blueprint, Material, Niagara, BehaviorTree)
367
+ // ============================================================================
368
+
369
+ export interface GraphArgs extends HandlerArgs {
370
+ assetPath?: string;
371
+ blueprintPath?: string;
372
+ systemPath?: string;
373
+ graphName?: string;
374
+ nodeType?: string;
375
+ nodeId?: string;
376
+ x?: number;
377
+ y?: number;
378
+ memberName?: string;
379
+ variableName?: string;
380
+ eventName?: string;
381
+ functionName?: string;
382
+ targetClass?: string;
383
+ memberClass?: string;
384
+ componentClass?: string;
385
+ pinName?: string;
386
+ linkedTo?: string;
387
+ fromNodeId?: string;
388
+ fromPinName?: string;
389
+ toNodeId?: string;
390
+ toPinName?: string;
391
+ parentNodeId?: string;
392
+ childNodeId?: string;
393
+ properties?: Record<string, unknown>;
394
+ }
395
+
396
+ // ============================================================================
397
+ // System Types
398
+ // ============================================================================
399
+
400
+ export interface SystemArgs extends HandlerArgs {
401
+ command?: string;
402
+ category?: string;
403
+ profileType?: string;
404
+ level?: number;
405
+ key?: string;
406
+ value?: string;
407
+ section?: string;
408
+ configName?: string;
409
+ resolution?: string;
410
+ enabled?: boolean;
411
+ widgetPath?: string;
412
+ parentName?: string;
413
+ childClass?: string;
414
+ target?: string;
415
+ platform?: string;
416
+ configuration?: string;
417
+ arguments?: string;
418
+ }
419
+
420
+ // ============================================================================
421
+ // Input Types
422
+ // ============================================================================
423
+
424
+ export interface InputArgs extends HandlerArgs {
425
+ name?: string;
426
+ path?: string;
427
+ actionPath?: string;
428
+ contextPath?: string;
429
+ key?: string;
430
+ }
431
+
432
+ // ============================================================================
433
+ // Pipeline Types
434
+ // ============================================================================
435
+
436
+ export interface PipelineArgs extends HandlerArgs {
437
+ target?: string;
438
+ platform?: string;
439
+ configuration?: string;
440
+ arguments?: string;
441
+ projectPath?: string;
442
+ }
@@ -1,7 +1,7 @@
1
1
  import { Logger } from './utils/logger.js';
2
2
  import { ErrorHandler } from './utils/error-handler.js';
3
3
  import type { AutomationBridge } from './automation/index.js';
4
- import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT } from './constants.js';
4
+ import { DEFAULT_AUTOMATION_HOST, DEFAULT_AUTOMATION_PORT, CONSOLE_COMMAND_TIMEOUT_MS, ENGINE_QUERY_TIMEOUT_MS } from './constants.js';
5
5
  import { UnrealCommandQueue } from './utils/unreal-command-queue.js';
6
6
  import { CommandValidator } from './utils/command-validator.js';
7
7
 
@@ -153,7 +153,9 @@ export class UnrealBridge {
153
153
  if (this.connectPromise) {
154
154
  try {
155
155
  await this.connectPromise;
156
- } catch { }
156
+ } catch (err) {
157
+ this.log.debug('Existing connect promise rejected', err instanceof Error ? err.message : String(err));
158
+ }
157
159
  return this.connected;
158
160
  }
159
161
 
@@ -483,7 +485,7 @@ export class UnrealBridge {
483
485
  }
484
486
 
485
487
  // Execute a console command safely with validation and throttling
486
- async executeConsoleCommand(command: string, _options: Record<string, never> = {}): Promise<any> {
488
+ async executeConsoleCommand(command: string): Promise<any> {
487
489
  const automationAvailable = Boolean(
488
490
  this.automationBridge && typeof this.automationBridge.sendAutomationRequest === 'function'
489
491
  );
@@ -517,7 +519,7 @@ export class UnrealBridge {
517
519
  const pluginResp: ConsoleCommandResponse = await this.automationBridge.sendAutomationRequest(
518
520
  'console_command',
519
521
  { command: cmdTrimmed },
520
- { timeoutMs: 30000 }
522
+ { timeoutMs: CONSOLE_COMMAND_TIMEOUT_MS }
521
523
  );
522
524
 
523
525
  if (pluginResp && pluginResp.success) {
@@ -601,7 +603,7 @@ export class UnrealBridge {
601
603
  const resp = await bridge.sendAutomationRequest(
602
604
  'system_control',
603
605
  { action: 'get_engine_version' },
604
- { timeoutMs: 15000 }
606
+ { timeoutMs: ENGINE_QUERY_TIMEOUT_MS }
605
607
  );
606
608
  const raw: EngineVersionResult = resp && typeof resp.result === 'object'
607
609
  ? (resp.result as Record<string, unknown>)
@@ -644,7 +646,7 @@ export class UnrealBridge {
644
646
  const resp = await bridge.sendAutomationRequest(
645
647
  'system_control',
646
648
  { action: 'get_feature_flags' },
647
- { timeoutMs: 15000 }
649
+ { timeoutMs: ENGINE_QUERY_TIMEOUT_MS }
648
650
  );
649
651
  const raw = resp && typeof resp.result === 'object'
650
652
  ? (resp.result as Record<string, unknown>)
@@ -48,6 +48,14 @@ export class CommandValidator {
48
48
  /^this_is_not_a_valid/i,
49
49
  ];
50
50
 
51
+ /**
52
+ * Pre-compiled patterns for dangerous commands using word boundaries.
53
+ * This prevents false positives like 'show exit menu' matching 'exit'.
54
+ */
55
+ private static readonly DANGEROUS_PATTERNS = CommandValidator.DANGEROUS_COMMANDS.map(
56
+ cmd => new RegExp(`(?:^|\\s)${cmd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`, 'i')
57
+ );
58
+
51
59
  /**
52
60
  * Validates a console command for safety before execution.
53
61
  * @param command - The console command string to validate
@@ -73,7 +81,8 @@ export class CommandValidator {
73
81
  throw new Error('Python console commands are blocked from external calls for safety.');
74
82
  }
75
83
 
76
- if (this.DANGEROUS_COMMANDS.some(dangerous => cmdLower.includes(dangerous))) {
84
+ // Use word-boundary matching to avoid false positives like 'show exit menu'
85
+ if (this.DANGEROUS_PATTERNS.some(pattern => pattern.test(cmdLower))) {
77
86
  throw new Error(`Dangerous command blocked: ${command}`);
78
87
  }
79
88
 
@@ -81,9 +90,22 @@ export class CommandValidator {
81
90
  throw new Error('Command chaining with && or || is blocked for safety.');
82
91
  }
83
92
 
93
+ // Block semicolon and pipe which can also be used for command chaining/injection
94
+ if (cmdTrimmed.includes(';')) {
95
+ throw new Error('Command chaining with ; (semicolon) is blocked for safety.');
96
+ }
97
+ if (cmdTrimmed.includes('|')) {
98
+ throw new Error('Command piping with | is blocked for safety.');
99
+ }
100
+
84
101
  if (this.FORBIDDEN_TOKENS.some(token => cmdLower.includes(token))) {
85
102
  throw new Error(`Command contains unsafe token and was blocked: ${command}`);
86
103
  }
104
+
105
+ // Block backticks which can be used for shell execution
106
+ if (cmdTrimmed.includes('`')) {
107
+ throw new Error('Backtick characters are blocked for safety.');
108
+ }
87
109
  }
88
110
 
89
111
  /**
@@ -240,7 +240,10 @@ export class ErrorHandler {
240
240
  if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE'].includes(code)) return true;
241
241
  if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
242
242
  if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
243
- } catch { }
243
+ } catch (err) {
244
+ // Error checking retriability is uncommon; log at debug level
245
+ log.debug('isRetriable check failed', err instanceof Error ? err.message : String(err));
246
+ }
244
247
  return false;
245
248
  }
246
249
 
@@ -1,4 +1,4 @@
1
- import Ajv from 'ajv';
1
+ import Ajv, { ValidateFunction } from 'ajv';
2
2
  import { Logger } from './logger.js';
3
3
  import { cleanObject } from './safe-json.js';
4
4
  import { wasmIntegration } from '../wasm/index.js';
@@ -161,16 +161,14 @@ function buildSummaryText(toolName: string, payload: unknown): string {
161
161
  * Validates tool responses against their defined output schemas
162
162
  */
163
163
  export class ResponseValidator {
164
- // Keep ajv as any to avoid complex interop typing issues with Ajv's ESM/CJS dual export
165
- // shape when using NodeNext module resolution.
166
- private ajv: any;
167
- private validators: Map<string, any> = new Map();
164
+ // Ajv instance - using Ajv.default for ESM/CJS interop
165
+ private ajv: Ajv.default;
166
+ private validators: Map<string, ValidateFunction> = new Map();
168
167
 
169
168
  constructor() {
170
- // Cast Ajv to any for construction to avoid errors when TypeScript's NodeNext
171
- // module resolution represents the import as a namespace object.
172
- const AjvCtor: any = (Ajv as any)?.default ?? Ajv;
173
- this.ajv = new AjvCtor({
169
+ // Ajv exports differ between ESM and CJS - handle both patterns
170
+ const AjvClass = (Ajv as unknown as { default: typeof Ajv.default }).default ?? Ajv.default;
171
+ this.ajv = new AjvClass({
174
172
  allErrors: true,
175
173
  verbose: true,
176
174
  strict: true // Enforce strict schema validation
@@ -1,11 +1,18 @@
1
1
  import { Logger } from './logger.js';
2
2
 
3
- // Remove circular references and non-serializable properties from an object
4
- export function cleanObject(obj: any, maxDepth: number = 10): any {
5
- const seen = new WeakSet();
6
- const logger = new Logger('safe-json');
3
+ // Module-level logger to avoid creating new instances on every call
4
+ const log = new Logger('safe-json');
7
5
 
8
- function clean(value: any, depth: number, path: string = 'root'): any {
6
+ /**
7
+ * Remove circular references and non-serializable properties from an object.
8
+ * @param obj - The object to clean
9
+ * @param maxDepth - Maximum recursion depth (default: 10)
10
+ * @returns Cleaned object safe for JSON serialization
11
+ */
12
+ export function cleanObject<T = unknown>(obj: T, maxDepth: number = 10): T {
13
+ const seen = new WeakSet<object>();
14
+
15
+ function clean(value: unknown, depth: number, path: string = 'root'): unknown {
9
16
  // Prevent infinite recursion
10
17
  if (depth > maxDepth) {
11
18
  return '[Max depth reached]';
@@ -26,7 +33,8 @@ export function cleanObject(obj: any, maxDepth: number = 10): any {
26
33
  return value;
27
34
  }
28
35
 
29
- // Check for circular reference
36
+ // Check for circular reference - keep in set permanently for this call
37
+ // This prevents the same object from appearing in multiple branches
30
38
  if (seen.has(value)) {
31
39
  return '[Circular Reference]';
32
40
  }
@@ -35,31 +43,28 @@ export function cleanObject(obj: any, maxDepth: number = 10): any {
35
43
 
36
44
  // Handle arrays
37
45
  if (Array.isArray(value)) {
38
- const result = value.map((item, index) => clean(item, depth + 1, `${path}[${index}]`));
39
- seen.delete(value); // Remove from seen after processing
40
- return result;
46
+ return value.map((item, index) => clean(item, depth + 1, `${path}[${index}]`));
41
47
  }
42
48
 
43
49
  // Handle objects
44
- const cleaned: any = {};
50
+ const cleaned: Record<string, unknown> = {};
45
51
 
46
52
  // Use Object.keys to avoid prototype properties
47
- const keys = Object.keys(value);
53
+ const keys = Object.keys(value as object);
48
54
  for (const key of keys) {
49
55
  try {
50
- const cleanedValue = clean(value[key], depth + 1, `${path}.${key}`);
56
+ const cleanedValue = clean((value as Record<string, unknown>)[key], depth + 1, `${path}.${key}`);
51
57
  if (cleanedValue !== undefined) {
52
58
  cleaned[key] = cleanedValue;
53
59
  }
54
60
  } catch (e) {
55
61
  // Skip properties that throw errors when accessed
56
- logger.error(`Error cleaning property ${path}.${key}`, e);
62
+ log.error(`Error cleaning property ${path}.${key}`, e);
57
63
  }
58
64
  }
59
65
 
60
- seen.delete(value); // Remove from seen after processing
61
66
  return cleaned;
62
67
  }
63
68
 
64
- return clean(obj, 0);
69
+ return clean(obj, 0) as T;
65
70
  }
@@ -140,11 +140,13 @@ export class UnrealCommandQueue {
140
140
  }
141
141
 
142
142
  private startProcessor(): void {
143
+ // Fallback processor - primary processing is triggered directly from execute()
144
+ // Reduced from 1000ms to 250ms for faster recovery if processor stalls
143
145
  this.processorInterval = setInterval(() => {
144
146
  if (!this.isProcessing && this.queue.length > 0) {
145
147
  this.processQueue();
146
148
  }
147
- }, 1000);
149
+ }, 250);
148
150
  }
149
151
 
150
152
  /**
@@ -59,9 +59,9 @@ describe('sanitizePath', () => {
59
59
  });
60
60
 
61
61
  it('sanitizes path segments with dots', () => {
62
- const result = sanitizePath('/Game/../MyAsset');
63
- // Each segment is sanitized, .. becomes a segment that gets sanitized
64
- expect(result).toContain('/');
62
+ expect(() => sanitizePath('/Game/../MyAsset')).toThrow(
63
+ 'Path traversal (..) is not allowed'
64
+ );
65
65
  });
66
66
  });
67
67