unreal-engine-mcp-server 0.3.1 → 0.4.3

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 (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  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.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. package/src/utils/errors.ts +0 -312
@@ -1,818 +1,814 @@
1
1
  // Consolidated tool handlers - maps 13 tools to all 36 operations
2
- import { handleToolCall } from './tool-handlers.js';
3
2
  import { cleanObject } from '../utils/safe-json.js';
4
3
  import { Logger } from '../utils/logger.js';
5
4
  const log = new Logger('ConsolidatedToolHandler');
5
+ const ACTION_REQUIRED_ERROR = 'Missing required parameter: action';
6
+ function ensureArgsPresent(args) {
7
+ if (args === null || args === undefined) {
8
+ throw new Error('Invalid arguments: null or undefined');
9
+ }
10
+ }
11
+ function requireAction(args) {
12
+ ensureArgsPresent(args);
13
+ const action = args.action;
14
+ if (typeof action !== 'string' || action.trim() === '') {
15
+ throw new Error(ACTION_REQUIRED_ERROR);
16
+ }
17
+ return action;
18
+ }
19
+ function requireNonEmptyString(value, field, message) {
20
+ if (typeof value !== 'string' || value.trim() === '') {
21
+ throw new Error(message ?? `Invalid ${field}: must be a non-empty string`);
22
+ }
23
+ return value;
24
+ }
25
+ function requirePositiveNumber(value, field, message) {
26
+ if (typeof value !== 'number' || !isFinite(value) || value <= 0) {
27
+ throw new Error(message ?? `Invalid ${field}: must be a positive number`);
28
+ }
29
+ return value;
30
+ }
31
+ function requireVector3Components(vector, message) {
32
+ if (!vector ||
33
+ typeof vector.x !== 'number' ||
34
+ typeof vector.y !== 'number' ||
35
+ typeof vector.z !== 'number') {
36
+ throw new Error(message);
37
+ }
38
+ return [vector.x, vector.y, vector.z];
39
+ }
40
+ function getElicitationTimeoutMs(tools) {
41
+ if (!tools)
42
+ return undefined;
43
+ const direct = tools.elicitationTimeoutMs;
44
+ if (typeof direct === 'number' && Number.isFinite(direct)) {
45
+ return direct;
46
+ }
47
+ if (typeof tools.getElicitationTimeoutMs === 'function') {
48
+ const value = tools.getElicitationTimeoutMs();
49
+ if (typeof value === 'number' && Number.isFinite(value)) {
50
+ return value;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+ async function elicitMissingPrimitiveArgs(tools, args, prompt, fieldSchemas) {
56
+ if (!tools ||
57
+ typeof tools.supportsElicitation !== 'function' ||
58
+ !tools.supportsElicitation() ||
59
+ typeof tools.elicit !== 'function') {
60
+ return;
61
+ }
62
+ const properties = {};
63
+ const required = [];
64
+ for (const [key, schema] of Object.entries(fieldSchemas)) {
65
+ const value = args?.[key];
66
+ const missing = value === undefined ||
67
+ value === null ||
68
+ (typeof value === 'string' && value.trim() === '');
69
+ if (missing) {
70
+ properties[key] = schema;
71
+ required.push(key);
72
+ }
73
+ }
74
+ if (required.length === 0)
75
+ return;
76
+ const timeoutMs = getElicitationTimeoutMs(tools);
77
+ const options = {
78
+ fallback: async () => ({ ok: false, error: 'missing-params' })
79
+ };
80
+ if (typeof timeoutMs === 'number') {
81
+ options.timeoutMs = timeoutMs;
82
+ }
83
+ try {
84
+ const elicited = await tools.elicit(prompt, { type: 'object', properties, required }, options);
85
+ if (elicited?.ok && elicited.value) {
86
+ for (const key of required) {
87
+ const value = elicited.value[key];
88
+ if (value === undefined || value === null)
89
+ continue;
90
+ args[key] = typeof value === 'string' ? value.trim() : value;
91
+ }
92
+ }
93
+ }
94
+ catch (err) {
95
+ log.debug('Special elicitation fallback skipped', {
96
+ prompt,
97
+ err: err?.message || String(err)
98
+ });
99
+ }
100
+ }
6
101
  export async function handleConsolidatedToolCall(name, args, tools) {
7
102
  const startTime = Date.now();
8
103
  // Use scoped logger (stderr) to avoid polluting stdout JSON
9
104
  log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
10
105
  try {
11
- // Validate args is not null/undefined
12
- if (args === null || args === undefined) {
13
- throw new Error('Invalid arguments: null or undefined');
14
- }
15
- let mappedName;
16
- let mappedArgs = { ...args };
106
+ ensureArgsPresent(args);
17
107
  switch (name) {
18
108
  // 1. ASSET MANAGER
19
109
  case 'manage_asset':
20
- // Validate args is not null/undefined
21
- if (args === null || args === undefined) {
22
- throw new Error('Invalid arguments: null or undefined');
23
- }
24
- // Validate action exists
25
- if (!args.action) {
26
- throw new Error('Missing required parameter: action');
27
- }
28
- switch (args.action) {
29
- case 'list':
30
- // Directory is optional
31
- if (args.directory !== undefined && args.directory !== null) {
32
- if (typeof args.directory !== 'string') {
33
- throw new Error('Invalid directory: must be a string');
34
- }
110
+ switch (requireAction(args)) {
111
+ case 'list': {
112
+ if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') {
113
+ throw new Error('Invalid directory: must be a string');
35
114
  }
36
- mappedName = 'list_assets';
37
- mappedArgs = {
38
- directory: args.directory
39
- // recursive removed - always false internally
40
- };
41
- break;
42
- case 'import':
43
- // Validate required parameters
44
- if (args.sourcePath === undefined || args.sourcePath === null) {
45
- throw new Error('Missing required parameter: sourcePath');
46
- }
47
- if (typeof args.sourcePath !== 'string') {
48
- throw new Error('Invalid sourcePath: must be a string');
49
- }
50
- if (args.sourcePath.trim() === '') {
51
- throw new Error('Invalid sourcePath: cannot be empty');
52
- }
53
- if (args.destinationPath === undefined || args.destinationPath === null) {
54
- throw new Error('Missing required parameter: destinationPath');
55
- }
56
- if (typeof args.destinationPath !== 'string') {
57
- throw new Error('Invalid destinationPath: must be a string');
58
- }
59
- if (args.destinationPath.trim() === '') {
60
- throw new Error('Invalid destinationPath: cannot be empty');
61
- }
62
- mappedName = 'import_asset';
63
- mappedArgs = {
64
- sourcePath: args.sourcePath,
65
- destinationPath: args.destinationPath
66
- };
67
- break;
68
- case 'create_material':
69
- // Validate required parameters
70
- if (args.name === undefined || args.name === null) {
71
- throw new Error('Missing required parameter: name');
72
- }
73
- if (typeof args.name !== 'string') {
74
- throw new Error('Invalid name: must be a string');
75
- }
76
- if (args.name.trim() === '') {
77
- throw new Error('Invalid name: cannot be empty');
78
- }
79
- // Path is optional
80
- if (args.path !== undefined && args.path !== null) {
81
- if (typeof args.path !== 'string') {
82
- throw new Error('Invalid path: must be a string');
115
+ const res = await tools.assetResources.list(args.directory || '/Game', false);
116
+ return cleanObject({ success: true, ...res });
117
+ }
118
+ case 'import': {
119
+ let sourcePath = typeof args.sourcePath === 'string' ? args.sourcePath.trim() : '';
120
+ let destinationPath = typeof args.destinationPath === 'string' ? args.destinationPath.trim() : '';
121
+ if ((!sourcePath || !destinationPath) && typeof tools.supportsElicitation === 'function' && tools.supportsElicitation() && typeof tools.elicit === 'function') {
122
+ const schemaProps = {};
123
+ const required = [];
124
+ if (!sourcePath) {
125
+ schemaProps.sourcePath = {
126
+ type: 'string',
127
+ title: 'Source File Path',
128
+ description: 'Full path to the asset file on disk to import'
129
+ };
130
+ required.push('sourcePath');
131
+ }
132
+ if (!destinationPath) {
133
+ schemaProps.destinationPath = {
134
+ type: 'string',
135
+ title: 'Destination Path',
136
+ description: 'Unreal content path where the asset should be imported (e.g., /Game/MCP/Assets)'
137
+ };
138
+ required.push('destinationPath');
139
+ }
140
+ if (required.length > 0) {
141
+ const timeoutMs = getElicitationTimeoutMs(tools);
142
+ const options = { fallback: async () => ({ ok: false, error: 'missing-import-params' }) };
143
+ if (typeof timeoutMs === 'number') {
144
+ options.timeoutMs = timeoutMs;
145
+ }
146
+ const elicited = await tools.elicit('Provide the missing import parameters for manage_asset.import', { type: 'object', properties: schemaProps, required }, options);
147
+ if (elicited?.ok && elicited.value) {
148
+ if (typeof elicited.value.sourcePath === 'string') {
149
+ sourcePath = elicited.value.sourcePath.trim();
150
+ }
151
+ if (typeof elicited.value.destinationPath === 'string') {
152
+ destinationPath = elicited.value.destinationPath.trim();
153
+ }
154
+ }
83
155
  }
84
156
  }
85
- mappedName = 'create_material';
86
- mappedArgs = {
87
- name: args.name,
88
- path: args.path
89
- };
90
- break;
157
+ const sourcePathValidated = requireNonEmptyString(sourcePath || args.sourcePath, 'sourcePath', 'Invalid sourcePath');
158
+ const destinationPathValidated = requireNonEmptyString(destinationPath || args.destinationPath, 'destinationPath', 'Invalid destinationPath');
159
+ const res = await tools.assetTools.importAsset(sourcePathValidated, destinationPathValidated);
160
+ return cleanObject(res);
161
+ }
162
+ case 'create_material': {
163
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the material details for manage_asset.create_material', {
164
+ name: {
165
+ type: 'string',
166
+ title: 'Material Name',
167
+ description: 'Name for the new material asset'
168
+ },
169
+ path: {
170
+ type: 'string',
171
+ title: 'Save Path',
172
+ description: 'Optional Unreal content path where the material should be saved'
173
+ }
174
+ });
175
+ const sanitizedName = typeof args.name === 'string' ? args.name.trim() : args.name;
176
+ const sanitizedPath = typeof args.path === 'string' ? args.path.trim() : args.path;
177
+ const name = requireNonEmptyString(sanitizedName, 'name', 'Invalid name: must be a non-empty string');
178
+ const res = await tools.materialTools.createMaterial(name, sanitizedPath || '/Game/Materials');
179
+ return cleanObject(res);
180
+ }
91
181
  default:
92
182
  throw new Error(`Unknown asset action: ${args.action}`);
93
183
  }
94
- break;
95
184
  // 2. ACTOR CONTROL
96
185
  case 'control_actor':
97
- // Validate action exists
98
- if (!args.action) {
99
- throw new Error('Missing required parameter: action');
100
- }
101
- switch (args.action) {
102
- case 'spawn':
103
- // Validate spawn parameters
104
- if (!args.classPath) {
105
- throw new Error('Missing required parameter: classPath');
106
- }
107
- if (typeof args.classPath !== 'string' || args.classPath.trim() === '') {
108
- throw new Error('Invalid classPath: must be a non-empty string');
109
- }
110
- mappedName = 'spawn_actor';
111
- mappedArgs = {
112
- classPath: args.classPath,
186
+ switch (requireAction(args)) {
187
+ case 'spawn': {
188
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the spawn parameters for control_actor.spawn', {
189
+ classPath: {
190
+ type: 'string',
191
+ title: 'Actor Class or Asset Path',
192
+ description: 'Class name (e.g., StaticMeshActor) or asset path (e.g., /Engine/BasicShapes/Cube) to spawn'
193
+ }
194
+ });
195
+ const classPathInput = typeof args.classPath === 'string' ? args.classPath.trim() : args.classPath;
196
+ const classPath = requireNonEmptyString(classPathInput, 'classPath', 'Invalid classPath: must be a non-empty string');
197
+ const actorNameInput = typeof args.actorName === 'string' && args.actorName.trim() !== ''
198
+ ? args.actorName
199
+ : (typeof args.name === 'string' ? args.name : undefined);
200
+ const res = await tools.actorTools.spawn({
201
+ classPath,
113
202
  location: args.location,
114
- rotation: args.rotation
115
- };
116
- break;
117
- case 'delete':
118
- // Validate delete parameters
119
- if (!args.actorName) {
120
- throw new Error('Missing required parameter: actorName');
121
- }
122
- if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
123
- throw new Error('Invalid actorName: must be a non-empty string');
124
- }
125
- mappedName = 'delete_actor';
126
- mappedArgs = {
127
- actorName: args.actorName
128
- };
129
- break;
130
- case 'apply_force':
131
- // Validate apply_force parameters
132
- if (!args.actorName) {
133
- throw new Error('Missing required parameter: actorName');
134
- }
135
- if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
136
- throw new Error('Invalid actorName: must be a non-empty string');
137
- }
138
- if (!args.force) {
139
- throw new Error('Missing required parameter: force');
140
- }
141
- if (typeof args.force !== 'object' || args.force === null) {
142
- throw new Error('Invalid force: must be an object with x, y, z properties');
143
- }
144
- if (typeof args.force.x !== 'number' ||
145
- typeof args.force.y !== 'number' ||
146
- typeof args.force.z !== 'number') {
147
- throw new Error('Invalid force: x, y, z must all be numbers');
148
- }
149
- mappedName = 'apply_force';
150
- mappedArgs = {
151
- actorName: args.actorName,
152
- force: args.force
153
- };
154
- break;
203
+ rotation: args.rotation,
204
+ actorName: actorNameInput
205
+ });
206
+ return cleanObject(res);
207
+ }
208
+ case 'delete': {
209
+ await elicitMissingPrimitiveArgs(tools, args, 'Which actor should control_actor.delete remove?', {
210
+ actorName: {
211
+ type: 'string',
212
+ title: 'Actor Name',
213
+ description: 'Exact label of the actor to delete'
214
+ }
215
+ });
216
+ const actorNameArg = typeof args.actorName === 'string' && args.actorName.trim() !== ''
217
+ ? args.actorName
218
+ : (typeof args.name === 'string' ? args.name : undefined);
219
+ const actorName = requireNonEmptyString(actorNameArg, 'actorName', 'Invalid actorName');
220
+ const res = await tools.bridge.executeEditorFunction('DELETE_ACTOR', { actor_name: actorName });
221
+ return cleanObject(res);
222
+ }
223
+ case 'apply_force': {
224
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the target actor for control_actor.apply_force', {
225
+ actorName: {
226
+ type: 'string',
227
+ title: 'Actor Name',
228
+ description: 'Physics-enabled actor that should receive the force'
229
+ }
230
+ });
231
+ const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
232
+ const vector = requireVector3Components(args.force, 'Invalid force: must have numeric x,y,z');
233
+ const res = await tools.physicsTools.applyForce({
234
+ actorName,
235
+ forceType: 'Force',
236
+ vector
237
+ });
238
+ return cleanObject(res);
239
+ }
155
240
  default:
156
241
  throw new Error(`Unknown actor action: ${args.action}`);
157
242
  }
158
- break;
159
243
  // 3. EDITOR CONTROL
160
244
  case 'control_editor':
161
- // Validate action exists
162
- if (!args.action) {
163
- throw new Error('Missing required parameter: action');
164
- }
165
- switch (args.action) {
166
- case 'play':
167
- mappedName = 'play_in_editor';
168
- mappedArgs = {};
169
- break;
170
- case 'stop':
171
- mappedName = 'stop_play_in_editor';
172
- mappedArgs = {};
173
- break;
174
- case 'pause':
175
- mappedName = 'pause_play_in_editor';
176
- mappedArgs = {};
177
- break;
178
- case 'set_game_speed':
179
- // Validate game speed parameter
180
- if (args.speed === undefined || args.speed === null) {
181
- throw new Error('Missing required parameter: speed');
182
- }
183
- if (typeof args.speed !== 'number') {
184
- throw new Error('Invalid speed: must be a number');
185
- }
186
- if (isNaN(args.speed)) {
187
- throw new Error('Invalid speed: cannot be NaN');
188
- }
189
- if (!isFinite(args.speed)) {
190
- throw new Error('Invalid speed: must be finite');
191
- }
192
- if (args.speed <= 0) {
193
- throw new Error('Invalid speed: must be positive');
194
- }
195
- mappedName = 'set_game_speed';
196
- mappedArgs = {
197
- speed: args.speed
198
- };
199
- break;
200
- case 'eject':
201
- mappedName = 'eject_from_pawn';
202
- mappedArgs = {};
203
- break;
204
- case 'possess':
205
- mappedName = 'possess_pawn';
206
- mappedArgs = {};
207
- break;
208
- case 'set_camera':
209
- // Allow either location or rotation or both
210
- // Don't require both to be present
211
- mappedName = 'set_camera';
212
- mappedArgs = {
213
- location: args.location,
214
- rotation: args.rotation
215
- };
216
- break;
217
- case 'set_view_mode':
218
- // Validate view mode parameter
219
- if (!args.viewMode) {
220
- throw new Error('Missing required parameter: viewMode');
221
- }
222
- if (typeof args.viewMode !== 'string' || args.viewMode.trim() === '') {
223
- throw new Error('Invalid viewMode: must be a non-empty string');
224
- }
225
- // Normalize view mode to match what debug.ts expects
226
- const validModes = ['lit', 'unlit', 'wireframe', 'detail_lighting', 'lighting_only',
227
- 'light_complexity', 'shader_complexity', 'lightmap_density',
228
- 'stationary_light_overlap', 'reflections', 'visualize_buffer',
229
- 'collision_pawn', 'collision_visibility', 'lod_coloration', 'quad_overdraw'];
230
- const normalizedMode = args.viewMode.toLowerCase().replace(/_/g, '');
231
- // Map to proper case for debug.ts
232
- let mappedMode = '';
233
- switch (normalizedMode) {
234
- case 'lit':
235
- mappedMode = 'Lit';
236
- break;
237
- case 'unlit':
238
- mappedMode = 'Unlit';
239
- break;
240
- case 'wireframe':
241
- mappedMode = 'Wireframe';
242
- break;
243
- case 'detaillighting':
244
- mappedMode = 'DetailLighting';
245
- break;
246
- case 'lightingonly':
247
- mappedMode = 'LightingOnly';
248
- break;
249
- case 'lightcomplexity':
250
- mappedMode = 'LightComplexity';
251
- break;
252
- case 'shadercomplexity':
253
- mappedMode = 'ShaderComplexity';
254
- break;
255
- case 'lightmapdensity':
256
- mappedMode = 'LightmapDensity';
257
- break;
258
- case 'stationarylightoverlap':
259
- mappedMode = 'StationaryLightOverlap';
260
- break;
261
- case 'reflections':
262
- mappedMode = 'ReflectionOverride';
263
- break;
264
- case 'visualizebuffer':
265
- mappedMode = 'VisualizeBuffer';
266
- break;
267
- case 'collisionpawn':
268
- mappedMode = 'CollisionPawn';
269
- break;
270
- case 'collisionvisibility':
271
- mappedMode = 'CollisionVisibility';
272
- break;
273
- case 'lodcoloration':
274
- mappedMode = 'LODColoration';
275
- break;
276
- case 'quadoverdraw':
277
- mappedMode = 'QuadOverdraw';
278
- break;
279
- default:
280
- throw new Error(`Invalid viewMode: '${args.viewMode}'. Valid modes are: ${validModes.join(', ')}`);
281
- }
282
- mappedName = 'set_view_mode';
283
- mappedArgs = {
284
- mode: mappedMode
285
- };
286
- break;
245
+ switch (requireAction(args)) {
246
+ case 'play': {
247
+ const res = await tools.editorTools.playInEditor();
248
+ return cleanObject(res);
249
+ }
250
+ case 'stop': {
251
+ const res = await tools.editorTools.stopPlayInEditor();
252
+ return cleanObject(res);
253
+ }
254
+ case 'pause': {
255
+ const res = await tools.editorTools.pausePlayInEditor();
256
+ return cleanObject(res);
257
+ }
258
+ case 'set_game_speed': {
259
+ const speed = requirePositiveNumber(args.speed, 'speed', 'Invalid speed: must be a positive number');
260
+ // Use console command via bridge
261
+ const res = await tools.bridge.executeConsoleCommand(`slomo ${speed}`);
262
+ return cleanObject(res);
263
+ }
264
+ case 'eject': {
265
+ const res = await tools.bridge.executeConsoleCommand('eject');
266
+ return cleanObject(res);
267
+ }
268
+ case 'possess': {
269
+ const res = await tools.bridge.executeConsoleCommand('viewself');
270
+ return cleanObject(res);
271
+ }
272
+ case 'set_camera': {
273
+ const res = await tools.editorTools.setViewportCamera(args.location, args.rotation);
274
+ return cleanObject(res);
275
+ }
276
+ case 'set_view_mode': {
277
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the view mode for control_editor.set_view_mode', {
278
+ viewMode: {
279
+ type: 'string',
280
+ title: 'View Mode',
281
+ description: 'Viewport view mode (e.g., Lit, Unlit, Wireframe)'
282
+ }
283
+ });
284
+ const viewMode = requireNonEmptyString(args.viewMode, 'viewMode', 'Missing required parameter: viewMode');
285
+ const res = await tools.bridge.setSafeViewMode(viewMode);
286
+ return cleanObject(res);
287
+ }
287
288
  default:
288
289
  throw new Error(`Unknown editor action: ${args.action}`);
289
290
  }
290
- break;
291
291
  // 4. LEVEL MANAGER
292
292
  case 'manage_level':
293
- switch (args.action) {
294
- case 'load':
295
- mappedName = 'load_level';
296
- mappedArgs = {
297
- levelPath: args.levelPath
298
- };
299
- if (args.streaming !== undefined) {
300
- mappedArgs.streaming = args.streaming;
301
- }
302
- break;
303
- case 'save':
304
- mappedName = 'save_level';
305
- mappedArgs = {
306
- levelName: args.levelName
307
- };
308
- if (args.savePath !== undefined) {
309
- mappedArgs.savePath = args.savePath;
310
- }
311
- break;
312
- case 'stream':
313
- mappedName = 'stream_level';
314
- mappedArgs = {
315
- levelName: args.levelName,
316
- shouldBeLoaded: args.shouldBeLoaded,
317
- shouldBeVisible: args.shouldBeVisible
318
- };
319
- break;
320
- case 'create_light':
321
- // Validate light type
322
- if (!args.lightType) {
323
- throw new Error('Missing required parameter: lightType');
324
- }
325
- const validLightTypes = ['directional', 'point', 'spot', 'rect', 'sky'];
326
- const normalizedLightType = String(args.lightType).toLowerCase();
327
- if (!validLightTypes.includes(normalizedLightType)) {
328
- throw new Error(`Invalid lightType: '${args.lightType}'. Valid types are: ${validLightTypes.join(', ')}`);
329
- }
330
- // Validate name
331
- if (!args.name) {
332
- throw new Error('Missing required parameter: name');
333
- }
334
- if (typeof args.name !== 'string' || args.name.trim() === '') {
335
- throw new Error('Invalid name: must be a non-empty string');
336
- }
337
- // Validate intensity if provided
338
- if (args.intensity !== undefined) {
339
- if (typeof args.intensity !== 'number' || !isFinite(args.intensity)) {
340
- throw new Error(`Invalid intensity: must be a finite number, got ${typeof args.intensity}`);
293
+ switch (requireAction(args)) {
294
+ case 'load': {
295
+ await elicitMissingPrimitiveArgs(tools, args, 'Select the level to load for manage_level.load', {
296
+ levelPath: {
297
+ type: 'string',
298
+ title: 'Level Path',
299
+ description: 'Content path of the level asset to load (e.g., /Game/Maps/MyLevel)'
341
300
  }
342
- if (args.intensity < 0) {
343
- throw new Error('Invalid intensity: must be non-negative');
301
+ });
302
+ const levelPath = requireNonEmptyString(args.levelPath, 'levelPath', 'Missing required parameter: levelPath');
303
+ const res = await tools.levelTools.loadLevel({ levelPath, streaming: !!args.streaming });
304
+ return cleanObject(res);
305
+ }
306
+ case 'save': {
307
+ const res = await tools.levelTools.saveLevel({ levelName: args.levelName, savePath: args.savePath });
308
+ return cleanObject(res);
309
+ }
310
+ case 'stream': {
311
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the streaming level name for manage_level.stream', {
312
+ levelName: {
313
+ type: 'string',
314
+ title: 'Level Name',
315
+ description: 'Streaming level name to toggle'
344
316
  }
345
- }
346
- // Validate location if provided
347
- if (args.location !== undefined && args.location !== null) {
348
- if (!Array.isArray(args.location) && typeof args.location !== 'object') {
349
- throw new Error('Invalid location: must be an array [x,y,z] or object {x,y,z}');
317
+ });
318
+ const levelName = requireNonEmptyString(args.levelName, 'levelName', 'Missing required parameter: levelName');
319
+ const res = await tools.levelTools.streamLevel({ levelName, shouldBeLoaded: !!args.shouldBeLoaded, shouldBeVisible: !!args.shouldBeVisible });
320
+ return cleanObject(res);
321
+ }
322
+ case 'create_light': {
323
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the light details for manage_level.create_light', {
324
+ lightType: {
325
+ type: 'string',
326
+ title: 'Light Type',
327
+ description: 'Directional, Point, Spot, Rect, or Sky'
328
+ },
329
+ name: {
330
+ type: 'string',
331
+ title: 'Light Name',
332
+ description: 'Name for the new light actor'
350
333
  }
351
- }
352
- mappedName = 'create_light';
353
- mappedArgs = {
354
- lightType: args.lightType,
355
- name: args.name,
356
- location: args.location,
357
- intensity: args.intensity
358
- };
359
- break;
360
- case 'build_lighting':
361
- mappedName = 'build_lighting';
362
- mappedArgs = {
363
- quality: args.quality
364
- };
365
- break;
366
- default:
367
- throw new Error(`Unknown level action: ${args.action}`);
368
- }
369
- break;
370
- // 5. ANIMATION & PHYSICS
371
- case 'animation_physics':
372
- // Validate action exists
373
- if (!args.action) {
374
- throw new Error('Missing required parameter: action');
375
- }
376
- switch (args.action) {
377
- case 'create_animation_bp':
378
- // Validate required parameters
379
- if (args.name === undefined || args.name === null) {
380
- throw new Error('Missing required parameter: name');
381
- }
382
- if (typeof args.name !== 'string') {
383
- throw new Error('Invalid name: must be a string');
384
- }
385
- if (args.name.trim() === '') {
386
- throw new Error('Invalid name: cannot be empty');
387
- }
388
- if (args.skeletonPath === undefined || args.skeletonPath === null) {
389
- throw new Error('Missing required parameter: skeletonPath');
390
- }
391
- if (typeof args.skeletonPath !== 'string') {
392
- throw new Error('Invalid skeletonPath: must be a string');
393
- }
394
- if (args.skeletonPath.trim() === '') {
395
- throw new Error('Invalid skeletonPath: cannot be empty');
396
- }
397
- // Optional savePath validation
398
- if (args.savePath !== undefined && args.savePath !== null) {
399
- if (typeof args.savePath !== 'string') {
400
- throw new Error('Invalid savePath: must be a string');
334
+ });
335
+ const lightType = requireNonEmptyString(args.lightType, 'lightType', 'Missing required parameter: lightType');
336
+ const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
337
+ const typeKey = lightType.toLowerCase();
338
+ const toVector = (value, fallback) => {
339
+ if (Array.isArray(value) && value.length === 3) {
340
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
401
341
  }
402
- }
403
- mappedName = 'create_animation_blueprint';
404
- mappedArgs = {
405
- name: args.name,
406
- skeletonPath: args.skeletonPath,
407
- savePath: args.savePath
342
+ if (value && typeof value === 'object') {
343
+ return [Number(value.x) || 0, Number(value.y) || 0, Number(value.z) || 0];
344
+ }
345
+ return fallback;
408
346
  };
409
- break;
410
- case 'play_montage':
411
- // Validate required parameters
412
- if (args.actorName === undefined || args.actorName === null) {
413
- throw new Error('Missing required parameter: actorName');
414
- }
415
- if (typeof args.actorName !== 'string') {
416
- throw new Error('Invalid actorName: must be a string');
417
- }
418
- if (args.actorName.trim() === '') {
419
- throw new Error('Invalid actorName: cannot be empty');
420
- }
421
- // Check for montagePath or animationPath
422
- const montagePath = args.montagePath || args.animationPath;
423
- if (montagePath === undefined || montagePath === null) {
424
- throw new Error('Missing required parameter: montagePath or animationPath');
425
- }
426
- if (typeof montagePath !== 'string') {
427
- throw new Error('Invalid montagePath: must be a string');
428
- }
429
- if (montagePath.trim() === '') {
430
- throw new Error('Invalid montagePath: cannot be empty');
431
- }
432
- // Optional playRate validation
433
- if (args.playRate !== undefined && args.playRate !== null) {
434
- if (typeof args.playRate !== 'number') {
435
- throw new Error('Invalid playRate: must be a number');
347
+ const toRotator = (value, fallback) => {
348
+ if (Array.isArray(value) && value.length === 3) {
349
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
436
350
  }
437
- if (isNaN(args.playRate)) {
438
- throw new Error('Invalid playRate: cannot be NaN');
351
+ if (value && typeof value === 'object') {
352
+ return [Number(value.pitch) || 0, Number(value.yaw) || 0, Number(value.roll) || 0];
439
353
  }
440
- if (!isFinite(args.playRate)) {
441
- throw new Error('Invalid playRate: must be finite');
354
+ return fallback;
355
+ };
356
+ const toColor = (value) => {
357
+ if (Array.isArray(value) && value.length === 3) {
358
+ return [Number(value[0]) || 0, Number(value[1]) || 0, Number(value[2]) || 0];
442
359
  }
443
- }
444
- mappedName = 'play_animation_montage';
445
- mappedArgs = {
446
- actorName: args.actorName,
447
- montagePath: montagePath,
448
- playRate: args.playRate
360
+ if (value && typeof value === 'object') {
361
+ return [Number(value.r) || 0, Number(value.g) || 0, Number(value.b) || 0];
362
+ }
363
+ return undefined;
449
364
  };
450
- break;
451
- case 'setup_ragdoll':
452
- // Validate required parameters
453
- if (args.skeletonPath === undefined || args.skeletonPath === null) {
454
- throw new Error('Missing required parameter: skeletonPath');
455
- }
456
- if (typeof args.skeletonPath !== 'string') {
457
- throw new Error('Invalid skeletonPath: must be a string');
458
- }
459
- if (args.skeletonPath.trim() === '') {
460
- throw new Error('Invalid skeletonPath: cannot be empty');
461
- }
462
- if (args.physicsAssetName === undefined || args.physicsAssetName === null) {
463
- throw new Error('Missing required parameter: physicsAssetName');
464
- }
465
- if (typeof args.physicsAssetName !== 'string') {
466
- throw new Error('Invalid physicsAssetName: must be a string');
467
- }
468
- if (args.physicsAssetName.trim() === '') {
469
- throw new Error('Invalid physicsAssetName: cannot be empty');
470
- }
471
- // Optional blendWeight validation
472
- if (args.blendWeight !== undefined && args.blendWeight !== null) {
473
- if (typeof args.blendWeight !== 'number') {
474
- throw new Error('Invalid blendWeight: must be a number');
365
+ const location = toVector(args.location, [0, 0, typeKey === 'directional' ? 500 : 0]);
366
+ const rotation = toRotator(args.rotation, [0, 0, 0]);
367
+ const color = toColor(args.color);
368
+ const castShadows = typeof args.castShadows === 'boolean' ? args.castShadows : undefined;
369
+ if (typeKey === 'directional') {
370
+ return cleanObject(await tools.lightingTools.createDirectionalLight({
371
+ name,
372
+ intensity: args.intensity,
373
+ color,
374
+ rotation,
375
+ castShadows,
376
+ temperature: args.temperature
377
+ }));
378
+ }
379
+ if (typeKey === 'point') {
380
+ return cleanObject(await tools.lightingTools.createPointLight({
381
+ name,
382
+ location,
383
+ intensity: args.intensity,
384
+ radius: args.radius,
385
+ color,
386
+ falloffExponent: args.falloffExponent,
387
+ castShadows
388
+ }));
389
+ }
390
+ if (typeKey === 'spot') {
391
+ const innerCone = typeof args.innerCone === 'number' ? args.innerCone : undefined;
392
+ const outerCone = typeof args.outerCone === 'number' ? args.outerCone : undefined;
393
+ if (innerCone !== undefined && outerCone !== undefined && innerCone >= outerCone) {
394
+ throw new Error('innerCone must be less than outerCone');
475
395
  }
476
- if (isNaN(args.blendWeight)) {
477
- throw new Error('Invalid blendWeight: cannot be NaN');
396
+ return cleanObject(await tools.lightingTools.createSpotLight({
397
+ name,
398
+ location,
399
+ rotation,
400
+ intensity: args.intensity,
401
+ innerCone: args.innerCone,
402
+ outerCone: args.outerCone,
403
+ radius: args.radius,
404
+ color,
405
+ castShadows
406
+ }));
407
+ }
408
+ if (typeKey === 'rect') {
409
+ return cleanObject(await tools.lightingTools.createRectLight({
410
+ name,
411
+ location,
412
+ rotation,
413
+ intensity: args.intensity,
414
+ width: args.width,
415
+ height: args.height,
416
+ color
417
+ }));
418
+ }
419
+ if (typeKey === 'sky' || typeKey === 'skylight') {
420
+ return cleanObject(await tools.lightingTools.createSkyLight({
421
+ name,
422
+ sourceType: args.sourceType,
423
+ cubemapPath: args.cubemapPath,
424
+ intensity: args.intensity,
425
+ recapture: args.recapture
426
+ }));
427
+ }
428
+ throw new Error(`Unknown light type: ${lightType}`);
429
+ }
430
+ case 'build_lighting': {
431
+ const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true });
432
+ return cleanObject(res);
433
+ }
434
+ default:
435
+ throw new Error(`Unknown level action: ${args.action}`);
436
+ }
437
+ // 5. ANIMATION & PHYSICS
438
+ case 'animation_physics':
439
+ switch (requireAction(args)) {
440
+ case 'create_animation_bp': {
441
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide details for animation_physics.create_animation_bp', {
442
+ name: {
443
+ type: 'string',
444
+ title: 'Blueprint Name',
445
+ description: 'Name of the Animation Blueprint to create'
446
+ },
447
+ skeletonPath: {
448
+ type: 'string',
449
+ title: 'Skeleton Path',
450
+ description: 'Content path of the skeleton asset to bind'
478
451
  }
479
- if (!isFinite(args.blendWeight)) {
480
- throw new Error('Invalid blendWeight: must be finite');
452
+ });
453
+ const name = requireNonEmptyString(args.name, 'name', 'Invalid name');
454
+ const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
455
+ const res = await tools.animationTools.createAnimationBlueprint({ name, skeletonPath, savePath: args.savePath });
456
+ return cleanObject(res);
457
+ }
458
+ case 'play_montage': {
459
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide playback details for animation_physics.play_montage', {
460
+ actorName: {
461
+ type: 'string',
462
+ title: 'Actor Name',
463
+ description: 'Actor that should play the montage'
464
+ },
465
+ montagePath: {
466
+ type: 'string',
467
+ title: 'Montage Path',
468
+ description: 'Montage or animation asset path to play'
481
469
  }
482
- }
483
- // Optional savePath validation
484
- if (args.savePath !== undefined && args.savePath !== null) {
485
- if (typeof args.savePath !== 'string') {
486
- throw new Error('Invalid savePath: must be a string');
470
+ });
471
+ const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
472
+ const montagePath = args.montagePath || args.animationPath;
473
+ const validatedMontage = requireNonEmptyString(montagePath, 'montagePath', 'Invalid montagePath');
474
+ const res = await tools.animationTools.playAnimation({ actorName, animationType: 'Montage', animationPath: validatedMontage, playRate: args.playRate });
475
+ return cleanObject(res);
476
+ }
477
+ case 'setup_ragdoll': {
478
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide setup details for animation_physics.setup_ragdoll', {
479
+ skeletonPath: {
480
+ type: 'string',
481
+ title: 'Skeleton Path',
482
+ description: 'Content path for the skeleton asset'
483
+ },
484
+ physicsAssetName: {
485
+ type: 'string',
486
+ title: 'Physics Asset Name',
487
+ description: 'Name of the physics asset to apply'
487
488
  }
488
- }
489
- mappedName = 'setup_ragdoll';
490
- mappedArgs = {
491
- skeletonPath: args.skeletonPath,
492
- physicsAssetName: args.physicsAssetName,
493
- blendWeight: args.blendWeight,
494
- savePath: args.savePath
495
- };
496
- break;
489
+ });
490
+ const skeletonPath = requireNonEmptyString(args.skeletonPath, 'skeletonPath', 'Invalid skeletonPath');
491
+ const physicsAssetName = requireNonEmptyString(args.physicsAssetName, 'physicsAssetName', 'Invalid physicsAssetName');
492
+ const res = await tools.physicsTools.setupRagdoll({ skeletonPath, physicsAssetName, blendWeight: args.blendWeight, savePath: args.savePath });
493
+ return cleanObject(res);
494
+ }
497
495
  default:
498
496
  throw new Error(`Unknown animation/physics action: ${args.action}`);
499
497
  }
500
- break;
501
498
  // 6. EFFECTS SYSTEM
502
499
  case 'create_effect':
503
- switch (args.action) {
504
- case 'particle':
505
- mappedName = 'create_particle_effect';
506
- mappedArgs = {
507
- effectType: args.effectType,
508
- name: args.name,
509
- location: args.location
510
- };
511
- break;
512
- case 'niagara':
513
- mappedName = 'spawn_niagara_system';
514
- mappedArgs = {
515
- systemPath: args.systemPath,
516
- location: args.location,
500
+ switch (requireAction(args)) {
501
+ case 'particle': {
502
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the particle effect details for create_effect.particle', {
503
+ effectType: {
504
+ type: 'string',
505
+ title: 'Effect Type',
506
+ description: 'Preset effect type to spawn (e.g., Fire, Smoke)'
507
+ }
508
+ });
509
+ const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters });
510
+ return cleanObject(res);
511
+ }
512
+ case 'niagara': {
513
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the Niagara system path for create_effect.niagara', {
514
+ systemPath: {
515
+ type: 'string',
516
+ title: 'Niagara System Path',
517
+ description: 'Asset path of the Niagara system to spawn'
518
+ }
519
+ });
520
+ const systemPath = requireNonEmptyString(args.systemPath, 'systemPath', 'Invalid systemPath');
521
+ const verifyResult = await tools.bridge.executePythonWithResult(`
522
+ import unreal, json
523
+ path = r"${systemPath}"
524
+ exists = unreal.EditorAssetLibrary.does_asset_exist(path)
525
+ print('RESULT:' + json.dumps({'success': exists, 'exists': exists, 'path': path}))
526
+ `.trim());
527
+ if (!verifyResult?.exists) {
528
+ return cleanObject({ success: false, error: `Niagara system not found at ${systemPath}` });
529
+ }
530
+ const loc = Array.isArray(args.location)
531
+ ? { x: args.location[0], y: args.location[1], z: args.location[2] }
532
+ : args.location || { x: 0, y: 0, z: 0 };
533
+ const res = await tools.niagaraTools.spawnEffect({
534
+ systemPath,
535
+ location: [loc.x ?? 0, loc.y ?? 0, loc.z ?? 0],
536
+ rotation: Array.isArray(args.rotation) ? args.rotation : undefined,
517
537
  scale: args.scale
518
- };
519
- break;
520
- case 'debug_shape':
521
- mappedName = 'draw_debug_shape';
522
- // Convert location object to array for position
523
- const pos = args.location || args.position;
524
- mappedArgs = {
525
- shape: args.shape,
526
- position: pos ? [pos.x || 0, pos.y || 0, pos.z || 0] : [0, 0, 0],
527
- size: args.size,
528
- color: args.color,
529
- duration: args.duration
530
- };
531
- break;
538
+ });
539
+ return cleanObject(res);
540
+ }
541
+ case 'debug_shape': {
542
+ const shapeInput = args.shape ?? 'Sphere';
543
+ const shape = String(shapeInput).trim().toLowerCase();
544
+ const originalShapeLabel = String(shapeInput).trim() || 'shape';
545
+ const loc = args.location || { x: 0, y: 0, z: 0 };
546
+ const size = args.size || 100;
547
+ const color = args.color || [255, 0, 0, 255];
548
+ const duration = args.duration || 5;
549
+ if (shape === 'line') {
550
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
551
+ return cleanObject(await tools.debugTools.drawDebugLine({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
552
+ }
553
+ else if (shape === 'box') {
554
+ const extent = [size, size, size];
555
+ return cleanObject(await tools.debugTools.drawDebugBox({ center: [loc.x, loc.y, loc.z], extent, color, duration }));
556
+ }
557
+ else if (shape === 'sphere') {
558
+ return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
559
+ }
560
+ else if (shape === 'capsule') {
561
+ return cleanObject(await tools.debugTools.drawDebugCapsule({ center: [loc.x, loc.y, loc.z], halfHeight: size, radius: Math.max(10, size / 3), color, duration }));
562
+ }
563
+ else if (shape === 'cone') {
564
+ return cleanObject(await tools.debugTools.drawDebugCone({ origin: [loc.x, loc.y, loc.z], direction: [0, 0, 1], length: size, angleWidth: 0.5, angleHeight: 0.5, color, duration }));
565
+ }
566
+ else if (shape === 'arrow') {
567
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
568
+ return cleanObject(await tools.debugTools.drawDebugArrow({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
569
+ }
570
+ else if (shape === 'point') {
571
+ return cleanObject(await tools.debugTools.drawDebugPoint({ location: [loc.x, loc.y, loc.z], size, color, duration }));
572
+ }
573
+ else if (shape === 'text' || shape === 'string') {
574
+ const text = args.text || 'Debug';
575
+ return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration }));
576
+ }
577
+ // Default fallback
578
+ return cleanObject({ success: false, error: `Unsupported debug shape: ${originalShapeLabel}` });
579
+ }
532
580
  default:
533
581
  throw new Error(`Unknown effect action: ${args.action}`);
534
582
  }
535
- break;
536
583
  // 7. BLUEPRINT MANAGER
537
584
  case 'manage_blueprint':
538
- switch (args.action) {
539
- case 'create':
540
- mappedName = 'create_blueprint';
541
- mappedArgs = {
585
+ switch (requireAction(args)) {
586
+ case 'create': {
587
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide details for manage_blueprint.create', {
588
+ name: {
589
+ type: 'string',
590
+ title: 'Blueprint Name',
591
+ description: 'Name for the new Blueprint asset'
592
+ },
593
+ blueprintType: {
594
+ type: 'string',
595
+ title: 'Blueprint Type',
596
+ description: 'Base type such as Actor, Pawn, Character, etc.'
597
+ }
598
+ });
599
+ const res = await tools.blueprintTools.createBlueprint({
542
600
  name: args.name,
543
- blueprintType: args.blueprintType,
544
- savePath: args.savePath
545
- };
546
- break;
547
- case 'add_component':
548
- mappedName = 'add_blueprint_component';
549
- mappedArgs = {
550
- blueprintName: args.name,
551
- componentType: args.componentType,
552
- componentName: args.componentName
553
- };
554
- break;
601
+ blueprintType: args.blueprintType || 'Actor',
602
+ savePath: args.savePath,
603
+ parentClass: args.parentClass
604
+ });
605
+ return cleanObject(res);
606
+ }
607
+ case 'add_component': {
608
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide details for manage_blueprint.add_component', {
609
+ name: {
610
+ type: 'string',
611
+ title: 'Blueprint Name',
612
+ description: 'Blueprint asset to modify'
613
+ },
614
+ componentType: {
615
+ type: 'string',
616
+ title: 'Component Type',
617
+ description: 'Component class to add (e.g., StaticMeshComponent)'
618
+ },
619
+ componentName: {
620
+ type: 'string',
621
+ title: 'Component Name',
622
+ description: 'Name for the new component'
623
+ }
624
+ });
625
+ const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName });
626
+ return cleanObject(res);
627
+ }
555
628
  default:
556
629
  throw new Error(`Unknown blueprint action: ${args.action}`);
557
630
  }
558
- break;
559
631
  // 8. ENVIRONMENT BUILDER
560
632
  case 'build_environment':
561
- switch (args.action) {
562
- case 'create_landscape':
563
- mappedName = 'create_landscape';
564
- mappedArgs = {
565
- name: args.name,
633
+ switch (requireAction(args)) {
634
+ case 'create_landscape': {
635
+ const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath });
636
+ return cleanObject(res);
637
+ }
638
+ case 'sculpt': {
639
+ const res = await tools.landscapeTools.sculptLandscape({ landscapeName: args.name, tool: args.tool, brushSize: args.brushSize, strength: args.strength });
640
+ return cleanObject(res);
641
+ }
642
+ case 'add_foliage': {
643
+ const res = await tools.foliageTools.addFoliageType({ name: args.name, meshPath: args.meshPath, density: args.density });
644
+ return cleanObject(res);
645
+ }
646
+ case 'paint_foliage': {
647
+ const pos = args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0, 0, 0];
648
+ const res = await tools.foliageTools.paintFoliage({ foliageType: args.foliageType, position: pos, brushSize: args.brushSize, paintDensity: args.paintDensity, eraseMode: args.eraseMode });
649
+ return cleanObject(res);
650
+ }
651
+ case 'create_procedural_terrain': {
652
+ const loc = args.location ? [args.location.x || 0, args.location.y || 0, args.location.z || 0] : [0, 0, 0];
653
+ const res = await tools.buildEnvAdvanced.createProceduralTerrain({
654
+ name: args.name || 'ProceduralTerrain',
655
+ location: loc,
566
656
  sizeX: args.sizeX,
567
657
  sizeY: args.sizeY,
568
- materialPath: args.materialPath
569
- };
570
- break;
571
- case 'sculpt':
572
- mappedName = 'sculpt_landscape';
573
- mappedArgs = {
574
- landscapeName: args.name,
575
- tool: args.tool,
576
- brushSize: args.brushSize,
577
- strength: args.strength
658
+ subdivisions: args.subdivisions,
659
+ heightFunction: args.heightFunction,
660
+ material: args.materialPath
661
+ });
662
+ return cleanObject(res);
663
+ }
664
+ case 'create_procedural_foliage': {
665
+ if (!args.bounds || !args.bounds.location || !args.bounds.size)
666
+ throw new Error('bounds.location and bounds.size are required');
667
+ const bounds = {
668
+ location: [args.bounds.location.x || 0, args.bounds.location.y || 0, args.bounds.location.z || 0],
669
+ size: [args.bounds.size.x || 1000, args.bounds.size.y || 1000, args.bounds.size.z || 100]
578
670
  };
579
- break;
580
- case 'add_foliage':
581
- // Validate foliage creation parameters to avoid bad console commands / engine warnings
582
- if (args.name === undefined || args.name === null || typeof args.name !== 'string' || args.name.trim() === '' || String(args.name).toLowerCase() === 'undefined' || String(args.name).toLowerCase() === 'any') {
583
- throw new Error(`Invalid foliage name: '${args.name}'`);
584
- }
585
- if (args.meshPath === undefined || args.meshPath === null || typeof args.meshPath !== 'string' || args.meshPath.trim() === '' || String(args.meshPath).toLowerCase() === 'undefined') {
586
- throw new Error(`Invalid meshPath: '${args.meshPath}'`);
587
- }
588
- if (args.density !== undefined) {
589
- if (typeof args.density !== 'number' || !isFinite(args.density) || args.density < 0) {
590
- throw new Error(`Invalid density: '${args.density}' (must be non-negative finite number)`);
591
- }
592
- }
593
- mappedName = 'add_foliage_type';
594
- mappedArgs = {
595
- name: args.name,
671
+ const res = await tools.buildEnvAdvanced.createProceduralFoliage({
672
+ name: args.name || 'ProceduralFoliage',
673
+ bounds,
674
+ foliageTypes: args.foliageTypes || [],
675
+ seed: args.seed
676
+ });
677
+ return cleanObject(res);
678
+ }
679
+ case 'add_foliage_instances': {
680
+ if (!args.foliageType)
681
+ throw new Error('foliageType is required');
682
+ if (!Array.isArray(args.transforms))
683
+ throw new Error('transforms array is required');
684
+ const transforms = args.transforms.map(t => ({
685
+ location: [t.location?.x || 0, t.location?.y || 0, t.location?.z || 0],
686
+ rotation: t.rotation ? [t.rotation.pitch || 0, t.rotation.yaw || 0, t.rotation.roll || 0] : undefined,
687
+ scale: t.scale ? [t.scale.x || 1, t.scale.y || 1, t.scale.z || 1] : undefined
688
+ }));
689
+ const res = await tools.buildEnvAdvanced.addFoliageInstances({ foliageType: args.foliageType, transforms });
690
+ return cleanObject(res);
691
+ }
692
+ case 'create_landscape_grass_type': {
693
+ const res = await tools.buildEnvAdvanced.createLandscapeGrassType({
694
+ name: args.name || 'GrassType',
596
695
  meshPath: args.meshPath,
597
- density: args.density
598
- };
599
- break;
600
- case 'paint_foliage':
601
- // Validate paint parameters
602
- if (args.foliageType === undefined || args.foliageType === null || typeof args.foliageType !== 'string' || args.foliageType.trim() === '' || String(args.foliageType).toLowerCase() === 'undefined' || String(args.foliageType).toLowerCase() === 'any') {
603
- throw new Error(`Invalid foliageType: '${args.foliageType}'`);
604
- }
605
- // Convert position object to array if needed
606
- let positionArray;
607
- if (args.position) {
608
- if (Array.isArray(args.position)) {
609
- positionArray = args.position;
610
- }
611
- else if (typeof args.position === 'object') {
612
- positionArray = [args.position.x || 0, args.position.y || 0, args.position.z || 0];
613
- }
614
- else {
615
- positionArray = [0, 0, 0];
616
- }
617
- }
618
- else {
619
- positionArray = [0, 0, 0];
620
- }
621
- // Validate numbers in position
622
- if (!Array.isArray(positionArray) || positionArray.length !== 3 || positionArray.some(v => typeof v !== 'number' || !isFinite(v))) {
623
- throw new Error(`Invalid position: '${JSON.stringify(args.position)}'`);
624
- }
625
- if (args.brushSize !== undefined) {
626
- if (typeof args.brushSize !== 'number' || !isFinite(args.brushSize) || args.brushSize < 0) {
627
- throw new Error(`Invalid brushSize: '${args.brushSize}' (must be non-negative finite number)`);
628
- }
629
- }
630
- mappedName = 'paint_foliage';
631
- mappedArgs = {
632
- foliageType: args.foliageType,
633
- position: positionArray,
634
- brushSize: args.brushSize
635
- };
636
- break;
696
+ density: args.density,
697
+ minScale: args.minScale,
698
+ maxScale: args.maxScale
699
+ });
700
+ return cleanObject(res);
701
+ }
637
702
  default:
638
703
  throw new Error(`Unknown environment action: ${args.action}`);
639
704
  }
640
- break;
641
705
  // 9. SYSTEM CONTROL
642
706
  case 'system_control':
643
- // Validate args is not null/undefined
644
- if (args === null || args === undefined) {
645
- throw new Error('Invalid arguments: null or undefined');
646
- }
647
- if (typeof args !== 'object') {
648
- throw new Error('Invalid arguments: must be an object');
649
- }
650
- // Validate action exists
651
- if (!args.action) {
652
- throw new Error('Missing required parameter: action');
653
- }
654
- switch (args.action) {
655
- case 'profile':
656
- // Validate profile type
657
- const validProfileTypes = ['CPU', 'GPU', 'Memory', 'RenderThread', 'GameThread', 'All'];
658
- if (!args.profileType || !validProfileTypes.includes(args.profileType)) {
659
- throw new Error(`Invalid profileType: '${args.profileType}'. Valid types: ${validProfileTypes.join(', ')}`);
660
- }
661
- mappedName = 'start_profiling';
662
- mappedArgs = {
663
- type: args.profileType,
664
- duration: args.duration
665
- };
666
- break;
667
- case 'show_fps':
668
- // Validate enabled is boolean
669
- if (args.enabled !== undefined && typeof args.enabled !== 'boolean') {
670
- throw new Error(`Invalid enabled: must be boolean, got ${typeof args.enabled}`);
671
- }
672
- mappedName = 'show_fps';
673
- mappedArgs = {
674
- enabled: args.enabled,
675
- verbose: args.verbose
676
- };
677
- break;
678
- case 'set_quality':
679
- // Validate category - normalize aliases and singular forms used by sg.*Quality
680
- const validCategories = ['ViewDistance', 'AntiAliasing', 'PostProcessing', 'PostProcess',
681
- 'Shadows', 'Shadow', 'GlobalIllumination', 'Reflections', 'Reflection', 'Textures', 'Texture',
682
- 'Effects', 'Foliage', 'Shading'];
683
- if (!args.category || !validCategories.includes(args.category)) {
684
- throw new Error(`Invalid category: '${args.category}'. Valid categories: ${validCategories.join(', ')}`);
685
- }
686
- // Validate level
687
- if (args.level === undefined || args.level === null) {
688
- throw new Error('Missing required parameter: level');
689
- }
690
- if (typeof args.level !== 'number' || !Number.isInteger(args.level) || args.level < 0 || args.level > 4) {
691
- throw new Error(`Invalid level: must be integer 0-4, got ${args.level}`);
692
- }
693
- // Normalize category to sg.<Base>Quality base (singular where needed)
694
- const map = {
695
- ViewDistance: 'ViewDistance',
696
- AntiAliasing: 'AntiAliasing',
697
- PostProcessing: 'PostProcess',
698
- PostProcess: 'PostProcess',
699
- Shadows: 'Shadow',
700
- Shadow: 'Shadow',
701
- GlobalIllumination: 'GlobalIllumination',
702
- Reflections: 'Reflection',
703
- Reflection: 'Reflection',
704
- Textures: 'Texture',
705
- Texture: 'Texture',
706
- Effects: 'Effects',
707
- Foliage: 'Foliage',
708
- Shading: 'Shading',
709
- };
710
- const categoryName = map[String(args.category)] || args.category;
711
- mappedName = 'set_scalability';
712
- mappedArgs = {
713
- category: categoryName,
714
- level: args.level
715
- };
716
- break;
717
- case 'play_sound':
718
- // Validate sound path
719
- if (!args.soundPath || typeof args.soundPath !== 'string') {
720
- throw new Error('Invalid soundPath: must be a non-empty string');
721
- }
722
- // Validate volume if provided
723
- if (args.volume !== undefined) {
724
- if (typeof args.volume !== 'number' || args.volume < 0 || args.volume > 1) {
725
- throw new Error(`Invalid volume: must be 0-1, got ${args.volume}`);
707
+ switch (requireAction(args)) {
708
+ case 'profile': {
709
+ const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
710
+ return cleanObject(res);
711
+ }
712
+ case 'show_fps': {
713
+ const res = await tools.performanceTools.showFPS({ enabled: !!args.enabled, verbose: !!args.verbose });
714
+ return cleanObject(res);
715
+ }
716
+ case 'set_quality': {
717
+ const res = await tools.performanceTools.setScalability({ category: args.category, level: args.level });
718
+ return cleanObject(res);
719
+ }
720
+ case 'play_sound': {
721
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide the audio asset for system_control.play_sound', {
722
+ soundPath: {
723
+ type: 'string',
724
+ title: 'Sound Asset Path',
725
+ description: 'Asset path of the sound to play'
726
726
  }
727
- }
728
- mappedName = 'play_sound';
729
- mappedArgs = {
730
- soundPath: args.soundPath,
731
- location: args.location,
732
- volume: args.volume,
733
- is3D: args.is3D
734
- };
735
- break;
736
- case 'create_widget':
737
- // Validate widget name
738
- if (!args.widgetName || typeof args.widgetName !== 'string' || args.widgetName.trim() === '') {
739
- throw new Error('Invalid widgetName: must be a non-empty string');
740
- }
741
- mappedName = 'create_widget';
742
- mappedArgs = {
743
- name: args.widgetName,
744
- type: args.widgetType,
745
- savePath: args.savePath
746
- };
747
- break;
748
- case 'show_widget':
749
- // Validate widget name
750
- if (!args.widgetName || typeof args.widgetName !== 'string') {
751
- throw new Error('Invalid widgetName: must be a non-empty string');
752
- }
753
- // Validate visible is boolean (default to true if not provided)
754
- const isVisible = args.visible !== undefined ? args.visible : true;
755
- if (typeof isVisible !== 'boolean') {
756
- throw new Error(`Invalid visible: must be boolean, got ${typeof isVisible}`);
757
- }
758
- mappedName = 'show_widget';
759
- mappedArgs = {
760
- widgetName: args.widgetName,
761
- visible: isVisible
762
- };
763
- break;
764
- case 'screenshot':
765
- mappedName = 'take_screenshot';
766
- mappedArgs = { resolution: args.resolution };
767
- break;
768
- case 'engine_start':
769
- mappedName = 'launch_editor';
770
- mappedArgs = { editorExe: args.editorExe, projectPath: args.projectPath };
771
- break;
772
- case 'engine_quit':
773
- mappedName = 'quit_editor';
774
- mappedArgs = {};
775
- break;
727
+ });
728
+ const soundPath = requireNonEmptyString(args.soundPath, 'soundPath', 'Missing required parameter: soundPath');
729
+ if (args.location && typeof args.location === 'object') {
730
+ const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0];
731
+ const res = await tools.audioTools.playSoundAtLocation({ soundPath, location: loc, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
732
+ return cleanObject(res);
733
+ }
734
+ const res = await tools.audioTools.playSound2D({ soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
735
+ return cleanObject(res);
736
+ }
737
+ case 'create_widget': {
738
+ await elicitMissingPrimitiveArgs(tools, args, 'Provide details for system_control.create_widget', {
739
+ widgetName: {
740
+ type: 'string',
741
+ title: 'Widget Name',
742
+ description: 'Name for the new UI widget asset'
743
+ },
744
+ widgetType: {
745
+ type: 'string',
746
+ title: 'Widget Type',
747
+ description: 'Widget type such as HUD, Menu, Overlay, etc.'
748
+ }
749
+ });
750
+ const widgetName = requireNonEmptyString(args.widgetName ?? args.name, 'widgetName', 'Missing required parameter: widgetName');
751
+ const widgetType = requireNonEmptyString(args.widgetType, 'widgetType', 'Missing required parameter: widgetType');
752
+ const res = await tools.uiTools.createWidget({ name: widgetName, type: widgetType, savePath: args.savePath });
753
+ return cleanObject(res);
754
+ }
755
+ case 'show_widget': {
756
+ const res = await tools.uiTools.setWidgetVisibility({ widgetName: args.widgetName, visible: args.visible !== false });
757
+ return cleanObject(res);
758
+ }
759
+ case 'screenshot': {
760
+ const res = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
761
+ return cleanObject(res);
762
+ }
763
+ case 'engine_start': {
764
+ const res = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
765
+ return cleanObject(res);
766
+ }
767
+ case 'engine_quit': {
768
+ const res = await tools.engineTools.quitEditor();
769
+ return cleanObject(res);
770
+ }
776
771
  default:
777
772
  throw new Error(`Unknown system action: ${args.action}`);
778
773
  }
779
- break;
780
774
  // 10. CONSOLE COMMAND - handle validation here
781
775
  case 'console_command':
782
- // Handle empty/invalid commands gracefully
783
- if (args.command === undefined || args.command === null || args.command === '' || typeof args.command !== 'string') {
784
- return {
785
- content: [{
786
- type: 'text',
787
- text: 'Empty command ignored'
788
- }],
789
- isError: false,
790
- success: true,
791
- message: 'Empty command'
792
- };
776
+ if (!args.command || typeof args.command !== 'string' || args.command.trim() === '') {
777
+ return { success: true, message: 'Empty command' };
778
+ }
779
+ // Basic safety filter
780
+ const cmd = String(args.command).trim();
781
+ const blocked = [/\bquit\b/i, /\bexit\b/i, /debugcrash/i];
782
+ if (blocked.some(r => r.test(cmd))) {
783
+ return { success: false, error: 'Command blocked for safety' };
793
784
  }
794
- const cmdTrimmed = args.command.trim();
795
- if (cmdTrimmed.length === 0) {
796
- return {
797
- content: [{
798
- type: 'text',
799
- text: 'Empty command ignored'
800
- }],
801
- isError: false,
802
- success: true,
803
- message: 'Empty command'
804
- };
785
+ try {
786
+ const raw = await tools.bridge.executeConsoleCommand(cmd);
787
+ const summary = tools.bridge.summarizeConsoleCommand(cmd, raw);
788
+ const output = summary.output || '';
789
+ const looksInvalid = /unknown|invalid/i.test(output);
790
+ return cleanObject({
791
+ success: summary.returnValue !== false && !looksInvalid,
792
+ command: summary.command,
793
+ output: output || undefined,
794
+ logLines: summary.logLines?.length ? summary.logLines : undefined,
795
+ returnValue: summary.returnValue,
796
+ message: !looksInvalid
797
+ ? (output || 'Command executed')
798
+ : undefined,
799
+ error: looksInvalid ? output : undefined,
800
+ raw: summary.raw
801
+ });
802
+ }
803
+ catch (e) {
804
+ return cleanObject({ success: false, command: cmd, error: e?.message || String(e) });
805
805
  }
806
- mappedName = 'console_command';
807
- mappedArgs = args;
808
- break;
809
806
  // 11. REMOTE CONTROL PRESETS - Direct implementation
810
807
  case 'manage_rc':
811
- if (!args.action)
812
- throw new Error('Missing required parameter: action');
813
808
  // Handle RC operations directly through RcTools
814
809
  let rcResult;
815
- switch (args.action) {
810
+ const rcAction = requireAction(args);
811
+ switch (rcAction) {
816
812
  // Support both 'create_preset' and 'create' for compatibility
817
813
  case 'create_preset':
818
814
  case 'create':
@@ -838,9 +834,11 @@ export async function handleConsolidatedToolCall(name, args, tools) {
838
834
  rcResult = await tools.rcTools.listPresets();
839
835
  break;
840
836
  case 'delete':
841
- if (!args.presetId)
837
+ case 'delete_preset':
838
+ const presetIdentifier = args.presetId || args.presetPath;
839
+ if (!presetIdentifier)
842
840
  throw new Error('Missing required parameter: presetId');
843
- rcResult = await tools.rcTools.deletePreset(args.presetId);
841
+ rcResult = await tools.rcTools.deletePreset(presetIdentifier);
844
842
  if (rcResult.success) {
845
843
  rcResult.message = 'Preset deleted successfully';
846
844
  }
@@ -935,21 +933,20 @@ export async function handleConsolidatedToolCall(name, args, tools) {
935
933
  };
936
934
  break;
937
935
  default:
938
- 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`);
936
+ 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`);
939
937
  }
940
938
  // Return result directly - MCP formatting will be handled by response validator
941
939
  // Clean to prevent circular references
942
940
  return cleanObject(rcResult);
943
941
  // 12. SEQUENCER / CINEMATICS
944
942
  case 'manage_sequence':
945
- if (!args.action)
946
- throw new Error('Missing required parameter: action');
947
943
  // Direct handling for sequence operations
948
944
  const seqResult = await (async () => {
949
945
  const sequenceTools = tools.sequenceTools;
950
946
  if (!sequenceTools)
951
947
  throw new Error('Sequence tools not available');
952
- switch (args.action) {
948
+ const action = requireAction(args);
949
+ switch (action) {
953
950
  case 'create':
954
951
  return await sequenceTools.create({ name: args.name, path: args.path });
955
952
  case 'open':
@@ -993,7 +990,7 @@ export async function handleConsolidatedToolCall(name, args, tools) {
993
990
  throw new Error('Missing required parameter: speed');
994
991
  return await sequenceTools.setPlaybackSpeed({ speed: args.speed });
995
992
  default:
996
- throw new Error(`Unknown sequence action: ${args.action}`);
993
+ throw new Error(`Unknown sequence action: ${action}`);
997
994
  }
998
995
  })();
999
996
  // Return result directly - MCP formatting will be handled by response validator
@@ -1001,37 +998,23 @@ export async function handleConsolidatedToolCall(name, args, tools) {
1001
998
  return cleanObject(seqResult);
1002
999
  // 13. INTROSPECTION
1003
1000
  case 'inspect':
1004
- if (!args.action)
1005
- throw new Error('Missing required parameter: action');
1006
- switch (args.action) {
1007
- case 'inspect_object':
1008
- mappedName = 'inspect_object';
1009
- mappedArgs = { objectPath: args.objectPath };
1010
- break;
1011
- case 'set_property':
1012
- mappedName = 'inspect_set_property';
1013
- mappedArgs = { objectPath: args.objectPath, propertyName: args.propertyName, value: args.value };
1014
- break;
1001
+ const inspectAction = requireAction(args);
1002
+ switch (inspectAction) {
1003
+ case 'inspect_object': {
1004
+ const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed });
1005
+ return cleanObject(res);
1006
+ }
1007
+ case 'set_property': {
1008
+ const res = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
1009
+ return cleanObject(res);
1010
+ }
1015
1011
  default:
1016
- throw new Error(`Unknown inspect action: ${args.action}`);
1012
+ throw new Error(`Unknown inspect action: ${inspectAction}`);
1017
1013
  }
1018
- break;
1019
1014
  default:
1020
1015
  throw new Error(`Unknown consolidated tool: ${name}`);
1021
1016
  }
1022
- // Call the original handler with mapped name and args with timeout
1023
- const TOOL_TIMEOUT = 15000; // 15 seconds timeout for tool execution
1024
- const toolPromise = handleToolCall(mappedName, mappedArgs, tools);
1025
- const timeoutPromise = new Promise((_, reject) => {
1026
- setTimeout(() => {
1027
- reject(new Error(`Tool execution timeout after ${TOOL_TIMEOUT}ms`));
1028
- }, TOOL_TIMEOUT);
1029
- });
1030
- const result = await Promise.race([toolPromise, timeoutPromise]);
1031
- const duration = Date.now() - startTime;
1032
- console.log(`[ConsolidatedToolHandler] Completed execution of ${name} in ${duration}ms`);
1033
- // Clean the result to prevent circular reference errors
1034
- return cleanObject(result);
1017
+ // All cases return (or throw) above; this is a type guard for exhaustiveness.
1035
1018
  }
1036
1019
  catch (err) {
1037
1020
  const duration = Date.now() - startTime;