unreal-engine-mcp-server 0.4.0 → 0.4.4

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 (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +3 -2
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -4,6 +4,129 @@ import { Logger } from '../utils/logger.js';
4
4
 
5
5
  const log = new Logger('ConsolidatedToolHandler');
6
6
 
7
+ const ACTION_REQUIRED_ERROR = 'Missing required parameter: action';
8
+
9
+ function ensureArgsPresent(args: any) {
10
+ if (args === null || args === undefined) {
11
+ throw new Error('Invalid arguments: null or undefined');
12
+ }
13
+ }
14
+
15
+ function requireAction(args: any): string {
16
+ ensureArgsPresent(args);
17
+ const action = args.action;
18
+ if (typeof action !== 'string' || action.trim() === '') {
19
+ throw new Error(ACTION_REQUIRED_ERROR);
20
+ }
21
+ return action;
22
+ }
23
+
24
+ function requireNonEmptyString(value: any, field: string, message?: string): string {
25
+ if (typeof value !== 'string' || value.trim() === '') {
26
+ throw new Error(message ?? `Invalid ${field}: must be a non-empty string`);
27
+ }
28
+ return value;
29
+ }
30
+
31
+ function requirePositiveNumber(value: any, field: string, message?: string): number {
32
+ if (typeof value !== 'number' || !isFinite(value) || value <= 0) {
33
+ throw new Error(message ?? `Invalid ${field}: must be a positive number`);
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function requireVector3Components(
39
+ vector: any,
40
+ message: string
41
+ ): [number, number, number] {
42
+ if (
43
+ !vector ||
44
+ typeof vector.x !== 'number' ||
45
+ typeof vector.y !== 'number' ||
46
+ typeof vector.z !== 'number'
47
+ ) {
48
+ throw new Error(message);
49
+ }
50
+ return [vector.x, vector.y, vector.z];
51
+ }
52
+
53
+ function getElicitationTimeoutMs(tools: any): number | undefined {
54
+ if (!tools) return undefined;
55
+ const direct = tools.elicitationTimeoutMs;
56
+ if (typeof direct === 'number' && Number.isFinite(direct)) {
57
+ return direct;
58
+ }
59
+ if (typeof tools.getElicitationTimeoutMs === 'function') {
60
+ const value = tools.getElicitationTimeoutMs();
61
+ if (typeof value === 'number' && Number.isFinite(value)) {
62
+ return value;
63
+ }
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ async function elicitMissingPrimitiveArgs(
69
+ tools: any,
70
+ args: any,
71
+ prompt: string,
72
+ fieldSchemas: Record<string, { type: 'string' | 'number' | 'integer' | 'boolean'; title?: string; description?: string; enum?: string[]; enumNames?: string[]; minimum?: number; maximum?: number; minLength?: number; maxLength?: number; pattern?: string; format?: string; default?: unknown }>
73
+ ) {
74
+ if (
75
+ !tools ||
76
+ typeof tools.supportsElicitation !== 'function' ||
77
+ !tools.supportsElicitation() ||
78
+ typeof tools.elicit !== 'function'
79
+ ) {
80
+ return;
81
+ }
82
+
83
+ const properties: Record<string, any> = {};
84
+ const required: string[] = [];
85
+
86
+ for (const [key, schema] of Object.entries(fieldSchemas)) {
87
+ const value = args?.[key];
88
+ const missing =
89
+ value === undefined ||
90
+ value === null ||
91
+ (typeof value === 'string' && value.trim() === '');
92
+ if (missing) {
93
+ properties[key] = schema;
94
+ required.push(key);
95
+ }
96
+ }
97
+
98
+ if (required.length === 0) return;
99
+
100
+ const timeoutMs = getElicitationTimeoutMs(tools);
101
+ const options: any = {
102
+ fallback: async () => ({ ok: false, error: 'missing-params' })
103
+ };
104
+ if (typeof timeoutMs === 'number') {
105
+ options.timeoutMs = timeoutMs;
106
+ }
107
+
108
+ try {
109
+ const elicited = await tools.elicit(
110
+ prompt,
111
+ { type: 'object', properties, required },
112
+ options
113
+ );
114
+
115
+ if (elicited?.ok && elicited.value) {
116
+ for (const key of required) {
117
+ const value = elicited.value[key];
118
+ if (value === undefined || value === null) continue;
119
+ args[key] = typeof value === 'string' ? value.trim() : value;
120
+ }
121
+ }
122
+ } catch (err) {
123
+ log.debug('Special elicitation fallback skipped', {
124
+ prompt,
125
+ err: (err as any)?.message || String(err)
126
+ });
127
+ }
128
+ }
129
+
7
130
  export async function handleConsolidatedToolCall(
8
131
  name: string,
9
132
  args: any,
@@ -14,26 +137,12 @@ export async function handleConsolidatedToolCall(
14
137
  log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
15
138
 
16
139
  try {
17
- // Validate args is not null/undefined
18
- if (args === null || args === undefined) {
19
- throw new Error('Invalid arguments: null or undefined');
20
- }
21
-
140
+ ensureArgsPresent(args);
22
141
 
23
142
  switch (name) {
24
143
  // 1. ASSET MANAGER
25
144
  case 'manage_asset':
26
- // Validate args is not null/undefined
27
- if (args === null || args === undefined) {
28
- throw new Error('Invalid arguments: null or undefined');
29
- }
30
-
31
- // Validate action exists
32
- if (!args.action) {
33
- throw new Error('Missing required parameter: action');
34
- }
35
-
36
- switch (args.action) {
145
+ switch (requireAction(args)) {
37
146
  case 'list': {
38
147
  if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') {
39
148
  throw new Error('Invalid directory: must be a string');
@@ -42,20 +151,81 @@ switch (args.action) {
42
151
  return cleanObject({ success: true, ...res });
43
152
  }
44
153
  case 'import': {
45
- if (typeof args.sourcePath !== 'string' || args.sourcePath.trim() === '') {
46
- throw new Error('Invalid sourcePath');
47
- }
48
- if (typeof args.destinationPath !== 'string' || args.destinationPath.trim() === '') {
49
- throw new Error('Invalid destinationPath');
154
+ let sourcePath = typeof args.sourcePath === 'string' ? args.sourcePath.trim() : '';
155
+ let destinationPath = typeof args.destinationPath === 'string' ? args.destinationPath.trim() : '';
156
+
157
+ if ((!sourcePath || !destinationPath) && typeof tools.supportsElicitation === 'function' && tools.supportsElicitation() && typeof tools.elicit === 'function') {
158
+ const schemaProps: Record<string, any> = {};
159
+ const required: string[] = [];
160
+
161
+ if (!sourcePath) {
162
+ schemaProps.sourcePath = {
163
+ type: 'string',
164
+ title: 'Source File Path',
165
+ description: 'Full path to the asset file on disk to import'
166
+ };
167
+ required.push('sourcePath');
168
+ }
169
+
170
+ if (!destinationPath) {
171
+ schemaProps.destinationPath = {
172
+ type: 'string',
173
+ title: 'Destination Path',
174
+ description: 'Unreal content path where the asset should be imported (e.g., /Game/MCP/Assets)'
175
+ };
176
+ required.push('destinationPath');
177
+ }
178
+
179
+ if (required.length > 0) {
180
+ const timeoutMs = getElicitationTimeoutMs(tools);
181
+ const options: any = { fallback: async () => ({ ok: false, error: 'missing-import-params' }) };
182
+ if (typeof timeoutMs === 'number') {
183
+ options.timeoutMs = timeoutMs;
184
+ }
185
+ const elicited = await tools.elicit(
186
+ 'Provide the missing import parameters for manage_asset.import',
187
+ { type: 'object', properties: schemaProps, required },
188
+ options
189
+ );
190
+
191
+ if (elicited?.ok && elicited.value) {
192
+ if (typeof elicited.value.sourcePath === 'string') {
193
+ sourcePath = elicited.value.sourcePath.trim();
194
+ }
195
+ if (typeof elicited.value.destinationPath === 'string') {
196
+ destinationPath = elicited.value.destinationPath.trim();
197
+ }
198
+ }
199
+ }
50
200
  }
51
- const res = await tools.assetTools.importAsset(args.sourcePath, args.destinationPath);
201
+
202
+ const sourcePathValidated = requireNonEmptyString(sourcePath || args.sourcePath, 'sourcePath', 'Invalid sourcePath');
203
+ const destinationPathValidated = requireNonEmptyString(destinationPath || args.destinationPath, 'destinationPath', 'Invalid destinationPath');
204
+ const res = await tools.assetTools.importAsset(sourcePathValidated, destinationPathValidated);
52
205
  return cleanObject(res);
53
206
  }
54
207
  case 'create_material': {
55
- if (typeof args.name !== 'string' || args.name.trim() === '') {
56
- throw new Error('Invalid name: must be a non-empty string');
57
- }
58
- const res = await tools.materialTools.createMaterial(args.name, args.path || '/Game/Materials');
208
+ await elicitMissingPrimitiveArgs(
209
+ tools,
210
+ args,
211
+ 'Provide the material details for manage_asset.create_material',
212
+ {
213
+ name: {
214
+ type: 'string',
215
+ title: 'Material Name',
216
+ description: 'Name for the new material asset'
217
+ },
218
+ path: {
219
+ type: 'string',
220
+ title: 'Save Path',
221
+ description: 'Optional Unreal content path where the material should be saved'
222
+ }
223
+ }
224
+ );
225
+ const sanitizedName = typeof args.name === 'string' ? args.name.trim() : args.name;
226
+ const sanitizedPath = typeof args.path === 'string' ? args.path.trim() : args.path;
227
+ const name = requireNonEmptyString(sanitizedName, 'name', 'Invalid name: must be a non-empty string');
228
+ const res = await tools.materialTools.createMaterial(name, sanitizedPath || '/Game/Materials');
59
229
  return cleanObject(res);
60
230
  }
61
231
  default:
@@ -64,41 +234,72 @@ switch (args.action) {
64
234
 
65
235
  // 2. ACTOR CONTROL
66
236
  case 'control_actor':
67
- // Validate action exists
68
- if (!args.action) {
69
- throw new Error('Missing required parameter: action');
70
- }
71
-
72
- switch (args.action) {
237
+ switch (requireAction(args)) {
73
238
  case 'spawn': {
74
- if (!args.classPath || typeof args.classPath !== 'string' || args.classPath.trim() === '') {
75
- throw new Error('Invalid classPath: must be a non-empty string');
76
- }
239
+ await elicitMissingPrimitiveArgs(
240
+ tools,
241
+ args,
242
+ 'Provide the spawn parameters for control_actor.spawn',
243
+ {
244
+ classPath: {
245
+ type: 'string',
246
+ title: 'Actor Class or Asset Path',
247
+ description: 'Class name (e.g., StaticMeshActor) or asset path (e.g., /Engine/BasicShapes/Cube) to spawn'
248
+ }
249
+ }
250
+ );
251
+ const classPathInput = typeof args.classPath === 'string' ? args.classPath.trim() : args.classPath;
252
+ const classPath = requireNonEmptyString(classPathInput, 'classPath', 'Invalid classPath: must be a non-empty string');
253
+ const actorNameInput = typeof args.actorName === 'string' && args.actorName.trim() !== ''
254
+ ? args.actorName
255
+ : (typeof args.name === 'string' ? args.name : undefined);
77
256
  const res = await tools.actorTools.spawn({
78
- classPath: args.classPath,
257
+ classPath,
79
258
  location: args.location,
80
- rotation: args.rotation
259
+ rotation: args.rotation,
260
+ actorName: actorNameInput
81
261
  });
82
262
  return cleanObject(res);
83
263
  }
84
264
  case 'delete': {
85
- if (!args.actorName || typeof args.actorName !== 'string' || args.actorName.trim() === '') {
86
- throw new Error('Invalid actorName');
87
- }
88
- const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: args.actorName });
265
+ await elicitMissingPrimitiveArgs(
266
+ tools,
267
+ args,
268
+ 'Which actor should control_actor.delete remove?',
269
+ {
270
+ actorName: {
271
+ type: 'string',
272
+ title: 'Actor Name',
273
+ description: 'Exact label of the actor to delete'
274
+ }
275
+ }
276
+ );
277
+ const actorNameArg = typeof args.actorName === 'string' && args.actorName.trim() !== ''
278
+ ? args.actorName
279
+ : (typeof args.name === 'string' ? args.name : undefined);
280
+ const actorName = requireNonEmptyString(actorNameArg, 'actorName', 'Invalid actorName');
281
+ const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: actorName });
89
282
  return cleanObject(res);
90
283
  }
91
284
  case 'apply_force': {
92
- if (!args.actorName || typeof args.actorName !== 'string' || args.actorName.trim() === '') {
93
- throw new Error('Invalid actorName');
94
- }
95
- if (!args.force || typeof args.force.x !== 'number' || typeof args.force.y !== 'number' || typeof args.force.z !== 'number') {
96
- throw new Error('Invalid force: must have numeric x,y,z');
97
- }
285
+ await elicitMissingPrimitiveArgs(
286
+ tools,
287
+ args,
288
+ 'Provide the target actor for control_actor.apply_force',
289
+ {
290
+ actorName: {
291
+ type: 'string',
292
+ title: 'Actor Name',
293
+ description: 'Physics-enabled actor that should receive the force'
294
+ }
295
+ }
296
+ );
297
+ const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
298
+ const vector = requireVector3Components(args.force, 'Invalid force: must have numeric x,y,z');
98
299
  const res = await tools.physicsTools.applyForce({
99
- actorName: args.actorName,
300
+ actorName,
100
301
  forceType: 'Force',
101
- vector: [args.force.x, args.force.y, args.force.z]
302
+ vector
102
303
  });
103
304
  return cleanObject(res);
104
305
  }
@@ -108,12 +309,7 @@ switch (args.action) {
108
309
 
109
310
  // 3. EDITOR CONTROL
110
311
  case 'control_editor':
111
- // Validate action exists
112
- if (!args.action) {
113
- throw new Error('Missing required parameter: action');
114
- }
115
-
116
- switch (args.action) {
312
+ switch (requireAction(args)) {
117
313
  case 'play': {
118
314
  const res = await tools.editorTools.playInEditor();
119
315
  return cleanObject(res);
@@ -127,11 +323,9 @@ switch (args.action) {
127
323
  return cleanObject(res);
128
324
  }
129
325
  case 'set_game_speed': {
130
- if (typeof args.speed !== 'number' || !isFinite(args.speed) || args.speed <= 0) {
131
- throw new Error('Invalid speed: must be a positive number');
132
- }
326
+ const speed = requirePositiveNumber(args.speed, 'speed', 'Invalid speed: must be a positive number');
133
327
  // Use console command via bridge
134
- const res = await tools.bridge.executeConsoleCommand(`slomo ${args.speed}`);
328
+ const res = await tools.bridge.executeConsoleCommand(`slomo ${speed}`);
135
329
  return cleanObject(res);
136
330
  }
137
331
  case 'eject': {
@@ -147,8 +341,20 @@ switch (args.action) {
147
341
  return cleanObject(res);
148
342
  }
149
343
  case 'set_view_mode': {
150
- if (!args.viewMode || typeof args.viewMode !== 'string') throw new Error('Missing required parameter: viewMode');
151
- const res = await tools.bridge.setSafeViewMode(args.viewMode);
344
+ await elicitMissingPrimitiveArgs(
345
+ tools,
346
+ args,
347
+ 'Provide the view mode for control_editor.set_view_mode',
348
+ {
349
+ viewMode: {
350
+ type: 'string',
351
+ title: 'View Mode',
352
+ description: 'Viewport view mode (e.g., Lit, Unlit, Wireframe)'
353
+ }
354
+ }
355
+ );
356
+ const viewMode = requireNonEmptyString(args.viewMode, 'viewMode', 'Missing required parameter: viewMode');
357
+ const res = await tools.bridge.setSafeViewMode(viewMode);
152
358
  return cleanObject(res);
153
359
  }
154
360
  default:
@@ -157,11 +363,22 @@ switch (args.action) {
157
363
 
158
364
  // 4. LEVEL MANAGER
159
365
  case 'manage_level':
160
- if (!args.action) throw new Error('Missing required parameter: action');
161
- switch (args.action) {
366
+ switch (requireAction(args)) {
162
367
  case 'load': {
163
- if (!args.levelPath || typeof args.levelPath !== 'string') throw new Error('Missing required parameter: levelPath');
164
- const res = await tools.levelTools.loadLevel({ levelPath: args.levelPath, streaming: !!args.streaming });
368
+ await elicitMissingPrimitiveArgs(
369
+ tools,
370
+ args,
371
+ 'Select the level to load for manage_level.load',
372
+ {
373
+ levelPath: {
374
+ type: 'string',
375
+ title: 'Level Path',
376
+ description: 'Content path of the level asset to load (e.g., /Game/Maps/MyLevel)'
377
+ }
378
+ }
379
+ );
380
+ const levelPath = requireNonEmptyString(args.levelPath, 'levelPath', 'Missing required parameter: levelPath');
381
+ const res = await tools.levelTools.loadLevel({ levelPath, streaming: !!args.streaming });
165
382
  return cleanObject(res);
166
383
  }
167
384
  case 'save': {
@@ -169,19 +386,136 @@ case 'manage_level':
169
386
  return cleanObject(res);
170
387
  }
171
388
  case 'stream': {
172
- if (!args.levelName || typeof args.levelName !== 'string') throw new Error('Missing required parameter: levelName');
173
- const res = await tools.levelTools.streamLevel({ levelName: args.levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible });
389
+ await elicitMissingPrimitiveArgs(
390
+ tools,
391
+ args,
392
+ 'Provide the streaming level name for manage_level.stream',
393
+ {
394
+ levelName: {
395
+ type: 'string',
396
+ title: 'Level Name',
397
+ description: 'Streaming level name to toggle'
398
+ }
399
+ }
400
+ );
401
+ const levelName = requireNonEmptyString(args.levelName, 'levelName', 'Missing required parameter: levelName');
402
+ const res = await tools.levelTools.streamLevel({ levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible });
174
403
  return cleanObject(res);
175
404
  }
176
405
  case 'create_light': {
177
- if (!args.lightType) throw new Error('Missing required parameter: lightType');
178
- if (!args.name || typeof args.name !== 'string' || args.name.trim() === '') throw new Error('Invalid name');
179
- const t = String(args.lightType).toLowerCase();
180
- if (t === 'directional') return cleanObject(await tools.lightingTools.createDirectionalLight({ name: args.name, intensity: args.intensity }));
181
- if (t === 'point') return cleanObject(await tools.lightingTools.createPointLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], intensity: args.intensity }));
182
- if (t === 'spot') return cleanObject(await tools.lightingTools.createSpotLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], rotation: [0,0,0], intensity: args.intensity }));
183
- if (t === 'rect') return cleanObject(await tools.lightingTools.createRectLight({ name: args.name, location: args.location ? [args.location.x, args.location.y, args.location.z] : [0,0,0], rotation: [0,0,0], intensity: args.intensity }));
184
- throw new Error(`Unknown light type: ${args.lightType}`);
406
+ await elicitMissingPrimitiveArgs(
407
+ tools,
408
+ args,
409
+ 'Provide the light details for manage_level.create_light',
410
+ {
411
+ lightType: {
412
+ type: 'string',
413
+ title: 'Light Type',
414
+ description: 'Directional, Point, Spot, Rect, or Sky'
415
+ },
416
+ name: {
417
+ type: 'string',
418
+ title: 'Light Name',
419
+ description: 'Name for the new light actor'
420
+ }
421
+ }
422
+ );
423
+ const lightType = requireNonEmptyString(args.lightType, 'lightType', 'Missing required parameter: lightType');
424
+ const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
425
+ const typeKey = lightType.toLowerCase();
426
+ const toVector = (value: any, fallback: [number, number, number]): [number, number, number] => {
427
+ if (Array.isArray(value) && value.length === 3) {
428
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
429
+ }
430
+ if (value && typeof value === 'object') {
431
+ return [Number(value.x) || 0, Number(value.y) || 0, Number(value.z) || 0];
432
+ }
433
+ return fallback;
434
+ };
435
+ const toRotator = (value: any, fallback: [number, number, number]): [number, number, number] => {
436
+ if (Array.isArray(value) && value.length === 3) {
437
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
438
+ }
439
+ if (value && typeof value === 'object') {
440
+ return [Number(value.pitch) || 0, Number(value.yaw) || 0, Number(value.roll) || 0];
441
+ }
442
+ return fallback;
443
+ };
444
+ const toColor = (value: any): [number, number, number] | undefined => {
445
+ if (Array.isArray(value) && value.length === 3) {
446
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
447
+ }
448
+ if (value && typeof value === 'object') {
449
+ return [Number(value.r) || 0, Number(value.g) || 0, Number(value.b) || 0];
450
+ }
451
+ return undefined;
452
+ };
453
+
454
+ const location = toVector(args.location, [0, 0, typeKey === 'directional' ? 500 : 0]);
455
+ const rotation = toRotator(args.rotation, [0, 0, 0]);
456
+ const color = toColor(args.color);
457
+ const castShadows = typeof args.castShadows === 'boolean' ? args.castShadows : undefined;
458
+
459
+ if (typeKey === 'directional') {
460
+ return cleanObject(await tools.lightingTools.createDirectionalLight({
461
+ name,
462
+ intensity: args.intensity,
463
+ color,
464
+ rotation,
465
+ castShadows,
466
+ temperature: args.temperature
467
+ }));
468
+ }
469
+ if (typeKey === 'point') {
470
+ return cleanObject(await tools.lightingTools.createPointLight({
471
+ name,
472
+ location,
473
+ intensity: args.intensity,
474
+ radius: args.radius,
475
+ color,
476
+ falloffExponent: args.falloffExponent,
477
+ castShadows
478
+ }));
479
+ }
480
+ if (typeKey === 'spot') {
481
+ const innerCone = typeof args.innerCone === 'number' ? args.innerCone : undefined;
482
+ const outerCone = typeof args.outerCone === 'number' ? args.outerCone : undefined;
483
+ if (innerCone !== undefined && outerCone !== undefined && innerCone >= outerCone) {
484
+ throw new Error('innerCone must be less than outerCone');
485
+ }
486
+ return cleanObject(await tools.lightingTools.createSpotLight({
487
+ name,
488
+ location,
489
+ rotation,
490
+ intensity: args.intensity,
491
+ innerCone: args.innerCone,
492
+ outerCone: args.outerCone,
493
+ radius: args.radius,
494
+ color,
495
+ castShadows
496
+ }));
497
+ }
498
+ if (typeKey === 'rect') {
499
+ return cleanObject(await tools.lightingTools.createRectLight({
500
+ name,
501
+ location,
502
+ rotation,
503
+ intensity: args.intensity,
504
+ width: args.width,
505
+ height: args.height,
506
+ color
507
+ }));
508
+ }
509
+ if (typeKey === 'sky' || typeKey === 'skylight') {
510
+ return cleanObject(await tools.lightingTools.createSkyLight({
511
+ name,
512
+ sourceType: args.sourceType,
513
+ cubemapPath: args.cubemapPath,
514
+ intensity: args.intensity,
515
+ recapture: args.recapture
516
+ }));
517
+ }
518
+ throw new Error(`Unknown light type: ${lightType}`);
185
519
  }
186
520
  case 'build_lighting': {
187
521
  const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true });
@@ -193,29 +527,75 @@ case 'manage_level':
193
527
 
194
528
  // 5. ANIMATION & PHYSICS
195
529
  case 'animation_physics':
196
- // Validate action exists
197
- if (!args.action) {
198
- throw new Error('Missing required parameter: action');
199
- }
200
-
201
- switch (args.action) {
530
+ switch (requireAction(args)) {
202
531
  case 'create_animation_bp': {
203
- if (typeof args.name !== 'string' || args.name.trim() === '') throw new Error('Invalid name');
204
- if (typeof args.skeletonPath !== 'string' || args.skeletonPath.trim() === '') throw new Error('Invalid skeletonPath');
205
- const res = await tools.animationTools.createAnimationBlueprint({ name: args.name, skeletonPath: args.skeletonPath, savePath: args.savePath });
532
+ await elicitMissingPrimitiveArgs(
533
+ tools,
534
+ args,
535
+ 'Provide details for animation_physics.create_animation_bp',
536
+ {
537
+ name: {
538
+ type: 'string',
539
+ title: 'Blueprint Name',
540
+ description: 'Name of the Animation Blueprint to create'
541
+ },
542
+ skeletonPath: {
543
+ type: 'string',
544
+ title: 'Skeleton Path',
545
+ description: 'Content path of the skeleton asset to bind'
546
+ }
547
+ }
548
+ );
549
+ const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
550
+ const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
551
+ const res = await tools.animationTools.createAnimationBlueprint({ name, skeletonPath, savePath: args.savePath });
206
552
  return cleanObject(res);
207
553
  }
208
554
  case 'play_montage': {
209
- if (typeof args.actorName !== 'string' || args.actorName.trim() === '') throw new Error('Invalid actorName');
555
+ await elicitMissingPrimitiveArgs(
556
+ tools,
557
+ args,
558
+ 'Provide playback details for animation_physics.play_montage',
559
+ {
560
+ actorName: {
561
+ type: 'string',
562
+ title: 'Actor Name',
563
+ description: 'Actor that should play the montage'
564
+ },
565
+ montagePath: {
566
+ type: 'string',
567
+ title: 'Montage Path',
568
+ description: 'Montage or animation asset path to play'
569
+ }
570
+ }
571
+ );
572
+ const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
210
573
  const montagePath = args.montagePath || args.animationPath;
211
- if (typeof montagePath !== 'string' || montagePath.trim() === '') throw new Error('Invalid montagePath');
212
- const res = await tools.animationTools.playAnimation({ actorName: args.actorName, animationType: 'Montage', animationPath: montagePath, playRate: args.playRate });
574
+ const validatedMontage = requireNonEmptyString(montagePath, 'montagePath', 'Invalid montagePath');
575
+ const res = await tools.animationTools.playAnimation({ actorName, animationType: 'Montage', animationPath: validatedMontage, playRate: args.playRate });
213
576
  return cleanObject(res);
214
577
  }
215
578
  case 'setup_ragdoll': {
216
- if (typeof args.skeletonPath !== 'string' || args.skeletonPath.trim() === '') throw new Error('Invalid skeletonPath');
217
- if (typeof args.physicsAssetName !== 'string' || args.physicsAssetName.trim() === '') throw new Error('Invalid physicsAssetName');
218
- const res = await tools.physicsTools.setupRagdoll({ skeletonPath: args.skeletonPath, physicsAssetName: args.physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath });
579
+ await elicitMissingPrimitiveArgs(
580
+ tools,
581
+ args,
582
+ 'Provide setup details for animation_physics.setup_ragdoll',
583
+ {
584
+ skeletonPath: {
585
+ type: 'string',
586
+ title: 'Skeleton Path',
587
+ description: 'Content path for the skeleton asset'
588
+ },
589
+ physicsAssetName: {
590
+ type: 'string',
591
+ title: 'Physics Asset Name',
592
+ description: 'Name of the physics asset to apply'
593
+ }
594
+ }
595
+ );
596
+ const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
597
+ const physicsAssetName = requireNonEmptyString(args.physicsAssetName, 'physicsAssetName', 'Invalid physicsAssetName');
598
+ const res = await tools.physicsTools.setupRagdoll({ skeletonPath, physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath });
219
599
  return cleanObject(res);
220
600
  }
221
601
  default:
@@ -224,20 +604,61 @@ case 'animation_physics':
224
604
 
225
605
  // 6. EFFECTS SYSTEM
226
606
  case 'create_effect':
227
- switch (args.action) {
607
+ switch (requireAction(args)) {
228
608
  case 'particle': {
609
+ await elicitMissingPrimitiveArgs(
610
+ tools,
611
+ args,
612
+ 'Provide the particle effect details for create_effect.particle',
613
+ {
614
+ effectType: {
615
+ type: 'string',
616
+ title: 'Effect Type',
617
+ description: 'Preset effect type to spawn (e.g., Fire, Smoke)'
618
+ }
619
+ }
620
+ );
229
621
  const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters });
230
622
  return cleanObject(res);
231
623
  }
232
624
  case 'niagara': {
233
- if (typeof args.systemPath !== 'string' || args.systemPath.trim() === '') throw new Error('Invalid systemPath');
234
- // Create or ensure system exists (spawning in editor is not universally supported via RC)
235
- const name = args.name || args.systemPath.split('/').pop();
236
- const res = await tools.niagaraTools.createSystem({ name, savePath: args.savePath || '/Game/Effects/Niagara' });
625
+ await elicitMissingPrimitiveArgs(
626
+ tools,
627
+ args,
628
+ 'Provide the Niagara system path for create_effect.niagara',
629
+ {
630
+ systemPath: {
631
+ type: 'string',
632
+ title: 'Niagara System Path',
633
+ description: 'Asset path of the Niagara system to spawn'
634
+ }
635
+ }
636
+ );
637
+ const systemPath = requireNonEmptyString(args.systemPath, 'systemPath', 'Invalid systemPath');
638
+ const verifyResult = await tools.bridge.executePythonWithResult(`
639
+ import unreal, json
640
+ path = r"${systemPath}"
641
+ exists = unreal.EditorAssetLibrary.does_asset_exist(path)
642
+ print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path}))
643
+ `.trim());
644
+ if (!verifyResult?.exists) {
645
+ return cleanObject({ success: false, error: `Niagara system not found at ${systemPath}` });
646
+ }
647
+ const loc = Array.isArray(args.location)
648
+ ? { x: args.location[0], y: args.location[1], z: args.location[2] }
649
+ : args.location || { x: 0, y: 0, z: 0 };
650
+ const res = await tools.niagaraTools.spawnEffect({
651
+ systemPath,
652
+ location: [loc.x ?? 0, loc.y ?? 0, loc.z ?? 0],
653
+ rotation: Array.isArray(args.rotation) ? args.rotation : undefined,
654
+ scale: args.scale
655
+ });
237
656
  return cleanObject(res);
238
657
  }
239
658
  case 'debug_shape': {
240
- const shape = String(args.shape || 'Sphere').toLowerCase();
659
+ const shapeInput = args.shape ?? 'Sphere';
660
+ const shape = String(shapeInput).trim().toLowerCase();
661
+ const originalShapeLabel = String(shapeInput).trim() || 'shape';
241
662
  const loc = args.location || { x: 0, y: 0, z: 0 };
242
663
  const size = args.size || 100;
243
664
  const color = args.color || [255, 0, 0, 255];
@@ -264,7 +685,7 @@ case 'animation_physics':
264
685
  return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration }));
265
686
  }
266
687
  // Default fallback
267
- return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
688
+ return cleanObject({ success: false, error: `Unsupported debug shape: ${originalShapeLabel}` });
268
689
  }
269
690
  default:
270
691
  throw new Error(`Unknown effect action: ${args.action}`);
@@ -272,12 +693,56 @@ case 'animation_physics':
272
693
 
273
694
  // 7. BLUEPRINT MANAGER
274
695
  case 'manage_blueprint':
275
- switch (args.action) {
696
+ switch (requireAction(args)) {
276
697
  case 'create': {
277
- const res = await tools.blueprintTools.createBlueprint({ name: args.name, blueprintType: args.blueprintType || 'Actor', savePath: args.savePath });
698
+ await elicitMissingPrimitiveArgs(
699
+ tools,
700
+ args,
701
+ 'Provide details for manage_blueprint.create',
702
+ {
703
+ name: {
704
+ type: 'string',
705
+ title: 'Blueprint Name',
706
+ description: 'Name for the new Blueprint asset'
707
+ },
708
+ blueprintType: {
709
+ type: 'string',
710
+ title: 'Blueprint Type',
711
+ description: 'Base type such as Actor, Pawn, Character, etc.'
712
+ }
713
+ }
714
+ );
715
+ const res = await tools.blueprintTools.createBlueprint({
716
+ name: args.name,
717
+ blueprintType: args.blueprintType || 'Actor',
718
+ savePath: args.savePath,
719
+ parentClass: args.parentClass
720
+ });
278
721
  return cleanObject(res);
279
722
  }
280
723
  case 'add_component': {
724
+ await elicitMissingPrimitiveArgs(
725
+ tools,
726
+ args,
727
+ 'Provide details for manage_blueprint.add_component',
728
+ {
729
+ name: {
730
+ type: 'string',
731
+ title: 'Blueprint Name',
732
+ description: 'Blueprint asset to modify'
733
+ },
734
+ componentType: {
735
+ type: 'string',
736
+ title: 'Component Type',
737
+ description: 'Component class to add (e.g., StaticMeshComponent)'
738
+ },
739
+ componentName: {
740
+ type: 'string',
741
+ title: 'Component Name',
742
+ description: 'Name for the new component'
743
+ }
744
+ }
745
+ );
281
746
  const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName });
282
747
  return cleanObject(res);
283
748
  }
@@ -287,7 +752,7 @@ case 'animation_physics':
287
752
 
288
753
  // 8. ENVIRONMENT BUILDER
289
754
  case 'build_environment':
290
- switch (args.action) {
755
+ switch (requireAction(args)) {
291
756
  case 'create_landscape': {
292
757
  const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath });
293
758
  return cleanObject(res);
@@ -359,8 +824,7 @@ case 'animation_physics':
359
824
 
360
825
  // 9. SYSTEM CONTROL
361
826
  case 'system_control':
362
- if (!args.action) throw new Error('Missing required parameter: action');
363
- switch (args.action) {
827
+ switch (requireAction(args)) {
364
828
  case 'profile': {
365
829
  const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
366
830
  return cleanObject(res);
@@ -374,16 +838,48 @@ case 'system_control':
374
838
  return cleanObject(res);
375
839
  }
376
840
  case 'play_sound': {
841
+ await elicitMissingPrimitiveArgs(
842
+ tools,
843
+ args,
844
+ 'Provide the audio asset for system_control.play_sound',
845
+ {
846
+ soundPath: {
847
+ type: 'string',
848
+ title: 'Sound Asset Path',
849
+ description: 'Asset path of the sound to play'
850
+ }
851
+ }
852
+ );
853
+ const soundPath = requireNonEmptyString(args.soundPath, 'soundPath', 'Missing required parameter: soundPath');
377
854
  if (args.location && typeof args.location === 'object') {
378
855
  const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0];
379
- const res = await tools.audioTools.playSoundAtLocation({ soundPath: args.soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime });
856
+ const res = await tools.audioTools.playSoundAtLocation({ soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime });
380
857
  return cleanObject(res);
381
858
  }
382
- const res = await tools.audioTools.playSound2D({ soundPath: args.soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
859
+ const res = await tools.audioTools.playSound2D({ soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
383
860
  return cleanObject(res);
384
861
  }
385
862
  case 'create_widget': {
386
- const res = await tools.uiTools.createWidget({ name: args.widgetName, type: args.widgetType, savePath: args.savePath });
863
+ await elicitMissingPrimitiveArgs(
864
+ tools,
865
+ args,
866
+ 'Provide details for system_control.create_widget',
867
+ {
868
+ widgetName: {
869
+ type: 'string',
870
+ title: 'Widget Name',
871
+ description: 'Name for the new UI widget asset'
872
+ },
873
+ widgetType: {
874
+ type: 'string',
875
+ title: 'Widget Type',
876
+ description: 'Widget type such as HUD, Menu, Overlay, etc.'
877
+ }
878
+ }
879
+ );
880
+ const widgetName = requireNonEmptyString(args.widgetName ?? args.name, 'widgetName', 'Missing required parameter: widgetName');
881
+ const widgetType = requireNonEmptyString(args.widgetType, 'widgetType', 'Missing required parameter: widgetType');
882
+ const res = await tools.uiTools.createWidget({ name: widgetName, type: widgetType as any, savePath: args.savePath });
387
883
  return cleanObject(res);
388
884
  }
389
885
  case 'show_widget': {
@@ -418,8 +914,22 @@ case 'console_command':
418
914
  return { success: false, error: 'Command blocked for safety' } as any;
419
915
  }
420
916
  try {
421
- const res = await tools.bridge.executeConsoleCommand(cmd);
422
- return cleanObject({ success: true, command: cmd, result: res });
917
+ const raw = await tools.bridge.executeConsoleCommand(cmd);
918
+ const summary = tools.bridge.summarizeConsoleCommand(cmd, raw);
919
+ const output = summary.output || '';
920
+ const looksInvalid = /unknown|invalid/i.test(output);
921
+ return cleanObject({
922
+ success: summary.returnValue !== false && !looksInvalid,
923
+ command: summary.command,
924
+ output: output || undefined,
925
+ logLines: summary.logLines?.length ? summary.logLines : undefined,
926
+ returnValue: summary.returnValue,
927
+ message: !looksInvalid
928
+ ? (output || 'Command executed')
929
+ : undefined,
930
+ error: looksInvalid ? output : undefined,
931
+ raw: summary.raw
932
+ });
423
933
  } catch (e: any) {
424
934
  return cleanObject({ success: false, command: cmd, error: e?.message || String(e) });
425
935
  }
@@ -427,12 +937,10 @@ case 'console_command':
427
937
 
428
938
  // 11. REMOTE CONTROL PRESETS - Direct implementation
429
939
  case 'manage_rc':
430
- if (!args.action) throw new Error('Missing required parameter: action');
431
-
432
940
  // Handle RC operations directly through RcTools
433
941
  let rcResult: any;
434
-
435
- switch (args.action) {
942
+ const rcAction = requireAction(args);
943
+ switch (rcAction) {
436
944
  // Support both 'create_preset' and 'create' for compatibility
437
945
  case 'create_preset':
438
946
  case 'create':
@@ -459,8 +967,10 @@ case 'console_command':
459
967
  break;
460
968
 
461
969
  case 'delete':
462
- if (!args.presetId) throw new Error('Missing required parameter: presetId');
463
- rcResult = await tools.rcTools.deletePreset(args.presetId);
970
+ case 'delete_preset':
971
+ const presetIdentifier = args.presetId || args.presetPath;
972
+ if (!presetIdentifier) throw new Error('Missing required parameter: presetId');
973
+ rcResult = await tools.rcTools.deletePreset(presetIdentifier);
464
974
  if (rcResult.success) {
465
975
  rcResult.message = 'Preset deleted successfully';
466
976
  }
@@ -557,7 +1067,7 @@ case 'console_command':
557
1067
  break;
558
1068
 
559
1069
  default:
560
- throw new Error(`Unknown RC action: ${args.action}. Valid actions are: create_preset, expose_actor, expose_property, list_fields, set_property, get_property, or their simplified versions: create, list, delete, expose, get_exposed, set_value, get_value, call_function`);
1070
+ throw new Error(`Unknown RC action: ${rcAction}. Valid actions are: create_preset, expose_actor, expose_property, list_fields, set_property, get_property, or their simplified versions: create, list, delete, expose, get_exposed, set_value, get_value, call_function`);
561
1071
  }
562
1072
 
563
1073
  // Return result directly - MCP formatting will be handled by response validator
@@ -566,14 +1076,13 @@ case 'console_command':
566
1076
 
567
1077
  // 12. SEQUENCER / CINEMATICS
568
1078
  case 'manage_sequence':
569
- if (!args.action) throw new Error('Missing required parameter: action');
570
-
571
1079
  // Direct handling for sequence operations
572
1080
  const seqResult = await (async () => {
573
1081
  const sequenceTools = tools.sequenceTools;
574
1082
  if (!sequenceTools) throw new Error('Sequence tools not available');
1083
+ const action = requireAction(args);
575
1084
 
576
- switch (args.action) {
1085
+ switch (action) {
577
1086
  case 'create':
578
1087
  return await sequenceTools.create({ name: args.name, path: args.path });
579
1088
  case 'open':
@@ -613,7 +1122,7 @@ case 'console_command':
613
1122
  if (args.speed === undefined) throw new Error('Missing required parameter: speed');
614
1123
  return await sequenceTools.setPlaybackSpeed({ speed: args.speed });
615
1124
  default:
616
- throw new Error(`Unknown sequence action: ${args.action}`);
1125
+ throw new Error(`Unknown sequence action: ${action}`);
617
1126
  }
618
1127
  })();
619
1128
 
@@ -622,8 +1131,8 @@ case 'console_command':
622
1131
  return cleanObject(seqResult);
623
1132
  // 13. INTROSPECTION
624
1133
  case 'inspect':
625
- if (!args.action) throw new Error('Missing required parameter: action');
626
- switch (args.action) {
1134
+ const inspectAction = requireAction(args);
1135
+ switch (inspectAction) {
627
1136
  case 'inspect_object': {
628
1137
  const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed });
629
1138
  return cleanObject(res);
@@ -633,7 +1142,7 @@ case 'inspect':
633
1142
  return cleanObject(res);
634
1143
  }
635
1144
  default:
636
- throw new Error(`Unknown inspect action: ${args.action}`);
1145
+ throw new Error(`Unknown inspect action: ${inspectAction}`);
637
1146
  }
638
1147
 
639
1148