unreal-engine-mcp-server 0.5.0 → 0.5.1

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 (139) 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 +1 -0
  6. package/.github/workflows/release-drafter.yml +1 -1
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +71 -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/config.d.ts +0 -1
  14. package/dist/config.js +0 -1
  15. package/dist/constants.d.ts +4 -0
  16. package/dist/constants.js +4 -0
  17. package/dist/graphql/loaders.d.ts +64 -0
  18. package/dist/graphql/loaders.js +117 -0
  19. package/dist/graphql/resolvers.d.ts +3 -3
  20. package/dist/graphql/resolvers.js +33 -30
  21. package/dist/graphql/server.js +3 -1
  22. package/dist/graphql/types.d.ts +2 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +13 -2
  25. package/dist/server-setup.d.ts +0 -1
  26. package/dist/server-setup.js +0 -40
  27. package/dist/tools/actors.d.ts +40 -24
  28. package/dist/tools/actors.js +8 -2
  29. package/dist/tools/assets.d.ts +19 -71
  30. package/dist/tools/assets.js +28 -22
  31. package/dist/tools/base-tool.d.ts +4 -4
  32. package/dist/tools/base-tool.js +1 -1
  33. package/dist/tools/blueprint.d.ts +33 -61
  34. package/dist/tools/consolidated-tool-handlers.js +96 -110
  35. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  36. package/dist/tools/dynamic-handler-registry.js +17 -95
  37. package/dist/tools/editor.d.ts +19 -193
  38. package/dist/tools/editor.js +8 -0
  39. package/dist/tools/environment.d.ts +8 -14
  40. package/dist/tools/foliage.d.ts +18 -143
  41. package/dist/tools/foliage.js +4 -2
  42. package/dist/tools/handlers/actor-handlers.js +0 -5
  43. package/dist/tools/handlers/asset-handlers.js +454 -454
  44. package/dist/tools/landscape.d.ts +16 -116
  45. package/dist/tools/landscape.js +7 -3
  46. package/dist/tools/level.d.ts +22 -103
  47. package/dist/tools/level.js +24 -16
  48. package/dist/tools/lighting.js +5 -1
  49. package/dist/tools/materials.js +5 -1
  50. package/dist/tools/niagara.js +37 -2
  51. package/dist/tools/performance.d.ts +0 -1
  52. package/dist/tools/performance.js +0 -1
  53. package/dist/tools/physics.js +5 -1
  54. package/dist/tools/sequence.d.ts +24 -24
  55. package/dist/tools/sequence.js +13 -0
  56. package/dist/tools/ui.d.ts +0 -2
  57. package/dist/types/automation-responses.d.ts +115 -0
  58. package/dist/types/automation-responses.js +2 -0
  59. package/dist/types/responses.d.ts +249 -0
  60. package/dist/types/responses.js +2 -0
  61. package/dist/types/tool-interfaces.d.ts +135 -135
  62. package/dist/utils/command-validator.js +3 -2
  63. package/dist/utils/path-security.d.ts +2 -0
  64. package/dist/utils/path-security.js +24 -0
  65. package/dist/utils/response-factory.d.ts +4 -4
  66. package/dist/utils/response-factory.js +15 -21
  67. package/docs/Migration-Guide-v0.5.0.md +1 -9
  68. package/docs/testing-guide.md +2 -2
  69. package/package.json +12 -6
  70. package/scripts/run-all-tests.mjs +25 -20
  71. package/server.json +3 -2
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +7 -0
  74. package/src/graphql/loaders.ts +244 -0
  75. package/src/graphql/resolvers.ts +47 -49
  76. package/src/graphql/server.ts +3 -1
  77. package/src/graphql/types.ts +3 -0
  78. package/src/index.ts +15 -2
  79. package/src/resources/assets.ts +5 -4
  80. package/src/server-setup.ts +3 -37
  81. package/src/tools/actors.ts +36 -28
  82. package/src/tools/animation.ts +1 -0
  83. package/src/tools/assets.ts +74 -63
  84. package/src/tools/base-tool.ts +3 -3
  85. package/src/tools/blueprint.ts +59 -59
  86. package/src/tools/consolidated-tool-handlers.ts +129 -150
  87. package/src/tools/dynamic-handler-registry.ts +22 -140
  88. package/src/tools/editor.ts +39 -26
  89. package/src/tools/environment.ts +21 -27
  90. package/src/tools/foliage.ts +28 -25
  91. package/src/tools/handlers/actor-handlers.ts +2 -8
  92. package/src/tools/handlers/asset-handlers.ts +484 -484
  93. package/src/tools/handlers/sequence-handlers.ts +1 -1
  94. package/src/tools/landscape.ts +34 -28
  95. package/src/tools/level.ts +96 -76
  96. package/src/tools/lighting.ts +6 -1
  97. package/src/tools/materials.ts +8 -2
  98. package/src/tools/niagara.ts +44 -2
  99. package/src/tools/performance.ts +1 -2
  100. package/src/tools/physics.ts +7 -1
  101. package/src/tools/sequence.ts +41 -25
  102. package/src/tools/ui.ts +0 -2
  103. package/src/types/automation-responses.ts +119 -0
  104. package/src/types/responses.ts +355 -0
  105. package/src/types/tool-interfaces.ts +135 -135
  106. package/src/utils/command-validator.ts +3 -2
  107. package/src/utils/normalize.test.ts +162 -0
  108. package/src/utils/path-security.ts +43 -0
  109. package/src/utils/response-factory.ts +29 -24
  110. package/src/utils/safe-json.test.ts +90 -0
  111. package/src/utils/validation.test.ts +184 -0
  112. package/tests/test-animation.mjs +358 -33
  113. package/tests/test-asset-graph.mjs +311 -0
  114. package/tests/test-audio.mjs +314 -116
  115. package/tests/test-behavior-tree.mjs +327 -144
  116. package/tests/test-blueprint-graph.mjs +343 -12
  117. package/tests/test-control-editor.mjs +85 -53
  118. package/tests/test-graphql.mjs +58 -8
  119. package/tests/test-input.mjs +349 -0
  120. package/tests/test-inspect.mjs +291 -61
  121. package/tests/test-landscape.mjs +304 -48
  122. package/tests/test-lighting.mjs +428 -0
  123. package/tests/test-manage-level.mjs +70 -51
  124. package/tests/test-performance.mjs +539 -0
  125. package/tests/test-sequence.mjs +82 -46
  126. package/tests/test-system.mjs +72 -33
  127. package/tests/test-wasm.mjs +98 -8
  128. package/vitest.config.ts +35 -0
  129. package/.github/release-drafter.yml +0 -148
  130. package/dist/prompts/index.d.ts +0 -21
  131. package/dist/prompts/index.js +0 -217
  132. package/dist/tools/blueprint/helpers.d.ts +0 -29
  133. package/dist/tools/blueprint/helpers.js +0 -182
  134. package/src/prompts/index.ts +0 -249
  135. package/src/tools/blueprint/helpers.ts +0 -189
  136. package/tests/test-blueprint-events.mjs +0 -35
  137. package/tests/test-extra-tools.mjs +0 -38
  138. package/tests/test-render.mjs +0 -33
  139. package/tests/test-search-assets.mjs +0 -66
@@ -129,6 +129,35 @@ interface Blueprint {
129
129
  scsHierarchy?: Record<string, any>;
130
130
  }
131
131
 
132
+ import { Logger } from '../utils/logger.js';
133
+
134
+ const log = new Logger('GraphQL:Resolvers');
135
+
136
+ /**
137
+ * Creates a GraphQL-friendly error with proper extensions
138
+ */
139
+ class GraphQLResolverError extends Error {
140
+ extensions: { code: string; originalError?: string };
141
+
142
+ constructor(message: string, code: string = 'UNREAL_ENGINE_ERROR', originalError?: Error) {
143
+ super(message);
144
+ this.name = 'GraphQLResolverError';
145
+ this.extensions = {
146
+ code,
147
+ originalError: originalError?.message
148
+ };
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Helper to create resolver errors with proper logging
154
+ */
155
+ function createResolverError(operation: string, error: unknown): GraphQLResolverError {
156
+ const message = error instanceof Error ? error.message : String(error);
157
+ log.error(`${operation} failed:`, message);
158
+ return new GraphQLResolverError(`${operation} failed: ${message}`, 'UNREAL_ENGINE_ERROR', error instanceof Error ? error : undefined);
159
+ }
160
+
132
161
  function logAutomationFailure(source: string, response: any) {
133
162
  try {
134
163
  if (!response || response.success !== false) {
@@ -138,7 +167,7 @@ function logAutomationFailure(source: string, response: any) {
138
167
  if (errorText.length === 0) {
139
168
  return;
140
169
  }
141
- console.error(`[GraphQL] ${source} automation failure:`, errorText);
170
+ log.error(`${source} automation failure:`, errorText);
142
171
  } catch {
143
172
  }
144
173
  }
@@ -158,7 +187,7 @@ async function getActorProperties(
158
187
  });
159
188
  return result.success ? result.value || {} : {};
160
189
  } catch (error) {
161
- console.error('Failed to get actor properties:', error);
190
+ log.error('Failed to get actor properties:', error);
162
191
  return {};
163
192
  }
164
193
  }
@@ -190,11 +219,11 @@ async function listAssets(
190
219
  }
191
220
 
192
221
  logAutomationFailure('list_assets', response);
193
- console.error('Failed to list assets:', response);
222
+ log.warn('Failed to list assets - returning empty set');
194
223
  return { assets: [], totalCount: 0 };
195
224
  } catch (error) {
196
- console.error('Failed to list assets:', error);
197
- return { assets: [], totalCount: 0 };
225
+ log.error('Failed to list assets:', error);
226
+ throw createResolverError('listAssets', error);
198
227
  }
199
228
  }
200
229
 
@@ -229,33 +258,7 @@ async function listActors(
229
258
  }
230
259
  }
231
260
 
232
- /**
233
- * Helper to get blueprint details
234
- */
235
- async function getBlueprint(
236
- automationBridge: AutomationBridge,
237
- blueprintPath: string
238
- ): Promise<Blueprint | null> {
239
- try {
240
- const response = await automationBridge.sendAutomationRequest(
241
- 'get_blueprint',
242
- {
243
- blueprintPath
244
- },
245
- { timeoutMs: 30000 }
246
- );
247
261
 
248
- if (response.success && response.result) {
249
- return response.result as Blueprint;
250
- }
251
-
252
- logAutomationFailure('get_blueprint', response);
253
- return null;
254
- } catch (error) {
255
- console.error('Failed to get blueprint:', error);
256
- return null;
257
- }
258
- }
259
262
 
260
263
  /**
261
264
  * GraphQL Resolvers Implementation
@@ -291,17 +294,10 @@ export const resolvers = {
291
294
 
292
295
  asset: async (_: any, { path }: { path: string }, context: GraphQLContext) => {
293
296
  try {
294
- const response = await context.automationBridge.sendAutomationRequest(
295
- 'get_asset',
296
- { assetPath: path },
297
- { timeoutMs: 10000 }
298
- );
299
-
300
- if (response.success && response.result) {
301
- return response.result;
297
+ if (!context.loaders) {
298
+ throw new Error('Loaders not initialized');
302
299
  }
303
-
304
- return null;
300
+ return await context.loaders.assetLoader.load(path);
305
301
  } catch (error) {
306
302
  console.error('Failed to get asset:', error);
307
303
  return null;
@@ -336,11 +332,10 @@ export const resolvers = {
336
332
 
337
333
  actor: async (_: any, { name }: { name: string }, context: GraphQLContext) => {
338
334
  try {
339
- const actors = await listActors(context.automationBridge, { tag: name });
340
- if (actors.actors.length > 0) {
341
- return actors.actors[0];
335
+ if (!context.loaders) {
336
+ throw new Error('Loaders not initialized');
342
337
  }
343
- return null;
338
+ return await context.loaders.actorLoader.load(name);
344
339
  } catch (error) {
345
340
  console.error('Failed to get actor:', error);
346
341
  return null;
@@ -394,7 +389,10 @@ export const resolvers = {
394
389
  },
395
390
 
396
391
  blueprint: async (_: any, { path }: { path: string }, context: GraphQLContext) => {
397
- return await getBlueprint(context.automationBridge, path);
392
+ if (!context.loaders) {
393
+ throw new Error('Loaders not initialized');
394
+ }
395
+ return await context.loaders.blueprintLoader.load(path);
398
396
  },
399
397
 
400
398
  levels: async (_: any, __: any, context: GraphQLContext) => {
@@ -524,9 +522,9 @@ export const resolvers = {
524
522
  { subAction: 'get_cells' },
525
523
  { timeoutMs: 10000 }
526
524
  );
527
-
525
+
528
526
  if (response.success && response.result) {
529
- return (response.result as any).cells || [];
527
+ return (response.result as any).cells || [];
530
528
  }
531
529
  return [];
532
530
  } catch (error) {
@@ -550,7 +548,7 @@ export const resolvers = {
550
548
  const edges = assets.map((asset, index) => ({
551
549
  node: {
552
550
  ...asset,
553
- emitters: [],
551
+ emitters: [],
554
552
  parameters: []
555
553
  },
556
554
  cursor: Buffer.from(`${asset.path}:${offset + index}`).toString('base64')
@@ -5,6 +5,7 @@ import { createGraphQLSchema } from './schema.js';
5
5
  import type { GraphQLContext } from './types.js';
6
6
  import type { UnrealBridge } from '../unreal-bridge.js';
7
7
  import { AutomationBridge } from '../automation/index.js';
8
+ import { createLoaders } from './loaders.js';
8
9
 
9
10
  export interface GraphQLServerConfig {
10
11
  enabled?: boolean;
@@ -65,7 +66,8 @@ export class GraphQLServer {
65
66
  },
66
67
  context: (): GraphQLContext => ({
67
68
  bridge: this.bridge,
68
- automationBridge: this.automationBridge
69
+ automationBridge: this.automationBridge,
70
+ loaders: createLoaders(this.automationBridge)
69
71
  }),
70
72
  logging: {
71
73
  debug: (...args) => this.log.debug('[GraphQL]', ...args),
@@ -1,7 +1,10 @@
1
1
  import type { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { AutomationBridge } from '../automation/index.js';
3
+ import type { GraphQLLoaders } from './loaders.js';
3
4
 
4
5
  export interface GraphQLContext {
5
6
  bridge: UnrealBridge;
6
7
  automationBridge: AutomationBridge;
8
+ /** DataLoaders for batching and caching - solves N+1 query problem */
9
+ loaders?: GraphQLLoaders;
7
10
  }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import { HealthMonitor } from './services/health-monitor.js';
13
13
  import { ServerSetup } from './server-setup.js';
14
14
  import { startMetricsServer } from './services/metrics-server.js';
15
15
  import { config } from './config.js';
16
+ import { GraphQLServer } from './graphql/server.js';
16
17
 
17
18
  const require = createRequire(import.meta.url);
18
19
  const packageInfo: { name?: string; version?: string } = (() => {
@@ -106,6 +107,12 @@ export function createServer() {
106
107
  // Optionally expose Prometheus-style metrics via /metrics
107
108
  startMetricsServer({ healthMonitor, automationBridge, logger: log });
108
109
 
110
+ // Initialize GraphQL server (controlled by GRAPHQL_ENABLED env var)
111
+ const graphqlServer = new GraphQLServer(bridge, automationBridge);
112
+ graphqlServer.start().catch((error) => {
113
+ log.warn('GraphQL server failed to start:', error);
114
+ });
115
+
109
116
  // Initialize WebAssembly module for high-performance operations (5-8x faster)
110
117
  log.debug('Initializing WebAssembly integration...');
111
118
  initializeWASM().then(() => {
@@ -145,7 +152,7 @@ export function createServer() {
145
152
  const serverSetup = new ServerSetup(server, bridge, automationBridge, log, healthMonitor);
146
153
  serverSetup.setup(); // Register tools, resources, and prompts
147
154
 
148
- return { server, bridge, automationBridge };
155
+ return { server, bridge, automationBridge, graphqlServer };
149
156
  }
150
157
 
151
158
  // Export configuration schema for session UI and runtime validation
@@ -170,7 +177,7 @@ export default function createServerDefault({ config }: { config?: any } = {}) {
170
177
  }
171
178
 
172
179
  export async function startStdioServer() {
173
- const { server, automationBridge } = createServer();
180
+ const { server, automationBridge, graphqlServer } = createServer();
174
181
  const transport = new StdioServerTransport();
175
182
  let shuttingDown = false;
176
183
 
@@ -187,6 +194,12 @@ export async function startStdioServer() {
187
194
  log.warn('Failed to stop automation bridge cleanly', error);
188
195
  }
189
196
 
197
+ try {
198
+ await graphqlServer.stop();
199
+ } catch (error) {
200
+ log.warn('Failed to stop GraphQL server cleanly', error);
201
+ }
202
+
190
203
  try {
191
204
  if (typeof (server as any).close === 'function') {
192
205
  await (server as any).close();
@@ -1,6 +1,7 @@
1
1
  import { BaseTool } from '../tools/base-tool.js';
2
2
  import { IAssetResources } from '../types/tool-interfaces.js';
3
3
  import { coerceString } from '../utils/result-helpers.js';
4
+ import { AutomationResponse } from '../types/automation-responses.js';
4
5
 
5
6
  export class AssetResources extends BaseTool implements IAssetResources {
6
7
  // Simple in-memory cache for asset listing
@@ -175,14 +176,14 @@ export class AssetResources extends BaseTool implements IAssetResources {
175
176
  // Use the native C++ plugin's list action instead of Python
176
177
  try {
177
178
  const normalizedDir = this.normalizeDir(dir);
178
- const response = await this.sendAutomationRequest(
179
+ const response = await this.sendAutomationRequest<AutomationResponse>(
179
180
  'list',
180
181
  { directory: normalizedDir, limit, recursive: false },
181
182
  { timeoutMs: 30000 }
182
183
  );
183
184
 
184
185
  if (response.success !== false && response.result) {
185
- const payload = response.result;
186
+ const payload = response.result as any;
186
187
 
187
188
  const foldersArr = Array.isArray(payload.folders_list)
188
189
  ? payload.folders_list.map((f: any) => ({
@@ -257,12 +258,12 @@ export class AssetResources extends BaseTool implements IAssetResources {
257
258
 
258
259
  try {
259
260
  const normalizedPath = this.normalizeDir(assetPath);
260
- const response = await this.sendAutomationRequest(
261
+ const response = await this.sendAutomationRequest<AutomationResponse>(
261
262
  'asset_exists',
262
263
  { asset_path: normalizedPath }
263
264
  );
264
265
 
265
- return response?.success !== false && response?.result?.exists === true;
266
+ return response?.success !== false && (response?.result as any)?.exists === true;
266
267
  } catch {
267
268
  return false;
268
269
  }
@@ -1,10 +1,10 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
- import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
+ // import { ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
3
  import { UnrealBridge } from './unreal-bridge.js';
4
4
  import { AutomationBridge } from './automation/index.js';
5
5
  import { Logger } from './utils/logger.js';
6
6
  import { HealthMonitor } from './services/health-monitor.js';
7
- import { prompts } from './prompts/index.js';
7
+ // import { prompts } from './prompts/index.js';
8
8
  import { AssetResources } from './resources/assets.js';
9
9
  import { ActorResources } from './resources/actors.js';
10
10
  import { LevelResources } from './resources/levels.js';
@@ -73,7 +73,7 @@ export class ServerSetup {
73
73
  );
74
74
  toolRegistry.register();
75
75
 
76
- this.registerPrompts();
76
+ // this.registerPrompts();
77
77
  }
78
78
 
79
79
  private validateEnvironment() {
@@ -110,39 +110,5 @@ export class ServerSetup {
110
110
  return ok;
111
111
  }
112
112
 
113
- private registerPrompts() {
114
- this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
115
- return {
116
- prompts: prompts.map(p => ({
117
- name: p.name,
118
- description: p.description,
119
- arguments: Object.entries(p.arguments || {}).map(([name, schema]) => {
120
- const meta: Record<string, unknown> = {};
121
- if (schema.type) meta.type = schema.type;
122
- if (schema.enum) meta.enum = schema.enum;
123
- if (schema.default !== undefined) meta.default = schema.default;
124
- return {
125
- name,
126
- description: schema.description,
127
- required: schema.required ?? false,
128
- ...(Object.keys(meta).length ? { _meta: meta } : {})
129
- };
130
- })
131
- }))
132
- };
133
- });
134
113
 
135
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
136
- const prompt = prompts.find(p => p.name === request.params.name);
137
- if (!prompt) {
138
- throw new Error(`Unknown prompt: ${request.params.name}`);
139
- }
140
- const args = (request.params.arguments || {}) as Record<string, unknown>;
141
- const messages = prompt.build(args);
142
- return {
143
- description: prompt.description,
144
- messages
145
- };
146
- });
147
- }
148
114
  }
@@ -2,6 +2,8 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { ensureRotation, ensureVector3 } from '../utils/validation.js';
3
3
  import { BaseTool } from './base-tool.js';
4
4
  import { IActorTools, StandardActionResponse } from '../types/tool-interfaces.js';
5
+ import { ActorResponse } from '../types/automation-responses.js';
6
+ import { wasmIntegration } from '../wasm/index.js';
5
7
 
6
8
  export class ActorTools extends BaseTool implements IActorTools {
7
9
  constructor(bridge: UnrealBridge) {
@@ -44,7 +46,7 @@ export class ActorTools extends BaseTool implements IActorTools {
44
46
  try {
45
47
  const bridge = this.getAutomationBridge();
46
48
  const timeoutMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0 ? params.timeoutMs : undefined;
47
- const response = await bridge.sendAutomationRequest(
49
+ const response = await bridge.sendAutomationRequest<ActorResponse>(
48
50
  'control_actor',
49
51
  {
50
52
  action: 'spawn',
@@ -58,7 +60,9 @@ export class ActorTools extends BaseTool implements IActorTools {
58
60
  );
59
61
 
60
62
  if (!response || !response.success) {
61
- throw new Error(response?.error || response?.message || 'Failed to spawn actor');
63
+ const error = response?.error;
64
+ const errorMsg = typeof error === 'string' ? error : (error as any)?.message || response?.message || 'Failed to spawn actor';
65
+ throw new Error(errorMsg);
62
66
  }
63
67
 
64
68
  const data = (response as any).data || {};
@@ -117,7 +121,7 @@ export class ActorTools extends BaseTool implements IActorTools {
117
121
  // DELETE_PARTIAL as a handled, partial-success cleanup instead
118
122
  // of surfacing it as a hard error to the consolidated handler.
119
123
  const bridge = this.getAutomationBridge();
120
- const response: any = await bridge.sendAutomationRequest('control_actor', {
124
+ const response: any = await bridge.sendAutomationRequest<ActorResponse>('control_actor', {
121
125
  action: 'delete',
122
126
  actorNames: names
123
127
  });
@@ -158,7 +162,7 @@ export class ActorTools extends BaseTool implements IActorTools {
158
162
  throw new Error('Invalid actorName');
159
163
  }
160
164
 
161
- return this.sendRequest('delete', { actorName: params.actorName }, 'control_actor');
165
+ return this.sendRequest<StandardActionResponse>('delete', { actorName: params.actorName }, 'control_actor');
162
166
  }
163
167
 
164
168
  async applyForce(params: { actorName: string; force: { x: number; y: number; z: number } }) {
@@ -183,7 +187,7 @@ export class ActorTools extends BaseTool implements IActorTools {
183
187
  };
184
188
  }
185
189
 
186
- return this.sendRequest('apply_force', {
190
+ return this.sendRequest<StandardActionResponse>('apply_force', {
187
191
  actorName: params.actorName,
188
192
  force: { x: forceX, y: forceY, z: forceZ }
189
193
  }, 'control_actor');
@@ -259,7 +263,7 @@ export class ActorTools extends BaseTool implements IActorTools {
259
263
  if (location) payload.location = { x: location[0], y: location[1], z: location[2] };
260
264
  if (rotation) payload.rotation = { pitch: rotation[0], yaw: rotation[1], roll: rotation[2] };
261
265
 
262
- return this.sendRequest('spawn_blueprint', payload, 'control_actor');
266
+ return this.sendRequest<StandardActionResponse>('spawn_blueprint', payload, 'control_actor');
263
267
  }
264
268
 
265
269
  async setTransform(params: { actorName: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number }; scale?: { x: number; y: number; z: number } }) {
@@ -282,14 +286,14 @@ export class ActorTools extends BaseTool implements IActorTools {
282
286
  payload.scale = { x: scl[0], y: scl[1], z: scl[2] };
283
287
  }
284
288
 
285
- return this.sendRequest('set_transform', payload, 'control_actor');
289
+ return this.sendRequest<StandardActionResponse>('set_transform', payload, 'control_actor');
286
290
  }
287
291
 
288
292
  async getTransform(actorName: string) {
289
293
  if (typeof actorName !== 'string' || actorName.trim().length === 0) {
290
294
  throw new Error('Invalid actorName');
291
295
  }
292
- return this.sendRequest('get_transform', { actorName }, 'control_actor')
296
+ return this.sendRequest<StandardActionResponse>('get_transform', { actorName }, 'control_actor')
293
297
  .then(response => {
294
298
  // If response is standardized, extract data or return as is.
295
299
  // For now, return the full response which includes data.
@@ -302,7 +306,7 @@ export class ActorTools extends BaseTool implements IActorTools {
302
306
  if (!actorName) {
303
307
  throw new Error('Invalid actorName');
304
308
  }
305
- return this.sendRequest('set_visibility', { actorName, visible: Boolean(params.visible) }, 'control_actor');
309
+ return this.sendRequest<StandardActionResponse>('set_visibility', { actorName, visible: Boolean(params.visible) }, 'control_actor');
306
310
  }
307
311
 
308
312
  async addComponent(params: { actorName: string; componentType: string; componentName?: string; properties?: Record<string, unknown> }) {
@@ -311,7 +315,7 @@ export class ActorTools extends BaseTool implements IActorTools {
311
315
  if (!actorName) throw new Error('Invalid actorName');
312
316
  if (!componentType) throw new Error('Invalid componentType');
313
317
 
314
- return this.sendRequest('add_component', {
318
+ return this.sendRequest<StandardActionResponse>('add_component', {
315
319
  actorName,
316
320
  componentType,
317
321
  componentName: typeof params.componentName === 'string' ? params.componentName : undefined,
@@ -325,7 +329,7 @@ export class ActorTools extends BaseTool implements IActorTools {
325
329
  if (!actorName) throw new Error('Invalid actorName');
326
330
  if (!componentName) throw new Error('Invalid componentName');
327
331
 
328
- return this.sendRequest('set_component_properties', {
332
+ return this.sendRequest<StandardActionResponse>('set_component_properties', {
329
333
  actorName,
330
334
  componentName,
331
335
  properties: params.properties ?? {}
@@ -336,7 +340,7 @@ export class ActorTools extends BaseTool implements IActorTools {
336
340
  if (typeof actorName !== 'string' || actorName.trim().length === 0) {
337
341
  throw new Error('Invalid actorName');
338
342
  }
339
- const response = await this.sendRequest('get_components', { actorName }, 'control_actor');
343
+ const response = await this.sendRequest<StandardActionResponse>('get_components', { actorName }, 'control_actor');
340
344
  if (!response.success) {
341
345
  return { success: false, error: response.error || `Failed to get components for actor ${actorName}` };
342
346
  }
@@ -365,10 +369,14 @@ export class ActorTools extends BaseTool implements IActorTools {
365
369
  }
366
370
  if (params.offset) {
367
371
  const offs = ensureVector3(params.offset, 'duplicate offset');
368
- payload.offset = { x: offs[0], y: offs[1], z: offs[2] };
372
+ // Use WASM vectorAdd for offset calculation (origin + offset)
373
+ const origin: [number, number, number] = [0, 0, 0];
374
+ const calculatedOffset = wasmIntegration.vectorAdd(origin, offs);
375
+ console.error('[WASM] Using vectorAdd for duplicate offset calculation');
376
+ payload.offset = { x: calculatedOffset[0], y: calculatedOffset[1], z: calculatedOffset[2] };
369
377
  }
370
378
 
371
- return this.sendRequest('duplicate', payload, 'control_actor');
379
+ return this.sendRequest<StandardActionResponse>('duplicate', payload, 'control_actor');
372
380
  }
373
381
 
374
382
  async addTag(params: { actorName: string; tag: string }) {
@@ -377,7 +385,7 @@ export class ActorTools extends BaseTool implements IActorTools {
377
385
  if (!actorName) throw new Error('Invalid actorName');
378
386
  if (!tag) throw new Error('Invalid tag');
379
387
 
380
- return this.sendRequest('add_tag', { actorName, tag }, 'control_actor');
388
+ return this.sendRequest<StandardActionResponse>('add_tag', { actorName, tag }, 'control_actor');
381
389
  }
382
390
 
383
391
  async removeTag(params: { actorName: string; tag: string }) {
@@ -386,7 +394,7 @@ export class ActorTools extends BaseTool implements IActorTools {
386
394
  if (!actorName) throw new Error('Invalid actorName');
387
395
  if (!tag) throw new Error('Invalid tag');
388
396
 
389
- return this.sendRequest('remove_tag', { actorName, tag }, 'control_actor');
397
+ return this.sendRequest<StandardActionResponse>('remove_tag', { actorName, tag }, 'control_actor');
390
398
  }
391
399
 
392
400
  async findByTag(params: { tag: string; matchType?: string }) {
@@ -404,7 +412,7 @@ export class ActorTools extends BaseTool implements IActorTools {
404
412
  };
405
413
  }
406
414
 
407
- return this.sendRequest('find_by_tag', {
415
+ return this.sendRequest<StandardActionResponse>('find_by_tag', {
408
416
  tag,
409
417
  matchType: typeof params.matchType === 'string' ? params.matchType : undefined
410
418
  }, 'control_actor');
@@ -414,7 +422,7 @@ export class ActorTools extends BaseTool implements IActorTools {
414
422
  if (typeof name !== 'string' || name.trim().length === 0) {
415
423
  throw new Error('Invalid actor name query');
416
424
  }
417
- return this.sendRequest('find_by_name', { name: name.trim() }, 'control_actor');
425
+ return this.sendRequest<StandardActionResponse>('find_by_name', { name: name.trim() }, 'control_actor');
418
426
  }
419
427
 
420
428
  async detach(actorName: string) {
@@ -428,7 +436,7 @@ export class ActorTools extends BaseTool implements IActorTools {
428
436
  if (typeof actorName !== 'string' || actorName.trim().length === 0) {
429
437
  throw new Error('Invalid actorName');
430
438
  }
431
- return this.sendRequest('detach', { actorName }, 'control_actor');
439
+ return this.sendRequest<StandardActionResponse>('detach', { actorName }, 'control_actor');
432
440
  }
433
441
 
434
442
  async attach(params: { childActor: string; parentActor: string }) {
@@ -437,20 +445,20 @@ export class ActorTools extends BaseTool implements IActorTools {
437
445
  if (!child) throw new Error('Invalid childActor');
438
446
  if (!parent) throw new Error('Invalid parentActor');
439
447
 
440
- return this.sendRequest('attach', { childActor: child, parentActor: parent }, 'control_actor');
448
+ return this.sendRequest<StandardActionResponse>('attach', { childActor: child, parentActor: parent }, 'control_actor');
441
449
  }
442
450
 
443
451
  async deleteByTag(tag: string) {
444
452
  if (typeof tag !== 'string' || tag.trim().length === 0) {
445
453
  throw new Error('Invalid tag');
446
454
  }
447
- return this.sendRequest('delete_by_tag', { tag: tag.trim() }, 'control_actor');
455
+ return this.sendRequest<StandardActionResponse>('delete_by_tag', { tag: tag.trim() }, 'control_actor');
448
456
  }
449
457
 
450
458
  async setBlueprintVariables(params: { actorName: string; variables: Record<string, unknown> }) {
451
459
  const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
452
460
  if (!actorName) throw new Error('Invalid actorName');
453
- return this.sendRequest('set_blueprint_variables', { actorName, variables: params.variables ?? {} }, 'control_actor');
461
+ return this.sendRequest<StandardActionResponse>('set_blueprint_variables', { actorName, variables: params.variables ?? {} }, 'control_actor');
454
462
  }
455
463
 
456
464
  async createSnapshot(params: { actorName: string; snapshotName: string }) {
@@ -458,7 +466,7 @@ export class ActorTools extends BaseTool implements IActorTools {
458
466
  const snapshotName = typeof params.snapshotName === 'string' ? params.snapshotName.trim() : '';
459
467
  if (!actorName) throw new Error('Invalid actorName');
460
468
  if (!snapshotName) throw new Error('Invalid snapshotName');
461
- return this.sendRequest('create_snapshot', { actorName, snapshotName }, 'control_actor');
469
+ return this.sendRequest<StandardActionResponse>('create_snapshot', { actorName, snapshotName }, 'control_actor');
462
470
  }
463
471
 
464
472
  async restoreSnapshot(params: { actorName: string; snapshotName: string }) {
@@ -466,12 +474,12 @@ export class ActorTools extends BaseTool implements IActorTools {
466
474
  const snapshotName = typeof params.snapshotName === 'string' ? params.snapshotName.trim() : '';
467
475
  if (!actorName) throw new Error('Invalid actorName');
468
476
  if (!snapshotName) throw new Error('Invalid snapshotName');
469
- return this.sendRequest('restore_snapshot', { actorName, snapshotName }, 'control_actor');
477
+ return this.sendRequest<StandardActionResponse>('restore_snapshot', { actorName, snapshotName }, 'control_actor');
470
478
  }
471
479
  async exportActor(params: { actorName: string; destinationPath?: string }) {
472
480
  const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
473
481
  if (!actorName) throw new Error('Invalid actorName');
474
- return this.sendRequest('export', {
482
+ return this.sendRequest<StandardActionResponse>('export', {
475
483
  actorName,
476
484
  destinationPath: params.destinationPath
477
485
  }, 'control_actor');
@@ -481,7 +489,7 @@ export class ActorTools extends BaseTool implements IActorTools {
481
489
  if (typeof actorName !== 'string' || actorName.trim().length === 0) {
482
490
  throw new Error('Invalid actorName');
483
491
  }
484
- const response = await this.sendRequest('get_bounding_box', { actorName }, 'control_actor');
492
+ const response = await this.sendRequest<StandardActionResponse>('get_bounding_box', { actorName }, 'control_actor');
485
493
  if (!response.success) {
486
494
  return { success: false, error: response.error || `Failed to get bounding box for actor ${actorName}` };
487
495
  }
@@ -496,7 +504,7 @@ export class ActorTools extends BaseTool implements IActorTools {
496
504
  if (typeof actorName !== 'string' || actorName.trim().length === 0) {
497
505
  throw new Error('Invalid actorName');
498
506
  }
499
- const response = await this.sendRequest('get_metadata', { actorName }, 'control_actor');
507
+ const response = await this.sendRequest<StandardActionResponse>('get_metadata', { actorName }, 'control_actor');
500
508
  if (!response.success) {
501
509
  return { success: false, error: response.error || `Failed to get metadata for actor ${actorName}` };
502
510
  }
@@ -512,7 +520,7 @@ export class ActorTools extends BaseTool implements IActorTools {
512
520
  if (params?.filter) {
513
521
  payload.filter = params.filter;
514
522
  }
515
- const response = await this.sendRequest('list_actors', payload, 'control_actor');
523
+ const response = await this.sendRequest<StandardActionResponse>('list_actors', payload, 'control_actor');
516
524
  if (!response.success) {
517
525
  return { success: false, error: response.error || 'Failed to list actors' };
518
526
  }
@@ -2,6 +2,7 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { AutomationBridge } from '../automation/index.js';
3
3
  import { cleanObject } from '../utils/safe-json.js';
4
4
  import { validateAssetParams } from '../utils/validation.js';
5
+ import { wasmIntegration as _wasmIntegration } from '../wasm/index.js';
5
6
 
6
7
  type CreateAnimationBlueprintSuccess = {
7
8
  success: true;