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.
- package/.env.example +1 -1
- package/.github/release-drafter-config.yml +51 -0
- package/.github/workflows/greetings.yml +5 -1
- package/.github/workflows/labeler.yml +2 -1
- package/.github/workflows/publish-mcp.yml +2 -4
- package/.github/workflows/release-drafter.yml +3 -2
- package/.github/workflows/release.yml +3 -3
- package/CHANGELOG.md +109 -0
- package/CONTRIBUTING.md +1 -1
- package/GEMINI.md +115 -0
- package/Public/Plugin_setup_guide.mp4 +0 -0
- package/README.md +166 -200
- package/dist/automation/bridge.d.ts +1 -2
- package/dist/automation/bridge.js +24 -23
- package/dist/automation/connection-manager.d.ts +1 -0
- package/dist/automation/connection-manager.js +10 -0
- package/dist/automation/message-handler.js +5 -4
- package/dist/automation/request-tracker.d.ts +4 -0
- package/dist/automation/request-tracker.js +11 -3
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +4 -0
- package/dist/graphql/loaders.d.ts +64 -0
- package/dist/graphql/loaders.js +117 -0
- package/dist/graphql/resolvers.d.ts +3 -3
- package/dist/graphql/resolvers.js +33 -30
- package/dist/graphql/server.js +3 -1
- package/dist/graphql/types.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -2
- package/dist/server-setup.d.ts +0 -1
- package/dist/server-setup.js +0 -40
- package/dist/tools/actors.d.ts +58 -24
- package/dist/tools/actors.js +22 -6
- package/dist/tools/assets.d.ts +19 -71
- package/dist/tools/assets.js +28 -22
- package/dist/tools/base-tool.d.ts +4 -4
- package/dist/tools/base-tool.js +1 -1
- package/dist/tools/blueprint.d.ts +45 -61
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/consolidated-tool-handlers.js +96 -110
- package/dist/tools/dynamic-handler-registry.d.ts +11 -9
- package/dist/tools/dynamic-handler-registry.js +17 -95
- package/dist/tools/editor.d.ts +19 -193
- package/dist/tools/editor.js +11 -2
- package/dist/tools/environment.d.ts +8 -14
- package/dist/tools/foliage.d.ts +18 -143
- package/dist/tools/foliage.js +4 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -13
- package/dist/tools/handlers/asset-handlers.js +454 -454
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/sequence-handlers.js +24 -13
- package/dist/tools/introspection.d.ts +1 -1
- package/dist/tools/introspection.js +1 -1
- package/dist/tools/landscape.d.ts +16 -116
- package/dist/tools/landscape.js +7 -3
- package/dist/tools/level.d.ts +22 -103
- package/dist/tools/level.js +26 -18
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +9 -5
- package/dist/tools/materials.d.ts +1 -1
- package/dist/tools/materials.js +5 -1
- package/dist/tools/niagara.js +37 -2
- package/dist/tools/performance.d.ts +0 -1
- package/dist/tools/performance.js +0 -1
- package/dist/tools/physics.js +5 -1
- package/dist/tools/sequence.d.ts +24 -24
- package/dist/tools/sequence.js +13 -0
- package/dist/tools/ui.d.ts +0 -2
- package/dist/types/automation-responses.d.ts +115 -0
- package/dist/types/automation-responses.js +2 -0
- package/dist/types/responses.d.ts +249 -0
- package/dist/types/responses.js +2 -0
- package/dist/types/tool-interfaces.d.ts +135 -135
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +7 -5
- package/dist/utils/error-handler.d.ts +24 -2
- package/dist/utils/error-handler.js +58 -23
- package/dist/utils/normalize.d.ts +7 -4
- package/dist/utils/normalize.js +12 -10
- package/dist/utils/path-security.d.ts +2 -0
- package/dist/utils/path-security.js +24 -0
- package/dist/utils/response-factory.d.ts +4 -4
- package/dist/utils/response-factory.js +15 -21
- package/dist/utils/response-validator.js +88 -73
- package/dist/utils/unreal-command-queue.d.ts +2 -0
- package/dist/utils/unreal-command-queue.js +8 -1
- package/docs/Migration-Guide-v0.5.0.md +1 -9
- package/docs/handler-mapping.md +4 -2
- package/docs/testing-guide.md +2 -2
- package/package.json +12 -6
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
- package/scripts/run-all-tests.mjs +25 -20
- package/server.json +3 -2
- package/src/automation/bridge.ts +27 -25
- package/src/automation/connection-manager.ts +18 -0
- package/src/automation/message-handler.ts +33 -8
- package/src/automation/request-tracker.ts +39 -7
- package/src/config.ts +1 -1
- package/src/constants.ts +7 -0
- package/src/graphql/loaders.ts +244 -0
- package/src/graphql/resolvers.ts +47 -49
- package/src/graphql/server.ts +3 -1
- package/src/graphql/types.ts +3 -0
- package/src/index.ts +15 -2
- package/src/resources/assets.ts +5 -4
- package/src/server/tool-registry.ts +3 -3
- package/src/server-setup.ts +3 -37
- package/src/tools/actors.ts +77 -44
- package/src/tools/animation.ts +1 -0
- package/src/tools/assets.ts +76 -65
- package/src/tools/base-tool.ts +3 -3
- package/src/tools/blueprint.ts +170 -104
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/consolidated-tool-handlers.ts +129 -150
- package/src/tools/dynamic-handler-registry.ts +22 -140
- package/src/tools/editor.ts +43 -29
- package/src/tools/environment.ts +21 -27
- package/src/tools/foliage.ts +28 -25
- package/src/tools/handlers/actor-handlers.ts +16 -17
- package/src/tools/handlers/asset-handlers.ts +484 -484
- package/src/tools/handlers/sequence-handlers.ts +85 -62
- package/src/tools/introspection.ts +7 -7
- package/src/tools/landscape.ts +34 -28
- package/src/tools/level.ts +100 -80
- package/src/tools/lighting.ts +25 -20
- package/src/tools/materials.ts +9 -3
- package/src/tools/niagara.ts +44 -2
- package/src/tools/performance.ts +1 -2
- package/src/tools/physics.ts +7 -1
- package/src/tools/sequence.ts +42 -26
- package/src/tools/ui.ts +1 -3
- package/src/types/automation-responses.ts +119 -0
- package/src/types/responses.ts +355 -0
- package/src/types/tool-interfaces.ts +135 -135
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +47 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.test.ts +162 -0
- package/src/utils/normalize.ts +38 -16
- package/src/utils/path-security.ts +43 -0
- package/src/utils/response-factory.ts +29 -24
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/safe-json.test.ts +90 -0
- package/src/utils/unreal-command-queue.ts +13 -1
- package/src/utils/validation.test.ts +184 -0
- package/tests/test-animation.mjs +358 -33
- package/tests/test-asset-graph.mjs +311 -0
- package/tests/test-audio.mjs +314 -116
- package/tests/test-behavior-tree.mjs +327 -144
- package/tests/test-blueprint-graph.mjs +343 -12
- package/tests/test-control-editor.mjs +85 -53
- package/tests/test-graphql.mjs +58 -8
- package/tests/test-input.mjs +349 -0
- package/tests/test-inspect.mjs +291 -61
- package/tests/test-landscape.mjs +304 -48
- package/tests/test-lighting.mjs +428 -0
- package/tests/test-manage-level.mjs +70 -51
- package/tests/test-performance.mjs +539 -0
- package/tests/test-sequence.mjs +82 -46
- package/tests/test-system.mjs +72 -33
- package/tests/test-wasm.mjs +98 -8
- package/vitest.config.ts +35 -0
- package/.github/release-drafter.yml +0 -148
- package/dist/prompts/index.d.ts +0 -21
- package/dist/prompts/index.js +0 -217
- package/dist/tools/blueprint/helpers.d.ts +0 -29
- package/dist/tools/blueprint/helpers.js +0 -182
- package/src/prompts/index.ts +0 -249
- package/src/tools/blueprint/helpers.ts +0 -189
- package/tests/test-blueprint-events.mjs +0 -35
- package/tests/test-extra-tools.mjs +0 -38
- package/tests/test-render.mjs +0 -33
- package/tests/test-search-assets.mjs +0 -66
|
@@ -2,27 +2,32 @@
|
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
|
|
4
4
|
const tests = [
|
|
5
|
-
'test:control_actor', //
|
|
6
|
-
'test:control_editor', //
|
|
7
|
-
'test:manage_level', //
|
|
8
|
-
'test:animation',
|
|
9
|
-
'test:materials',
|
|
10
|
-
'test:niagara',
|
|
11
|
-
'test:landscape',
|
|
12
|
-
'test:sequence', //
|
|
13
|
-
'test:system',
|
|
14
|
-
'test:console_command',
|
|
15
|
-
'test:inspect',
|
|
16
|
-
'test:manage_asset',
|
|
17
|
-
'test:blueprint',
|
|
18
|
-
'test:blueprint_graph',
|
|
19
|
-
|
|
20
|
-
'test:
|
|
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',
|
|
23
|
-
'test:asset_advanced',
|
|
24
|
-
'test:
|
|
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.
|
|
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.
|
|
11
|
+
"version": "0.5.2",
|
|
11
12
|
"transport": {
|
|
12
13
|
"type": "stdio"
|
|
13
14
|
},
|
package/src/automation/bridge.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
353
|
-
heartbeatIntervalMs:
|
|
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
|
-
//
|
|
387
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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() >=
|
|
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() <
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
132
|
-
const
|
|
133
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
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
|
-
|
|
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;
|