unreal-engine-mcp-server 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -7,7 +7,6 @@
7
7
  #include "Serialization/JsonReader.h"
8
8
  #include "Serialization/JsonSerializer.h"
9
9
 
10
-
11
10
  // Reuse the log category from the subsystem for consistency
12
11
  // (It is declared extern in McpAutomationBridgeSubsystem.h)
13
12
 
@@ -2,27 +2,32 @@
2
2
  import { spawn } from 'node:child_process';
3
3
 
4
4
  const tests = [
5
- 'test:control_actor', // Pass
6
- 'test:control_editor', // Pass
7
- 'test:manage_level', // Pass
8
- 'test:animation', // Pass
9
- 'test:materials', // Pass
10
- 'test:niagara', // Pass
11
- 'test:landscape', // Pass
12
- 'test:sequence', // Pass
13
- 'test:system', // Pass
14
- 'test:console_command', // Pass
15
- 'test:inspect', // Pass
16
- 'test:manage_asset', // Pass
17
- 'test:blueprint', // Pass
18
- 'test:blueprint_graph', // Pass
19
- // 'test:graphql', // Pass
20
- 'test:wasm:all', // Pass
5
+ 'test:control_actor', // 1
6
+ 'test:control_editor', // 1
7
+ 'test:manage_level', // crashed 1
8
+ 'test:animation',
9
+ 'test:materials',
10
+ 'test:niagara',
11
+ 'test:landscape',
12
+ 'test:sequence', // crashed 1
13
+ 'test:system',
14
+ 'test:console_command',
15
+ 'test:inspect',
16
+ 'test:manage_asset',
17
+ 'test:blueprint',
18
+ 'test:blueprint_graph',
19
+ 'test:audio',
20
+ 'test:behavior_tree',
21
+ 'test:lighting',
22
+ 'test:performance',
23
+ 'test:input',
24
+ 'test:asset_graph',
25
+ 'test:graphql',
26
+ 'test:wasm:all',
21
27
  'test:no-inline-python',
22
- 'test:plugin-handshake', // Pass
23
- 'test:asset_advanced', // Pass
24
- 'test:render', // Pass
25
- 'test:world_partition' // Pass
28
+ 'test:plugin-handshake',
29
+ 'test:asset_advanced',
30
+ 'test:world_partition'
26
31
  ];
27
32
 
28
33
  const isWindows = process.platform === 'win32';
package/server.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
2
3
  "name": "io.github.ChiR24/unreal-engine-mcp",
3
4
  "description": "MCP server for Unreal Engine 5 with 17 tools for game development automation.",
4
- "version": "0.5.0",
5
+ "version": "0.5.2",
5
6
  "packages": [
6
7
  {
7
8
  "registryType": "npm",
8
9
  "registryBaseUrl": "https://registry.npmjs.org",
9
10
  "identifier": "unreal-engine-mcp-server",
10
- "version": "0.5.0",
11
+ "version": "0.5.2",
11
12
  "transport": {
12
13
  "type": "stdio"
13
14
  },
@@ -58,9 +58,9 @@ export class AutomationBridge extends EventEmitter {
58
58
  private lastHandshakeFailure?: { reason: string; at: Date };
59
59
  private lastDisconnect?: { code: number; reason: string; at: Date };
60
60
  private lastError?: { message: string; at: Date };
61
- private requestQueue: Array<() => void> = [];
62
61
  private queuedRequestItems: Array<{ resolve: (v: any) => void; reject: (e: any) => void; action: string; payload: any; options: any }> = [];
63
62
  private connectionPromise?: Promise<void>;
63
+ private connectionLock = false;
64
64
 
65
65
  constructor(options: AutomationBridgeOptions = {}) {
66
66
  super();
@@ -216,14 +216,15 @@ export class AutomationBridge extends EventEmitter {
216
216
  this.lastHandshakeFailure = undefined;
217
217
  this.connectionManager.updateLastMessageTime();
218
218
 
219
- // Extract remote address/port
220
- const underlying: any = (socket as any)._socket || (socket as any).socket;
219
+ // Extract remote address/port from underlying TCP socket
220
+ // Note: WebSocket types don't expose _socket, but it exists at runtime
221
+ const socketWithInternal = socket as unknown as { _socket?: { remoteAddress?: string; remotePort?: number }; socket?: { remoteAddress?: string; remotePort?: number } };
222
+ const underlying = socketWithInternal._socket || socketWithInternal.socket;
221
223
  const remoteAddr = underlying?.remoteAddress ?? undefined;
222
224
  const remotePort = underlying?.remotePort ?? undefined;
223
225
 
224
226
  this.connectionManager.registerSocket(socket, this.clientPort, metadata, remoteAddr, remotePort);
225
227
  this.connectionManager.startHeartbeat();
226
- this.flushQueue();
227
228
 
228
229
  this.emitAutomation('connected', {
229
230
  socket,
@@ -340,7 +341,7 @@ export class AutomationBridge extends EventEmitter {
340
341
  ? { message: this.lastError.message, at: this.lastError.at.toISOString() }
341
342
  : null,
342
343
  lastMessageAt: this.connectionManager.getLastMessageTime()?.toISOString() ?? null,
343
- lastRequestSentAt: null, // TODO: Track this in RequestTracker?
344
+ lastRequestSentAt: this.requestTracker.getLastRequestSentAt()?.toISOString() ?? null,
344
345
  pendingRequests: this.requestTracker.getPendingCount(),
345
346
  pendingRequestDetails: this.requestTracker.getPendingDetails(),
346
347
  connections: connectionInfos,
@@ -349,8 +350,8 @@ export class AutomationBridge extends EventEmitter {
349
350
  serverName: this.serverName,
350
351
  serverVersion: this.serverVersion,
351
352
  maxConcurrentConnections: this.maxConcurrentConnections,
352
- maxPendingRequests: 100, // TODO: Expose from RequestTracker
353
- heartbeatIntervalMs: 30000 // TODO: Expose from ConnectionManager
353
+ maxPendingRequests: this.requestTracker.getMaxPendingRequests(),
354
+ heartbeatIntervalMs: this.connectionManager.getHeartbeatIntervalMs()
354
355
  };
355
356
  }
356
357
 
@@ -363,8 +364,9 @@ export class AutomationBridge extends EventEmitter {
363
364
  if (this.enabled) {
364
365
  this.log.info('Automation bridge not connected, attempting lazy connection...');
365
366
 
366
- // Avoid multiple simultaneous connection attempts
367
- if (!this.connectionPromise) {
367
+ // Avoid multiple simultaneous connection attempts using lock
368
+ if (!this.connectionPromise && !this.connectionLock) {
369
+ this.connectionLock = true;
368
370
  this.connectionPromise = new Promise<void>((resolve, reject) => {
369
371
  const onConnect = () => {
370
372
  cleanup(); resolve();
@@ -383,8 +385,9 @@ export class AutomationBridge extends EventEmitter {
383
385
  this.off('connected', onConnect);
384
386
  this.off('error', onError);
385
387
  this.off('handshakeFailed', onHandshakeFail);
386
- // If we failed, clear the promise so next attempt can try again
387
- if (this.connectionPromise) this.connectionPromise = undefined;
388
+ // Clear lock and promise so next attempt can try again
389
+ this.connectionLock = false;
390
+ this.connectionPromise = undefined;
388
391
  };
389
392
 
390
393
  this.once('connected', onConnect);
@@ -402,10 +405,16 @@ export class AutomationBridge extends EventEmitter {
402
405
  try {
403
406
  // Wait for connection with a short timeout for the connection itself
404
407
  const connectTimeout = 5000;
405
- await Promise.race([
406
- this.connectionPromise,
407
- new Promise((_, reject) => setTimeout(() => reject(new Error('Lazy connection timeout')), connectTimeout))
408
- ]);
408
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
409
+ const timeoutPromise = new Promise<never>((_, reject) => {
410
+ timeoutId = setTimeout(() => reject(new Error('Lazy connection timeout')), connectTimeout);
411
+ });
412
+
413
+ try {
414
+ await Promise.race([this.connectionPromise, timeoutPromise]);
415
+ } finally {
416
+ if (timeoutId) clearTimeout(timeoutId);
417
+ }
409
418
  } catch (err: any) {
410
419
  this.log.error('Lazy connection failed', err);
411
420
  // We don't throw here immediately, we let the isConnected check fail below
@@ -426,7 +435,7 @@ export class AutomationBridge extends EventEmitter {
426
435
  // We use requestTracker directly to check limit as it's the source of truth
427
436
  // Note: requestTracker exposes maxPendingRequests via constructor but generic check logic isn't public
428
437
  // We assumed getPendingCount() is available
429
- if (this.requestTracker.getPendingCount() >= (this as any).requestTracker.maxPendingRequests) {
438
+ if (this.requestTracker.getPendingCount() >= this.requestTracker.getMaxPendingRequests()) {
430
439
  return new Promise<T>((resolve, reject) => {
431
440
  this.queuedRequestItems.push({
432
441
  resolve,
@@ -478,6 +487,7 @@ export class AutomationBridge extends EventEmitter {
478
487
  }).catch(() => { }); // catch to prevent unhandled rejection during finally chain? no, finally returns new promise
479
488
 
480
489
  if (this.send(message)) {
490
+ this.requestTracker.updateLastRequestSentAt();
481
491
  return resultPromise;
482
492
  } else {
483
493
  this.requestTracker.rejectRequest(requestId, new Error('Failed to send request'));
@@ -491,7 +501,7 @@ export class AutomationBridge extends EventEmitter {
491
501
  // while we have capacity and items
492
502
  while (
493
503
  this.queuedRequestItems.length > 0 &&
494
- this.requestTracker.getPendingCount() < (this as any).requestTracker.maxPendingRequests
504
+ this.requestTracker.getPendingCount() < this.requestTracker.getMaxPendingRequests()
495
505
  ) {
496
506
  const item = this.queuedRequestItems.shift();
497
507
  if (item) {
@@ -541,14 +551,6 @@ export class AutomationBridge extends EventEmitter {
541
551
  return sentCount > 0;
542
552
  }
543
553
 
544
- private flushQueue(): void {
545
- if (this.requestQueue.length === 0) return;
546
- this.log.info(`Flushing ${this.requestQueue.length} queued automation requests`);
547
- const queue = [...this.requestQueue];
548
- this.requestQueue = [];
549
- queue.forEach(fn => fn());
550
- }
551
-
552
554
  private emitAutomation<K extends keyof AutomationBridgeEvents>(
553
555
  event: K,
554
556
  ...args: Parameters<AutomationBridgeEvents[K]>
@@ -17,6 +17,14 @@ export class ConnectionManager extends EventEmitter {
17
17
  super();
18
18
  }
19
19
 
20
+ /**
21
+ * Get the configured heartbeat interval in milliseconds.
22
+ * @returns The heartbeat interval or 0 if disabled
23
+ */
24
+ public getHeartbeatIntervalMs(): number {
25
+ return this.heartbeatIntervalMs;
26
+ }
27
+
20
28
  public registerSocket(
21
29
  socket: WebSocket,
22
30
  port: number,
@@ -47,6 +55,16 @@ export class ConnectionManager extends EventEmitter {
47
55
  socket.on('pong', () => {
48
56
  this.lastMessageAt = new Date();
49
57
  });
58
+
59
+ // Auto-cleanup on close or error
60
+ socket.once('close', () => {
61
+ this.removeSocket(socket);
62
+ });
63
+
64
+ socket.once('error', (error) => {
65
+ this.log.error('Socket error in ConnectionManager', error);
66
+ this.removeSocket(socket);
67
+ });
50
68
  }
51
69
 
52
70
  public removeSocket(socket: WebSocket): SocketInfo | undefined {
@@ -12,6 +12,30 @@ function FStringSafe(val: unknown): string {
12
12
  }
13
13
  }
14
14
 
15
+ /** Response result with optional saved flag */
16
+ interface ResponseResult {
17
+ saved?: boolean;
18
+ action?: string;
19
+ success?: boolean;
20
+ message?: string;
21
+ error?: string;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ /** Event message structure */
26
+ interface EventMessage extends AutomationBridgeMessage {
27
+ requestId?: string;
28
+ event?: string;
29
+ payload?: unknown;
30
+ result?: ResponseResult;
31
+ message?: string;
32
+ }
33
+
34
+ /** Response with optional action field */
35
+ interface ResponseWithAction extends AutomationBridgeResponseMessage {
36
+ action?: string;
37
+ }
38
+
15
39
  export class MessageHandler {
16
40
  private log = new Logger('MessageHandler');
17
41
 
@@ -70,7 +94,7 @@ export class MessageHandler {
70
94
  }
71
95
 
72
96
  // If the response indicates it's already saved/done, resolve immediately
73
- const result = enforcedResponse.result as any;
97
+ const result = enforcedResponse.result as ResponseResult | undefined;
74
98
  if (result && result.saved === true) {
75
99
  this.requestTracker.resolveRequest(requestId, enforcedResponse);
76
100
  return;
@@ -93,7 +117,7 @@ export class MessageHandler {
93
117
  }
94
118
 
95
119
  private handleAutomationEvent(message: AutomationBridgeMessage): void {
96
- const evt: any = message as any;
120
+ const evt = message as EventMessage;
97
121
  const reqId = typeof evt.requestId === 'string' ? evt.requestId : undefined;
98
122
 
99
123
  if (reqId) {
@@ -106,7 +130,7 @@ export class MessageHandler {
106
130
  const synthetic: AutomationBridgeResponseMessage = {
107
131
  type: 'automation_response',
108
132
  requestId: reqId,
109
- success: evtSuccess !== undefined ? evtSuccess : (baseSuccess !== undefined ? baseSuccess : undefined as any),
133
+ success: evtSuccess !== undefined ? evtSuccess : baseSuccess,
110
134
  message: typeof evt.result?.message === 'string' ? evt.result.message : (typeof evt.message === 'string' ? evt.message : FStringSafe(evt.event)),
111
135
  error: typeof evt.result?.error === 'string' ? evt.result.error : undefined,
112
136
  result: evt.result ?? evt.payload ?? undefined
@@ -128,9 +152,10 @@ export class MessageHandler {
128
152
  try {
129
153
  const expected = (expectedAction || '').toString().toLowerCase();
130
154
  const echoed: string | undefined = (() => {
131
- const r: any = response as any;
132
- const candidate = (typeof r.action === 'string' && r.action) || (typeof r.result?.action === 'string' && r.result.action);
133
- return candidate as any;
155
+ const r = response as ResponseWithAction;
156
+ const resultObj = response.result as ResponseResult | undefined;
157
+ const candidate = (typeof r.action === 'string' && r.action) || (typeof resultObj?.action === 'string' && resultObj.action);
158
+ return candidate || undefined;
134
159
  })();
135
160
 
136
161
  if (expected && echoed && typeof echoed === 'string') {
@@ -151,7 +176,7 @@ export class MessageHandler {
151
176
  const startsEitherWay = got.startsWith(expected) || expected.startsWith(got);
152
177
 
153
178
  if (!startsEitherWay) {
154
- const mutated: any = { ...response };
179
+ const mutated: ResponseWithAction = { ...response };
155
180
  mutated.success = false;
156
181
  if (!mutated.error) mutated.error = 'ACTION_PREFIX_MISMATCH';
157
182
  const msgBase = typeof mutated.message === 'string' ? mutated.message + ' ' : '';
@@ -160,7 +185,7 @@ export class MessageHandler {
160
185
  }
161
186
  }
162
187
  } catch (e) {
163
- this.log.debug('enforceActionMatch check skipped', e as any);
188
+ this.log.debug('enforceActionMatch check skipped', e instanceof Error ? e.message : String(e));
164
189
  }
165
190
  return response;
166
191
  }
@@ -1,21 +1,52 @@
1
1
  import { PendingRequest, AutomationBridgeResponseMessage } from './types.js';
2
2
  import { randomUUID, createHash } from 'node:crypto';
3
3
 
4
- // Disabled: The two-phase event pattern was causing timeouts because C++ handlers
5
- // send a single response, not request+event. All actions now use simple request-response.
6
- const WAIT_FOR_EVENT_ACTIONS = new Set<string>([
7
- // Empty - all actions use single response pattern
8
- ]);
4
+ // Note: The two-phase event pattern was disabled because C++ handlers send a single response,
5
+ // not request+event. All actions now use simple request-response. The PendingRequest interface
6
+ // retains waitForEvent/eventTimeout fields for potential future use.
9
7
 
10
8
  export class RequestTracker {
11
9
  private pendingRequests = new Map<string, PendingRequest>();
12
10
  private coalescedRequests = new Map<string, Promise<AutomationBridgeResponseMessage>>();
11
+ private lastRequestSentAt?: Date;
13
12
 
14
13
 
15
14
  constructor(
16
15
  private maxPendingRequests: number
17
16
  ) { }
18
17
 
18
+ /**
19
+ * Get the maximum number of pending requests allowed.
20
+ * @returns The configured maximum pending requests limit
21
+ */
22
+ public getMaxPendingRequests(): number {
23
+ return this.maxPendingRequests;
24
+ }
25
+
26
+ /**
27
+ * Get the timestamp of when the last request was sent.
28
+ * @returns The Date of last request or undefined if no requests sent yet
29
+ */
30
+ public getLastRequestSentAt(): Date | undefined {
31
+ return this.lastRequestSentAt;
32
+ }
33
+
34
+ /**
35
+ * Update the last request sent timestamp.
36
+ * Called when a new request is dispatched.
37
+ */
38
+ public updateLastRequestSentAt(): void {
39
+ this.lastRequestSentAt = new Date();
40
+ }
41
+
42
+ /**
43
+ * Create a new pending request with timeout handling.
44
+ * @param action - The action name being requested
45
+ * @param payload - The request payload
46
+ * @param timeoutMs - Timeout in milliseconds before the request fails
47
+ * @returns Object containing the requestId and a promise that resolves with the response
48
+ * @throws Error if max pending requests limit is reached
49
+ */
19
50
  public createRequest(
20
51
  action: string,
21
52
  payload: Record<string, unknown>,
@@ -26,7 +57,6 @@ export class RequestTracker {
26
57
  }
27
58
 
28
59
  const requestId = randomUUID();
29
- const waitForEvent = WAIT_FOR_EVENT_ACTIONS.has(action);
30
60
 
31
61
  const promise = new Promise<AutomationBridgeResponseMessage>((resolve, reject) => {
32
62
  const timeout = setTimeout(() => {
@@ -43,7 +73,9 @@ export class RequestTracker {
43
73
  action,
44
74
  payload,
45
75
  requestedAt: new Date(),
46
- waitForEvent,
76
+ // Note: waitForEvent and eventTimeoutMs are preserved for potential future use
77
+ // but currently all actions use simple request-response pattern
78
+ waitForEvent: false,
47
79
  eventTimeoutMs: timeoutMs
48
80
  });
49
81
  });
package/src/config.ts CHANGED
@@ -40,7 +40,7 @@ export const EnvSchema = z.object({
40
40
  // Unreal Settings
41
41
  UE_PROJECT_PATH: z.string().optional(),
42
42
  UE_EDITOR_EXE: z.string().optional(),
43
- UE_SCREENSHOT_DIR: z.string().optional(),
43
+
44
44
 
45
45
  // Connection Settings
46
46
  MCP_AUTOMATION_PORT: z.preprocess((v) => stringToNumber(v, 8091), z.number().default(8091)),
package/src/constants.ts CHANGED
@@ -9,4 +9,11 @@ export const DEFAULT_MAX_PENDING_REQUESTS = 25;
9
9
  export const DEFAULT_TIME_OF_DAY = 9;
10
10
  export const DEFAULT_SUN_INTENSITY = 10000;
11
11
  export const DEFAULT_SKYLIGHT_INTENSITY = 1;
12
+
12
13
  export const DEFAULT_SCREENSHOT_RESOLUTION = '1920x1080';
14
+
15
+ // Operation Timeouts
16
+ export const DEFAULT_OPERATION_TIMEOUT_MS = 30000;
17
+ export const DEFAULT_ASSET_OP_TIMEOUT_MS = 60000;
18
+ export const EXTENDED_ASSET_OP_TIMEOUT_MS = 120000;
19
+ export const LONG_RUNNING_OP_TIMEOUT_MS = 300000;