unreal-engine-mcp-server 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -1,7 +1,18 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
- import { ITools } from '../../types/tool-interfaces.js';
2
+ import { ITools, StandardActionResponse } from '../../types/tool-interfaces.js';
3
3
  import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
4
4
 
5
+ /** Extended response with common sequence fields */
6
+ interface SequenceActionResponse extends StandardActionResponse {
7
+ result?: {
8
+ sequencePath?: string;
9
+ results?: Array<{ success?: boolean; error?: string }>;
10
+ [key: string]: unknown;
11
+ };
12
+ bindings?: Array<{ name?: string;[key: string]: unknown }>;
13
+ message?: string;
14
+ }
15
+
5
16
  const managedSequences = new Set<string>();
6
17
  const deletedSequences = new Set<string>();
7
18
 
@@ -22,37 +33,48 @@ function markSequenceDeleted(path: unknown) {
22
33
  const norm = normalizeSequencePath(path);
23
34
  if (!norm) return;
24
35
  managedSequences.delete(norm);
25
- deletedSequences.add(norm);
36
+ deletedSequences.delete(norm);
37
+ }
38
+
39
+ /** Helper to safely get string from error/message */
40
+ function getErrorString(res: SequenceActionResponse | null | undefined): string {
41
+ if (!res) return '';
42
+ return typeof res.error === 'string' ? res.error : '';
43
+ }
44
+
45
+ function getMessageString(res: SequenceActionResponse | null | undefined): string {
46
+ if (!res) return '';
47
+ return typeof res.message === 'string' ? res.message : '';
26
48
  }
27
49
 
28
50
 
29
51
 
30
- export async function handleSequenceTools(action: string, args: any, tools: ITools) {
52
+ export async function handleSequenceTools(action: string, args: Record<string, unknown>, tools: ITools) {
31
53
  const seqAction = String(action || '').trim();
32
54
  switch (seqAction) {
33
55
  case 'create': {
34
56
  const name = requireNonEmptyString(args.name, 'name', 'Missing required parameter: name');
35
- const res = await tools.sequenceTools.create({ name, path: args.path });
57
+ const res = await tools.sequenceTools.create({ name, path: args.path as string | undefined }) as SequenceActionResponse;
36
58
 
37
59
  let sequencePath: string | undefined;
38
- if (res && (res as any).result && typeof (res as any).result.sequencePath === 'string') {
39
- sequencePath = (res as any).result.sequencePath;
60
+ if (res && res.result && typeof res.result.sequencePath === 'string') {
61
+ sequencePath = res.result.sequencePath;
40
62
  } else if (typeof args.path === 'string' && args.path.trim().length > 0) {
41
63
  const basePath = args.path.trim().replace(/\/$/, '');
42
64
  sequencePath = `${basePath}/${name}`;
43
65
  }
44
- if (sequencePath && res && (res as any).success !== false) {
66
+ if (sequencePath && res && res.success !== false) {
45
67
  markSequenceCreated(sequencePath);
46
68
  }
47
69
 
48
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
49
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
50
- if (res && (res as any).success === false && (errorCode === 'FACTORY_NOT_AVAILABLE' || msgLower.includes('ulevelsequencefactorynew not available'))) {
70
+ const errorCode = getErrorString(res).toUpperCase();
71
+ const msgLower = getMessageString(res).toLowerCase();
72
+ if (res && res.success === false && (errorCode === 'FACTORY_NOT_AVAILABLE' || msgLower.includes('ulevelsequencefactorynew not available'))) {
51
73
  const path = sequencePath || (typeof args.path === 'string' ? args.path : undefined);
52
74
  return cleanObject({
53
75
  success: false,
54
76
  error: 'FACTORY_NOT_AVAILABLE',
55
- message: (res as any).message || 'Sequence creation failed: factory not available',
77
+ message: res.message || 'Sequence creation failed: factory not available',
56
78
  action: 'create',
57
79
  name,
58
80
  path,
@@ -69,7 +91,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
69
91
  return cleanObject(res);
70
92
  }
71
93
  case 'add_camera': {
72
- const res = await tools.sequenceTools.addCamera({ spawnable: args.spawnable, path: args.path });
94
+ const res = await tools.sequenceTools.addCamera({ spawnable: args.spawnable as boolean | undefined, path: args.path as string | undefined });
73
95
  return cleanObject(res);
74
96
  }
75
97
  case 'add_actor': {
@@ -82,18 +104,18 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
82
104
  subAction: 'add_actor'
83
105
  };
84
106
 
85
- const res = await executeAutomationRequest(tools, 'manage_sequence', payload);
107
+ const res = await executeAutomationRequest(tools, 'manage_sequence', payload) as SequenceActionResponse;
86
108
 
87
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
88
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
109
+ const errorCode = getErrorString(res).toUpperCase();
110
+ const msgLower = getMessageString(res).toLowerCase();
89
111
 
90
- if (res && (res as any).success === false && path) {
112
+ if (res && res.success === false && path) {
91
113
  const isInvalidSequence = errorCode === 'INVALID_SEQUENCE' || msgLower.includes('sequence_add_actor requires a sequence path') || msgLower.includes('sequence not found');
92
114
  if (isInvalidSequence) {
93
115
  return cleanObject({
94
116
  success: false,
95
117
  error: 'NOT_FOUND',
96
- message: (res as any).message || 'Sequence not found',
118
+ message: res.message || 'Sequence not found',
97
119
  action: 'add_actor',
98
120
  path,
99
121
  actorName
@@ -101,8 +123,8 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
101
123
  }
102
124
  }
103
125
 
104
- const results = res && (res as any).result && Array.isArray((res as any).result.results)
105
- ? (res as any).result.results as any[]
126
+ const results = res && res.result && Array.isArray(res.result.results)
127
+ ? res.result.results
106
128
  : undefined;
107
129
  if (results && results.length) {
108
130
  const failed = results.find((item) => item && item.success === false && typeof item.error === 'string');
@@ -124,24 +146,24 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
124
146
  return cleanObject(res);
125
147
  }
126
148
  case 'add_actors': {
127
- const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames : [];
128
- const res = await tools.sequenceTools.addActors({ actorNames, path: args.path });
129
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
130
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
131
- if (actorNames.length === 0 && res && (res as any).success === false && errorCode === 'INVALID_ARGUMENT') {
149
+ const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames as string[] : [];
150
+ const res = await tools.sequenceTools.addActors({ actorNames, path: args.path as string | undefined }) as SequenceActionResponse;
151
+ const errorCode = getErrorString(res).toUpperCase();
152
+ const msgLower = getMessageString(res).toLowerCase();
153
+ if (actorNames.length === 0 && res && res.success === false && errorCode === 'INVALID_ARGUMENT') {
132
154
  return cleanObject({
133
155
  success: false,
134
156
  error: 'INVALID_ARGUMENT',
135
- message: (res as any).message || 'Invalid argument: actorNames required',
157
+ message: res.message || 'Invalid argument: actorNames required',
136
158
  action: 'add_actors',
137
159
  actorNames
138
160
  });
139
161
  }
140
- if (res && (res as any).success === false && msgLower.includes('actor not found')) {
162
+ if (res && res.success === false && msgLower.includes('actor not found')) {
141
163
  return cleanObject({
142
164
  success: false,
143
165
  error: 'NOT_FOUND',
144
- message: (res as any).message || 'Actor not found',
166
+ message: res.message || 'Actor not found',
145
167
  action: 'add_actors',
146
168
  actorNames
147
169
  });
@@ -149,8 +171,8 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
149
171
  return cleanObject(res);
150
172
  }
151
173
  case 'remove_actors': {
152
- const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames : [];
153
- const res = await tools.sequenceTools.removeActors({ actorNames, path: args.path });
174
+ const actorNames: string[] = Array.isArray(args.actorNames) ? args.actorNames as string[] : [];
175
+ const res = await tools.sequenceTools.removeActors({ actorNames, path: args.path as string | undefined });
154
176
  return cleanObject(res);
155
177
  }
156
178
  case 'get_bindings': {
@@ -164,7 +186,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
164
186
  const property = typeof args.property === 'string' ? args.property : undefined;
165
187
  const frame = typeof args.frame === 'number' ? args.frame : Number(args.frame);
166
188
 
167
- const payload = {
189
+ const payload: Record<string, unknown> = {
168
190
  ...args,
169
191
  path: path || args.path,
170
192
  actorName,
@@ -185,16 +207,16 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
185
207
  payload.value = { scale: args.value };
186
208
  }
187
209
 
188
- const res = await executeAutomationRequest(tools, 'manage_sequence', payload);
189
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
190
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
210
+ const res = await executeAutomationRequest(tools, 'manage_sequence', payload) as SequenceActionResponse;
211
+ const errorCode = getErrorString(res).toUpperCase();
212
+ const msgLower = getMessageString(res).toLowerCase();
191
213
 
192
214
  // Keep explicit INVALID_ARGUMENT for missing frame as a real error
193
215
  if (errorCode === 'INVALID_ARGUMENT' || msgLower.includes('frame number is required')) {
194
216
  return cleanObject(res);
195
217
  }
196
218
 
197
- if (res && (res as any).success === false) {
219
+ if (res && res.success === false) {
198
220
  const isBindingIssue = errorCode === 'BINDING_NOT_FOUND' || msgLower.includes('binding not found');
199
221
  const isUnsupported = errorCode === 'UNSUPPORTED_PROPERTY' || msgLower.includes('unsupported property') || msgLower.includes('invalid_sequence_type');
200
222
  const isInvalidSeq = errorCode === 'INVALID_SEQUENCE' || msgLower.includes('sequence not found') || msgLower.includes('requires a sequence path');
@@ -203,7 +225,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
203
225
  return cleanObject({
204
226
  success: false,
205
227
  error: 'NOT_FOUND',
206
- message: (res as any).message || 'Sequence not found',
228
+ message: res.message || 'Sequence not found',
207
229
  action: 'add_keyframe',
208
230
  path,
209
231
  actorName,
@@ -222,28 +244,28 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
222
244
  }
223
245
  case 'add_spawnable_from_class': {
224
246
  const className = requireNonEmptyString(args.className, 'className', 'Missing required parameter: className');
225
- const res = await tools.sequenceTools.addSpawnableFromClass({ className, path: args.path });
247
+ const res = await tools.sequenceTools.addSpawnableFromClass({ className, path: args.path as string | undefined });
226
248
  return cleanObject(res);
227
249
  }
228
250
  case 'play': {
229
- const res = await tools.sequenceTools.play({ path: args.path, startTime: args.startTime, loopMode: args.loopMode });
251
+ const res = await tools.sequenceTools.play({ path: args.path as string | undefined, startTime: args.startTime as number | undefined, loopMode: args.loopMode as 'once' | 'loop' | 'pingpong' | undefined });
230
252
  return cleanObject(res);
231
253
  }
232
254
  case 'pause': {
233
- const res = await tools.sequenceTools.pause({ path: args.path });
255
+ const res = await tools.sequenceTools.pause({ path: args.path as string | undefined });
234
256
  return cleanObject(res);
235
257
  }
236
258
  case 'stop': {
237
- const res = await tools.sequenceTools.stop({ path: args.path });
259
+ const res = await tools.sequenceTools.stop({ path: args.path as string | undefined });
238
260
  return cleanObject(res);
239
261
  }
240
262
  case 'set_properties': {
241
263
  const res = await tools.sequenceTools.setSequenceProperties({
242
- path: args.path,
243
- frameRate: args.frameRate,
244
- lengthInFrames: args.lengthInFrames,
245
- playbackStart: args.playbackStart,
246
- playbackEnd: args.playbackEnd
264
+ path: args.path as string | undefined,
265
+ frameRate: args.frameRate as number | undefined,
266
+ lengthInFrames: args.lengthInFrames as number | undefined,
267
+ playbackStart: args.playbackStart as number | undefined,
268
+ playbackEnd: args.playbackEnd as number | undefined
247
269
  });
248
270
  return cleanObject(res);
249
271
  }
@@ -258,31 +280,32 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
258
280
  throw new Error('Invalid speed: must be a positive number');
259
281
  }
260
282
  // Try setting speed
261
- let res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path });
283
+ let res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path as string | undefined }) as SequenceActionResponse;
262
284
 
263
285
  // Fix: Auto-open if editor not open
264
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
286
+ const errorCode = getErrorString(res).toUpperCase();
265
287
  if ((!res || res.success === false) && errorCode === 'EDITOR_NOT_OPEN' && args.path) {
266
288
  // Attempt to open the sequence
267
- await tools.sequenceTools.open({ path: args.path });
289
+ await tools.sequenceTools.open({ path: args.path as string });
268
290
 
269
291
  // Wait a short moment for editor to initialize on game thread
270
292
  await new Promise(resolve => setTimeout(resolve, 1000));
271
293
 
272
294
  // Retry
273
- res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path });
295
+ res = await tools.sequenceTools.setPlaybackSpeed({ speed, path: args.path as string | undefined }) as SequenceActionResponse;
274
296
  }
275
297
 
276
298
  return cleanObject(res);
277
299
  }
278
300
  case 'list': {
279
- const res = await tools.sequenceTools.list({ path: args.path });
301
+ const res = await tools.sequenceTools.list({ path: args.path as string | undefined });
280
302
  return cleanObject(res);
281
303
  }
282
304
  case 'duplicate': {
283
305
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
284
306
  const destDir = requireNonEmptyString(args.destinationPath, 'destinationPath', 'Missing required parameter: destinationPath');
285
- const newName = requireNonEmptyString(args.newName || path.split('/').pop(), 'newName', 'Missing required parameter: newName');
307
+ const defaultNewName = path.split('/').pop() || '';
308
+ const newName = requireNonEmptyString(args.newName || defaultNewName, 'newName', 'Missing required parameter: newName');
286
309
  const baseDir = destDir.replace(/\/$/, '');
287
310
  const destPath = `${baseDir}/${newName}`;
288
311
  const res = await tools.sequenceTools.duplicate({ path, destinationPath: destPath });
@@ -291,15 +314,15 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
291
314
  case 'rename': {
292
315
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
293
316
  const newName = requireNonEmptyString(args.newName, 'newName', 'Missing required parameter: newName');
294
- const res = await tools.sequenceTools.rename({ path, newName });
295
- const errorCode = String((res && (res as any).error) || '').toUpperCase();
296
- const msgLower = String((res && (res as any).message) || '').toLowerCase();
297
- if (res && (res as any).success === false && (errorCode === 'OPERATION_FAILED' || msgLower.includes('failed to rename sequence'))) {
317
+ const res = await tools.sequenceTools.rename({ path, newName }) as SequenceActionResponse;
318
+ const errorCode = getErrorString(res).toUpperCase();
319
+ const msgLower = getMessageString(res).toLowerCase();
320
+ if (res && res.success === false && (errorCode === 'OPERATION_FAILED' || msgLower.includes('failed to rename sequence'))) {
298
321
  // Return actual failure, not best-effort success - rename is a destructive operation
299
322
  return cleanObject({
300
323
  success: false,
301
324
  error: 'OPERATION_FAILED',
302
- message: (res as any).message || 'Failed to rename sequence',
325
+ message: res.message || 'Failed to rename sequence',
303
326
  action: 'rename',
304
327
  path,
305
328
  newName
@@ -309,20 +332,20 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
309
332
  }
310
333
  case 'delete': {
311
334
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
312
- const res = await tools.sequenceTools.deleteSequence({ path });
335
+ const res = await tools.sequenceTools.deleteSequence({ path }) as SequenceActionResponse;
313
336
 
314
- if (res && (res as any).success !== false) {
337
+ if (res && res.success !== false) {
315
338
  markSequenceDeleted(path);
316
339
  }
317
340
  return cleanObject(res);
318
341
  }
319
342
  case 'get_metadata': {
320
- const res = await tools.sequenceTools.getMetadata({ path: args.path });
343
+ const res = await tools.sequenceTools.getMetadata({ path: args.path as string });
321
344
  return cleanObject(res);
322
345
  }
323
346
  case 'set_metadata': {
324
347
  const path = requireNonEmptyString(args.path, 'path', 'Missing required parameter: path');
325
- const metadata = (args.metadata && typeof args.metadata === 'object') ? args.metadata : {};
348
+ const metadata = (args.metadata && typeof args.metadata === 'object') ? args.metadata as Record<string, unknown> : {};
326
349
  const res = await executeAutomationRequest(tools, 'set_metadata', { assetPath: path, metadata });
327
350
  return cleanObject(res);
328
351
  }
@@ -335,10 +358,10 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
335
358
 
336
359
  // Fix: Check if actor is bound before adding track
337
360
  if (actorName) {
338
- const bindingsRes = await tools.sequenceTools.getBindings({ path });
361
+ const bindingsRes = await tools.sequenceTools.getBindings({ path }) as SequenceActionResponse;
339
362
  if (bindingsRes && bindingsRes.success) {
340
363
  const bindings = bindingsRes.bindings || [];
341
- const isBound = bindings.some((b: any) => b.name === actorName);
364
+ const isBound = bindings.some((b) => b.name === actorName);
342
365
  if (!isBound) {
343
366
  return cleanObject({
344
367
  success: false,
@@ -399,7 +422,7 @@ export async function handleSequenceTools(action: string, args: any, tools: IToo
399
422
  if (!Number.isFinite(end)) throw new Error('Invalid end: must be a number');
400
423
 
401
424
  const res = await tools.sequenceTools.setWorkRange({
402
- path: args.path,
425
+ path: args.path as string | undefined,
403
426
  start,
404
427
  end
405
428
  });
@@ -35,7 +35,7 @@ export interface PropertyInfo {
35
35
  type: string;
36
36
  value?: any;
37
37
  flags?: string[];
38
- metadata?: Record<string, any>;
38
+ metadata?: Record<string, unknown>;
39
39
  category?: string;
40
40
  tooltip?: string;
41
41
  description?: string;
@@ -141,7 +141,7 @@ export class IntrospectionTools {
141
141
  return value;
142
142
  }
143
143
 
144
- private isPlainObject(value: any): value is Record<string, any> {
144
+ private isPlainObject(value: any): value is Record<string, unknown> {
145
145
  return !!value && typeof value === 'object' && !Array.isArray(value);
146
146
  }
147
147
 
@@ -200,7 +200,7 @@ export class IntrospectionTools {
200
200
  const rawValue = entry.value ?? entry.currentValue ?? entry.defaultValue ?? entry.data ?? entry;
201
201
  const value = this.convertPropertyValue(rawValue, type);
202
202
  const flags: string[] | undefined = entry.flags ?? entry.attributes;
203
- const metadata: Record<string, any> | undefined = entry.metadata ?? entry.annotations;
203
+ const metadata: Record<string, unknown> | undefined = entry.metadata ?? entry.annotations;
204
204
  const filtered = this.shouldFilterProperty(name, value, flags, detailed);
205
205
  const dictionaryEntry = lookupPropertyMetadata(name);
206
206
  const propertyInfo: PropertyInfo = {
@@ -220,12 +220,12 @@ export class IntrospectionTools {
220
220
  return propertyInfo;
221
221
  }
222
222
 
223
- private flattenPropertyMap(source: Record<string, any>, prefix = '', detailed = false): PropertyInfo[] {
223
+ private flattenPropertyMap(source: Record<string, unknown>, prefix = '', detailed = false): PropertyInfo[] {
224
224
  const properties: PropertyInfo[] = [];
225
225
  for (const [rawKey, rawValue] of Object.entries(source)) {
226
226
  const name = prefix ? `${prefix}.${rawKey}` : rawKey;
227
227
  if (this.isLikelyPropertyDescriptor(rawValue)) {
228
- const normalized = this.normalizePropertyEntry({ ...rawValue, name }, detailed);
228
+ const normalized = this.normalizePropertyEntry({ ...(rawValue as Record<string, unknown>), name }, detailed);
229
229
  if (normalized) properties.push(normalized);
230
230
  continue;
231
231
  }
@@ -441,8 +441,8 @@ export class IntrospectionTools {
441
441
  }
442
442
 
443
443
  return res;
444
- } catch (err: any) {
445
- const errorMsg = err?.message || String(err);
444
+ } catch (err: unknown) {
445
+ const errorMsg = (err instanceof Error ? err.message : undefined) || String(err);
446
446
  if (errorMsg.includes('404')) {
447
447
  return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
448
448
  }
@@ -2,8 +2,10 @@
2
2
  import { UnrealBridge } from '../unreal-bridge.js';
3
3
  import { AutomationBridge } from '../automation/index.js';
4
4
  import { ensureVector3 } from '../utils/validation.js';
5
+ import { wasmIntegration } from '../wasm/index.js';
6
+ import { ILandscapeTools, StandardActionResponse } from '../types/tool-interfaces.js';
5
7
 
6
- export class LandscapeTools {
8
+ export class LandscapeTools implements ILandscapeTools {
7
9
  constructor(private bridge: UnrealBridge, private automationBridge?: AutomationBridge) { }
8
10
 
9
11
  setAutomationBridge(automationBridge?: AutomationBridge) { this.automationBridge = automationBridge; }
@@ -23,7 +25,7 @@ export class LandscapeTools {
23
25
  runtimeGrid?: string;
24
26
  isSpatiallyLoaded?: boolean;
25
27
  dataLayers?: string[];
26
- }) {
28
+ }): Promise<StandardActionResponse> {
27
29
  const name = params.name?.trim();
28
30
  if (!name) {
29
31
  return { success: false, error: 'Landscape name is required' };
@@ -46,6 +48,10 @@ export class LandscapeTools {
46
48
  }
47
49
 
48
50
  const [locX, locY, locZ] = ensureVector3(params.location ?? [0, 0, 0], 'landscape location');
51
+ // Use WASM vectorAdd for landscape location processing
52
+ const zeroVector: [number, number, number] = [0, 0, 0];
53
+ const processedLocation = wasmIntegration.vectorAdd(zeroVector, [locX, locY, locZ]);
54
+ console.error('[WASM] Using vectorAdd for landscape positioning');
49
55
  const sectionsPerComponent = Math.max(1, Math.floor(params.sectionsPerComponent ?? 1));
50
56
  const quadsPerSection = Math.max(1, Math.floor(params.quadsPerSection ?? 63));
51
57
 
@@ -57,9 +63,9 @@ export class LandscapeTools {
57
63
 
58
64
  const payload: Record<string, unknown> = {
59
65
  name,
60
- x: locX,
61
- y: locY,
62
- z: locZ,
66
+ x: processedLocation[0],
67
+ y: processedLocation[1],
68
+ z: processedLocation[2],
63
69
  componentsX,
64
70
  componentsY,
65
71
  quadsPerComponent,
@@ -101,7 +107,7 @@ export class LandscapeTools {
101
107
  result.spatiallyLoaded = params.isSpatiallyLoaded;
102
108
  }
103
109
 
104
- return result;
110
+ return result as StandardActionResponse;
105
111
  } catch (error) {
106
112
  return {
107
113
  success: false,
@@ -120,7 +126,7 @@ export class LandscapeTools {
120
126
  strength?: number;
121
127
  location?: [number, number, number];
122
128
  radius?: number;
123
- }) {
129
+ }): Promise<StandardActionResponse> {
124
130
  const [x, y, z] = ensureVector3(params.location ?? [0, 0, 0], 'sculpt location');
125
131
 
126
132
  const tool = (params.tool || '').trim();
@@ -161,7 +167,7 @@ export class LandscapeTools {
161
167
  success: true,
162
168
  message: `Sculpting applied to ${params.landscapeName}`,
163
169
  details: response
164
- };
170
+ } as StandardActionResponse;
165
171
  }
166
172
 
167
173
  // Paint landscape
@@ -174,7 +180,7 @@ export class LandscapeTools {
174
180
  targetValue?: number;
175
181
  radius?: number;
176
182
  density?: number;
177
- }) {
183
+ }): Promise<StandardActionResponse> {
178
184
  if (!this.automationBridge) {
179
185
  throw new Error('Automation Bridge not available.');
180
186
  }
@@ -208,7 +214,7 @@ export class LandscapeTools {
208
214
  success: true,
209
215
  message: `Painted layer ${params.layerName}`,
210
216
  details: response
211
- };
217
+ } as StandardActionResponse;
212
218
  }
213
219
 
214
220
  // Create procedural terrain using ProceduralMeshComponent
@@ -221,7 +227,7 @@ export class LandscapeTools {
221
227
  heightFunction?: string; // Expression for height calculation
222
228
  material?: string;
223
229
  settings?: Record<string, unknown>;
224
- }) {
230
+ }): Promise<StandardActionResponse> {
225
231
  if (!this.automationBridge) {
226
232
  throw new Error('Automation Bridge not available. Procedural terrain creation requires plugin support.');
227
233
  }
@@ -261,7 +267,7 @@ export class LandscapeTools {
261
267
  size: result?.size,
262
268
  subdivisions: result?.subdivisions,
263
269
  details: result
264
- };
270
+ } as StandardActionResponse;
265
271
  } catch (error) {
266
272
  return {
267
273
  success: false,
@@ -279,7 +285,7 @@ export class LandscapeTools {
279
285
  maxScale?: number;
280
286
  path?: string; // Legacy support
281
287
  staticMesh?: string; // Legacy support
282
- }): Promise<any> {
288
+ }): Promise<StandardActionResponse> {
283
289
  if (!this.automationBridge) {
284
290
  throw new Error('Automation Bridge not available. Landscape operations require plugin support.');
285
291
  }
@@ -323,7 +329,7 @@ export class LandscapeTools {
323
329
  success: true,
324
330
  message: response?.message || `Landscape grass type '${name}' created`,
325
331
  assetPath: result?.asset_path || response?.assetPath || response?.asset_path
326
- };
332
+ } as StandardActionResponse;
327
333
  } catch (error) {
328
334
  return {
329
335
  success: false,
@@ -333,7 +339,7 @@ export class LandscapeTools {
333
339
  }
334
340
 
335
341
  // Set the material used by an existing landscape actor
336
- async setLandscapeMaterial(params: { landscapeName: string; materialPath: string }): Promise<any> {
342
+ async setLandscapeMaterial(params: { landscapeName: string; materialPath: string }): Promise<StandardActionResponse> {
337
343
  const landscapeName = typeof params.landscapeName === 'string' ? params.landscapeName.trim() : '';
338
344
  const materialPath = typeof params.materialPath === 'string' ? params.materialPath.trim() : '';
339
345
 
@@ -366,7 +372,7 @@ export class LandscapeTools {
366
372
  message: response?.message || `Landscape material set on '${landscapeName}'`,
367
373
  landscapeName: response?.landscapeName || landscapeName,
368
374
  materialPath: response?.materialPath || materialPath
369
- };
375
+ } as StandardActionResponse;
370
376
  } catch (error) {
371
377
  return {
372
378
  success: false,
@@ -383,7 +389,7 @@ export class LandscapeTools {
383
389
  minScale?: number;
384
390
  maxScale?: number;
385
391
  randomRotation?: boolean;
386
- }) {
392
+ }): Promise<StandardActionResponse> {
387
393
  const commands: string[] = [];
388
394
 
389
395
  commands.push(`CreateLandscapeGrass ${params.landscapeName} ${params.grassType}`);
@@ -410,7 +416,7 @@ export class LandscapeTools {
410
416
  landscapeName: string;
411
417
  collisionMipLevel?: number;
412
418
  simpleCollision?: boolean;
413
- }) {
419
+ }): Promise<StandardActionResponse> {
414
420
  const commands: string[] = [];
415
421
 
416
422
  if (params.collisionMipLevel !== undefined) {
@@ -433,7 +439,7 @@ export class LandscapeTools {
433
439
  landscapeName: string;
434
440
  targetTriangleCount?: number;
435
441
  preserveDetails?: boolean;
436
- }) {
442
+ }): Promise<StandardActionResponse> {
437
443
  const commands: string[] = [];
438
444
 
439
445
  if (params.targetTriangleCount !== undefined) {
@@ -458,7 +464,7 @@ export class LandscapeTools {
458
464
  location?: [number, number, number];
459
465
  size?: [number, number];
460
466
  depth?: number;
461
- }) {
467
+ }): Promise<StandardActionResponse> {
462
468
  const loc = params.location || [0, 0, 0];
463
469
  const size = params.size || [1000, 1000];
464
470
  const depth = params.depth || 100;
@@ -475,7 +481,7 @@ export class LandscapeTools {
475
481
  runtimeGrid?: string;
476
482
  dataLayers?: string[];
477
483
  streamingDistance?: number;
478
- }) {
484
+ }): Promise<StandardActionResponse> {
479
485
  if (!this.automationBridge) {
480
486
  throw new Error('Automation Bridge not available. World Partition operations require plugin support.');
481
487
  }
@@ -502,7 +508,7 @@ export class LandscapeTools {
502
508
  success: true,
503
509
  message: response.message || 'World Partition configured',
504
510
  changes: response.changes
505
- };
511
+ } as StandardActionResponse;
506
512
  } catch (err) {
507
513
  return { success: false, error: `Failed to configure World Partition: ${err instanceof Error ? err.message : String(err)}` };
508
514
  }
@@ -513,7 +519,7 @@ export class LandscapeTools {
513
519
  landscapeName: string;
514
520
  dataLayerNames: string[];
515
521
  operation: 'add' | 'remove' | 'set';
516
- }) {
522
+ }): Promise<StandardActionResponse> {
517
523
  try {
518
524
  const commands = [];
519
525
 
@@ -535,7 +541,7 @@ export class LandscapeTools {
535
541
  success: true,
536
542
  message: `Data layers ${params.operation === 'add' ? 'added' : params.operation === 'remove' ? 'removed' : 'set'} for landscape`,
537
543
  layers: params.dataLayerNames
538
- };
544
+ } as StandardActionResponse;
539
545
  } catch (err) {
540
546
  return { success: false, error: `Failed to manage data layers: ${err}` };
541
547
  }
@@ -547,7 +553,7 @@ export class LandscapeTools {
547
553
  cellSize?: number;
548
554
  loadingRange?: number;
549
555
  enableHLOD?: boolean;
550
- }) {
556
+ }): Promise<StandardActionResponse> {
551
557
  const commands = [];
552
558
 
553
559
  // World Partition runtime commands
@@ -573,7 +579,7 @@ export class LandscapeTools {
573
579
  loadingRange: params.loadingRange,
574
580
  hlod: params.enableHLOD
575
581
  }
576
- };
582
+ } as StandardActionResponse;
577
583
  } catch (err) {
578
584
  return { success: false, error: `Failed to configure streaming cells: ${err}` };
579
585
  }
@@ -588,7 +594,7 @@ export class LandscapeTools {
588
594
  maxX: number;
589
595
  maxY: number;
590
596
  updateNormals?: boolean;
591
- }) {
597
+ }): Promise<StandardActionResponse> {
592
598
  if (!this.automationBridge) {
593
599
  throw new Error('Automation Bridge not available. Landscape operations require plugin support.');
594
600
  }
@@ -634,7 +640,7 @@ export class LandscapeTools {
634
640
  return {
635
641
  success: true,
636
642
  message: response.message || 'Heightmap modified successfully'
637
- };
643
+ } as StandardActionResponse;
638
644
  } catch (err) {
639
645
  return { success: false, error: `Failed to modify heightmap: ${err instanceof Error ? err.message : String(err)}` };
640
646
  }