unreal-engine-mcp-server 0.5.1 → 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/.github/workflows/publish-mcp.yml +1 -4
- package/.github/workflows/release-drafter.yml +2 -1
- package/CHANGELOG.md +38 -0
- 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/tools/actors.d.ts +19 -1
- package/dist/tools/actors.js +15 -5
- package/dist/tools/assets.js +1 -1
- package/dist/tools/blueprint.d.ts +12 -0
- package/dist/tools/blueprint.js +43 -14
- package/dist/tools/consolidated-tool-definitions.js +2 -1
- package/dist/tools/editor.js +3 -2
- package/dist/tools/handlers/actor-handlers.d.ts +1 -1
- package/dist/tools/handlers/actor-handlers.js +14 -8
- 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/level.js +3 -3
- package/dist/tools/lighting.d.ts +54 -7
- package/dist/tools/lighting.js +4 -4
- package/dist/tools/materials.d.ts +1 -1
- package/dist/types/tool-types.d.ts +2 -0
- package/dist/unreal-bridge.js +4 -4
- package/dist/utils/command-validator.js +6 -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/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/handler-mapping.md +4 -2
- package/package.json +1 -1
- 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/server.json +3 -3
- 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/server/tool-registry.ts +3 -3
- package/src/tools/actors.ts +44 -19
- package/src/tools/assets.ts +3 -3
- package/src/tools/blueprint.ts +115 -49
- package/src/tools/consolidated-tool-definitions.ts +2 -1
- package/src/tools/editor.ts +4 -3
- package/src/tools/handlers/actor-handlers.ts +14 -9
- package/src/tools/handlers/sequence-handlers.ts +86 -63
- package/src/tools/introspection.ts +7 -7
- package/src/tools/level.ts +6 -6
- package/src/tools/lighting.ts +19 -19
- package/src/tools/materials.ts +1 -1
- package/src/tools/sequence.ts +1 -1
- package/src/tools/ui.ts +1 -1
- package/src/types/tool-types.ts +4 -0
- package/src/unreal-bridge.ts +71 -26
- package/src/utils/command-validator.ts +46 -5
- package/src/utils/error-handler.ts +128 -45
- package/src/utils/normalize.ts +38 -16
- package/src/utils/response-validator.ts +103 -87
- package/src/utils/unreal-command-queue.ts +13 -1
package/server.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "https://
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.ChiR24/unreal-engine-mcp",
|
|
4
4
|
"description": "MCP server for Unreal Engine 5 with 17 tools for game development automation.",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.2",
|
|
6
6
|
"packages": [
|
|
7
7
|
{
|
|
8
8
|
"registryType": "npm",
|
|
9
9
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
10
10
|
"identifier": "unreal-engine-mcp-server",
|
|
11
|
-
"version": "0.5.
|
|
11
|
+
"version": "0.5.2",
|
|
12
12
|
"transport": {
|
|
13
13
|
"type": "stdio"
|
|
14
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
|
});
|
|
@@ -210,7 +210,7 @@ export class ToolRegistry {
|
|
|
210
210
|
|
|
211
211
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
212
212
|
const { name } = request.params;
|
|
213
|
-
let args:
|
|
213
|
+
let args: Record<string, unknown> = request.params.arguments || {};
|
|
214
214
|
const startTime = Date.now();
|
|
215
215
|
|
|
216
216
|
const connected = await this.ensureConnected();
|
|
@@ -258,13 +258,13 @@ export class ToolRegistry {
|
|
|
258
258
|
const props = inputSchema.properties || {};
|
|
259
259
|
const required: string[] = Array.isArray(inputSchema.required) ? inputSchema.required : [];
|
|
260
260
|
const missing = required.filter((k: string) => {
|
|
261
|
-
const v = (args as
|
|
261
|
+
const v = (args as Record<string, unknown>)[k];
|
|
262
262
|
if (v === undefined || v === null) return true;
|
|
263
263
|
if (typeof v === 'string' && v.trim() === '') return true;
|
|
264
264
|
return false;
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
const primitiveProps:
|
|
267
|
+
const primitiveProps: Record<string, unknown> = {};
|
|
268
268
|
for (const k of missing) {
|
|
269
269
|
const p = props[k];
|
|
270
270
|
if (!p || typeof p !== 'object') continue;
|
package/src/tools/actors.ts
CHANGED
|
@@ -5,6 +5,20 @@ import { IActorTools, StandardActionResponse } from '../types/tool-interfaces.js
|
|
|
5
5
|
import { ActorResponse } from '../types/automation-responses.js';
|
|
6
6
|
import { wasmIntegration } from '../wasm/index.js';
|
|
7
7
|
|
|
8
|
+
/** Extended actor response with spawn-specific fields */
|
|
9
|
+
interface SpawnActorResponse extends ActorResponse {
|
|
10
|
+
data?: {
|
|
11
|
+
name?: string;
|
|
12
|
+
objectPath?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
actorName?: string;
|
|
16
|
+
actorPath?: string;
|
|
17
|
+
warnings?: string[];
|
|
18
|
+
details?: unknown[];
|
|
19
|
+
componentPaths?: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export class ActorTools extends BaseTool implements IActorTools {
|
|
9
23
|
constructor(bridge: UnrealBridge) {
|
|
10
24
|
super(bridge);
|
|
@@ -46,7 +60,7 @@ export class ActorTools extends BaseTool implements IActorTools {
|
|
|
46
60
|
try {
|
|
47
61
|
const bridge = this.getAutomationBridge();
|
|
48
62
|
const timeoutMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0 ? params.timeoutMs : undefined;
|
|
49
|
-
const response = await bridge.sendAutomationRequest<
|
|
63
|
+
const response = await bridge.sendAutomationRequest<SpawnActorResponse>(
|
|
50
64
|
'control_actor',
|
|
51
65
|
{
|
|
52
66
|
action: 'spawn',
|
|
@@ -61,37 +75,38 @@ export class ActorTools extends BaseTool implements IActorTools {
|
|
|
61
75
|
|
|
62
76
|
if (!response || !response.success) {
|
|
63
77
|
const error = response?.error;
|
|
64
|
-
const
|
|
78
|
+
const errorObj = typeof error === 'object' && error !== null ? error as { message?: string } : null;
|
|
79
|
+
const errorMsg = typeof error === 'string' ? error : errorObj?.message || response?.message || 'Failed to spawn actor';
|
|
65
80
|
throw new Error(errorMsg);
|
|
66
81
|
}
|
|
67
82
|
|
|
68
|
-
const data =
|
|
83
|
+
const data = response.data || {};
|
|
69
84
|
const result: StandardActionResponse = {
|
|
70
85
|
success: true,
|
|
71
86
|
message: response.message || `Spawned actor ${className}`,
|
|
72
|
-
actorName: data.name ||
|
|
73
|
-
actorPath: data.objectPath ||
|
|
87
|
+
actorName: data.name || response.actorName,
|
|
88
|
+
actorPath: data.objectPath || response.actorPath,
|
|
74
89
|
resolvedClass: mappedClassPath,
|
|
75
90
|
requestedClass: className,
|
|
76
91
|
location: { x: locX, y: locY, z: locZ },
|
|
77
92
|
rotation: { pitch: rotPitch, yaw: rotYaw, roll: rotRoll },
|
|
78
93
|
data: data,
|
|
79
94
|
actor: {
|
|
80
|
-
name: data.name ||
|
|
81
|
-
path: data.objectPath ||
|
|
95
|
+
name: data.name || response.actorName,
|
|
96
|
+
path: data.objectPath || response.actorPath || mappedClassPath
|
|
82
97
|
}
|
|
83
98
|
};
|
|
84
99
|
|
|
85
|
-
if (
|
|
86
|
-
result.warnings =
|
|
100
|
+
if (response.warnings?.length) {
|
|
101
|
+
result.warnings = response.warnings;
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
// Legacy support for older fields if they exist at top level
|
|
90
|
-
if (
|
|
91
|
-
result.details =
|
|
105
|
+
if (response.details?.length) {
|
|
106
|
+
result.details = response.details;
|
|
92
107
|
}
|
|
93
|
-
if (
|
|
94
|
-
result.componentPaths =
|
|
108
|
+
if (response.componentPaths?.length) {
|
|
109
|
+
result.componentPaths = response.componentPaths;
|
|
95
110
|
}
|
|
96
111
|
|
|
97
112
|
return result;
|
|
@@ -293,12 +308,22 @@ export class ActorTools extends BaseTool implements IActorTools {
|
|
|
293
308
|
if (typeof actorName !== 'string' || actorName.trim().length === 0) {
|
|
294
309
|
throw new Error('Invalid actorName');
|
|
295
310
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
311
|
+
const response = await this.sendRequest<StandardActionResponse>('get_transform', { actorName }, 'control_actor');
|
|
312
|
+
if (!response.success) {
|
|
313
|
+
return { success: false, error: response.error || `Failed to get transform for actor ${actorName}` };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Extract transform data from nested response (data.data or data or result)
|
|
317
|
+
const rawData: any = response.data ?? response.result ?? response;
|
|
318
|
+
const data: any = rawData?.data ?? rawData;
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
message: 'Transform retrieved',
|
|
323
|
+
location: data.location ?? data.Location,
|
|
324
|
+
rotation: data.rotation ?? data.Rotation,
|
|
325
|
+
scale: data.scale ?? data.Scale
|
|
326
|
+
};
|
|
302
327
|
}
|
|
303
328
|
|
|
304
329
|
async setVisibility(params: { actorName: string; visible: boolean }) {
|
package/src/tools/assets.ts
CHANGED
|
@@ -230,7 +230,7 @@ export class AssetTools extends BaseTool implements IAssetTools {
|
|
|
230
230
|
// BaseTool unwraps the result, so 'response' is likely the payload itself.
|
|
231
231
|
// However, if the result was null, 'response' might be the wrapper.
|
|
232
232
|
// We handle both cases to be robust.
|
|
233
|
-
const resultObj = (response.result || response) as Record<string,
|
|
233
|
+
const resultObj = (response.result || response) as Record<string, unknown>;
|
|
234
234
|
return {
|
|
235
235
|
success: true,
|
|
236
236
|
message: 'Metadata retrieved',
|
|
@@ -310,8 +310,8 @@ export class AssetTools extends BaseTool implements IAssetTools {
|
|
|
310
310
|
message: 'graph analyzed',
|
|
311
311
|
analysis
|
|
312
312
|
};
|
|
313
|
-
} catch (e:
|
|
314
|
-
return { success: false, error: `Analysis failed: ${e.message} ` };
|
|
313
|
+
} catch (e: unknown) {
|
|
314
|
+
return { success: false, error: `Analysis failed: ${e instanceof Error ? e.message : String(e)} ` };
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|