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.
Files changed (75) hide show
  1. package/.github/workflows/publish-mcp.yml +1 -4
  2. package/.github/workflows/release-drafter.yml +2 -1
  3. package/CHANGELOG.md +38 -0
  4. package/dist/automation/bridge.d.ts +1 -2
  5. package/dist/automation/bridge.js +24 -23
  6. package/dist/automation/connection-manager.d.ts +1 -0
  7. package/dist/automation/connection-manager.js +10 -0
  8. package/dist/automation/message-handler.js +5 -4
  9. package/dist/automation/request-tracker.d.ts +4 -0
  10. package/dist/automation/request-tracker.js +11 -3
  11. package/dist/tools/actors.d.ts +19 -1
  12. package/dist/tools/actors.js +15 -5
  13. package/dist/tools/assets.js +1 -1
  14. package/dist/tools/blueprint.d.ts +12 -0
  15. package/dist/tools/blueprint.js +43 -14
  16. package/dist/tools/consolidated-tool-definitions.js +2 -1
  17. package/dist/tools/editor.js +3 -2
  18. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  19. package/dist/tools/handlers/actor-handlers.js +14 -8
  20. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  21. package/dist/tools/handlers/sequence-handlers.js +24 -13
  22. package/dist/tools/introspection.d.ts +1 -1
  23. package/dist/tools/introspection.js +1 -1
  24. package/dist/tools/level.js +3 -3
  25. package/dist/tools/lighting.d.ts +54 -7
  26. package/dist/tools/lighting.js +4 -4
  27. package/dist/tools/materials.d.ts +1 -1
  28. package/dist/types/tool-types.d.ts +2 -0
  29. package/dist/unreal-bridge.js +4 -4
  30. package/dist/utils/command-validator.js +6 -5
  31. package/dist/utils/error-handler.d.ts +24 -2
  32. package/dist/utils/error-handler.js +58 -23
  33. package/dist/utils/normalize.d.ts +7 -4
  34. package/dist/utils/normalize.js +12 -10
  35. package/dist/utils/response-validator.js +88 -73
  36. package/dist/utils/unreal-command-queue.d.ts +2 -0
  37. package/dist/utils/unreal-command-queue.js +8 -1
  38. package/docs/handler-mapping.md +4 -2
  39. package/package.json +1 -1
  40. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  41. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  42. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  43. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  44. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  45. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  46. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  47. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  48. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  49. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  50. package/server.json +3 -3
  51. package/src/automation/bridge.ts +27 -25
  52. package/src/automation/connection-manager.ts +18 -0
  53. package/src/automation/message-handler.ts +33 -8
  54. package/src/automation/request-tracker.ts +39 -7
  55. package/src/server/tool-registry.ts +3 -3
  56. package/src/tools/actors.ts +44 -19
  57. package/src/tools/assets.ts +3 -3
  58. package/src/tools/blueprint.ts +115 -49
  59. package/src/tools/consolidated-tool-definitions.ts +2 -1
  60. package/src/tools/editor.ts +4 -3
  61. package/src/tools/handlers/actor-handlers.ts +14 -9
  62. package/src/tools/handlers/sequence-handlers.ts +86 -63
  63. package/src/tools/introspection.ts +7 -7
  64. package/src/tools/level.ts +6 -6
  65. package/src/tools/lighting.ts +19 -19
  66. package/src/tools/materials.ts +1 -1
  67. package/src/tools/sequence.ts +1 -1
  68. package/src/tools/ui.ts +1 -1
  69. package/src/types/tool-types.ts +4 -0
  70. package/src/unreal-bridge.ts +71 -26
  71. package/src/utils/command-validator.ts +46 -5
  72. package/src/utils/error-handler.ts +128 -45
  73. package/src/utils/normalize.ts +38 -16
  74. package/src/utils/response-validator.ts +103 -87
  75. package/src/utils/unreal-command-queue.ts +13 -1
@@ -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
 
package/server.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
- "$schema": "https://raw.githubusercontent.com/modelcontextprotocol/registry/main/schema.json",
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.1",
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.1",
11
+ "version": "0.5.2",
12
12
  "transport": {
13
13
  "type": "stdio"
14
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
  });
@@ -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: any = request.params.arguments || {};
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 any)[k];
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: any = {};
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;
@@ -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<ActorResponse>(
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 errorMsg = typeof error === 'string' ? error : (error as any)?.message || response?.message || 'Failed to spawn actor';
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 = (response as any).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 || (response as any).actorName,
73
- actorPath: data.objectPath || (response as any).actorPath,
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 || (response as any).actorName,
81
- path: data.objectPath || (response as any).actorPath || mappedClassPath
95
+ name: data.name || response.actorName,
96
+ path: data.objectPath || response.actorPath || mappedClassPath
82
97
  }
83
98
  };
84
99
 
85
- if ((response as any).warnings?.length) {
86
- result.warnings = (response as any).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 ((response as any).details?.length) {
91
- result.details = (response as any).details;
105
+ if (response.details?.length) {
106
+ result.details = response.details;
92
107
  }
93
- if ((response as any).componentPaths?.length) {
94
- result.componentPaths = (response as any).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
- return this.sendRequest<StandardActionResponse>('get_transform', { actorName }, 'control_actor')
297
- .then(response => {
298
- // If response is standardized, extract data or return as is.
299
- // For now, return the full response which includes data.
300
- return response;
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 }) {
@@ -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, any>;
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: any) {
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