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,10 +1,132 @@
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
 
6
5
  const log = new Logger('ConsolidatedToolHandler');
7
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
+
8
130
  export async function handleConsolidatedToolCall(
9
131
  name: string,
10
132
  args: any,
@@ -15,842 +137,810 @@ export async function handleConsolidatedToolCall(
15
137
  log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
16
138
 
17
139
  try {
18
- // Validate args is not null/undefined
19
- if (args === null || args === undefined) {
20
- throw new Error('Invalid arguments: null or undefined');
21
- }
22
-
23
- let mappedName: string;
24
- let mappedArgs: any = { ...args };
140
+ ensureArgsPresent(args);
25
141
 
26
142
  switch (name) {
27
143
  // 1. ASSET MANAGER
28
144
  case 'manage_asset':
29
- // Validate args is not null/undefined
30
- if (args === null || args === undefined) {
31
- throw new Error('Invalid arguments: null or undefined');
32
- }
33
-
34
- // Validate action exists
35
- if (!args.action) {
36
- throw new Error('Missing required parameter: action');
37
- }
38
-
39
- switch (args.action) {
40
- case 'list':
41
- // Directory is optional
42
- if (args.directory !== undefined && args.directory !== null) {
43
- if (typeof args.directory !== 'string') {
44
- throw new Error('Invalid directory: must be a string');
45
- }
46
- }
47
-
48
- mappedName = 'list_assets';
49
- mappedArgs = {
50
- directory: args.directory
51
- // recursive removed - always false internally
52
- };
53
- break;
54
-
55
- case 'import':
56
- // Validate required parameters
57
- if (args.sourcePath === undefined || args.sourcePath === null) {
58
- throw new Error('Missing required parameter: sourcePath');
59
- }
60
- if (typeof args.sourcePath !== 'string') {
61
- throw new Error('Invalid sourcePath: must be a string');
62
- }
63
- if (args.sourcePath.trim() === '') {
64
- throw new Error('Invalid sourcePath: cannot be empty');
65
- }
66
-
67
- if (args.destinationPath === undefined || args.destinationPath === null) {
68
- throw new Error('Missing required parameter: destinationPath');
69
- }
70
- if (typeof args.destinationPath !== 'string') {
71
- throw new Error('Invalid destinationPath: must be a string');
72
- }
73
- if (args.destinationPath.trim() === '') {
74
- throw new Error('Invalid destinationPath: cannot be empty');
145
+ switch (requireAction(args)) {
146
+ case 'list': {
147
+ if (args.directory !== undefined && args.directory !== null && typeof args.directory !== 'string') {
148
+ throw new Error('Invalid directory: must be a string');
75
149
  }
76
-
77
- mappedName = 'import_asset';
78
- mappedArgs = {
79
- sourcePath: args.sourcePath,
80
- destinationPath: args.destinationPath
81
- };
82
- break;
83
-
84
- case 'create_material':
85
- // Validate required parameters
86
- if (args.name === undefined || args.name === null) {
87
- throw new Error('Missing required parameter: name');
88
- }
89
- if (typeof args.name !== 'string') {
90
- throw new Error('Invalid name: must be a string');
91
- }
92
- if (args.name.trim() === '') {
93
- throw new Error('Invalid name: cannot be empty');
94
- }
95
-
96
- // Path is optional
97
- if (args.path !== undefined && args.path !== null) {
98
- if (typeof args.path !== 'string') {
99
- throw new Error('Invalid path: must be a string');
150
+ const res = await tools.assetResources.list(args.directory || '/Game', false);
151
+ return cleanObject({ success: true, ...res });
152
+ }
153
+ case 'import': {
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
+ }
100
199
  }
101
200
  }
102
-
103
- mappedName = 'create_material';
104
- mappedArgs = {
105
- name: args.name,
106
- path: args.path
107
- };
108
- break;
109
-
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);
205
+ return cleanObject(res);
206
+ }
207
+ case 'create_material': {
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');
229
+ return cleanObject(res);
230
+ }
110
231
  default:
111
232
  throw new Error(`Unknown asset action: ${args.action}`);
112
233
  }
113
- break;
114
234
 
115
235
  // 2. ACTOR CONTROL
116
236
  case 'control_actor':
117
- // Validate action exists
118
- if (!args.action) {
119
- throw new Error('Missing required parameter: action');
120
- }
121
-
122
- switch (args.action) {
123
- case 'spawn':
124
- // Validate spawn parameters
125
- if (!args.classPath) {
126
- throw new Error('Missing required parameter: classPath');
127
- }
128
- if (typeof args.classPath !== 'string' || args.classPath.trim() === '') {
129
- throw new Error('Invalid classPath: must be a non-empty string');
130
- }
131
-
132
- mappedName = 'spawn_actor';
133
- mappedArgs = {
134
- classPath: args.classPath,
237
+ switch (requireAction(args)) {
238
+ case 'spawn': {
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);
256
+ const res = await tools.actorTools.spawn({
257
+ classPath,
135
258
  location: args.location,
136
- rotation: args.rotation
137
- };
138
- break;
139
-
140
- case 'delete':
141
- // Validate delete parameters
142
- if (!args.actorName) {
143
- throw new Error('Missing required parameter: actorName');
144
- }
145
- if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
146
- throw new Error('Invalid actorName: must be a non-empty string');
147
- }
148
-
149
- mappedName = 'delete_actor';
150
- mappedArgs = {
151
- actorName: args.actorName
152
- };
153
- break;
154
-
155
- case 'apply_force':
156
- // Validate apply_force parameters
157
- if (!args.actorName) {
158
- throw new Error('Missing required parameter: actorName');
159
- }
160
- if (typeof args.actorName !== 'string' || args.actorName.trim() === '') {
161
- throw new Error('Invalid actorName: must be a non-empty string');
162
- }
163
- if (!args.force) {
164
- throw new Error('Missing required parameter: force');
165
- }
166
- if (typeof args.force !== 'object' || args.force === null) {
167
- throw new Error('Invalid force: must be an object with x, y, z properties');
168
- }
169
- if (typeof args.force.x !== 'number' ||
170
- typeof args.force.y !== 'number' ||
171
- typeof args.force.z !== 'number') {
172
- throw new Error('Invalid force: x, y, z must all be numbers');
173
- }
174
-
175
- mappedName = 'apply_force';
176
- mappedArgs = {
177
- actorName: args.actorName,
178
- force: args.force
179
- };
180
- break;
181
-
259
+ rotation: args.rotation,
260
+ actorName: actorNameInput
261
+ });
262
+ return cleanObject(res);
263
+ }
264
+ case 'delete': {
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 });
282
+ return cleanObject(res);
283
+ }
284
+ case 'apply_force': {
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');
299
+ const res = await tools.physicsTools.applyForce({
300
+ actorName,
301
+ forceType: 'Force',
302
+ vector
303
+ });
304
+ return cleanObject(res);
305
+ }
182
306
  default:
183
307
  throw new Error(`Unknown actor action: ${args.action}`);
184
308
  }
185
- break;
186
309
 
187
310
  // 3. EDITOR CONTROL
188
311
  case 'control_editor':
189
- // Validate action exists
190
- if (!args.action) {
191
- throw new Error('Missing required parameter: action');
192
- }
193
-
194
- switch (args.action) {
195
- case 'play':
196
- mappedName = 'play_in_editor';
197
- mappedArgs = {};
198
- break;
199
- case 'stop':
200
- mappedName = 'stop_play_in_editor';
201
- mappedArgs = {};
202
- break;
203
- case 'pause':
204
- mappedName = 'pause_play_in_editor';
205
- mappedArgs = {};
206
- break;
207
- case 'set_game_speed':
208
- // Validate game speed parameter
209
- if (args.speed === undefined || args.speed === null) {
210
- throw new Error('Missing required parameter: speed');
211
- }
212
- if (typeof args.speed !== 'number') {
213
- throw new Error('Invalid speed: must be a number');
214
- }
215
- if (isNaN(args.speed)) {
216
- throw new Error('Invalid speed: cannot be NaN');
217
- }
218
- if (!isFinite(args.speed)) {
219
- throw new Error('Invalid speed: must be finite');
220
- }
221
- if (args.speed <= 0) {
222
- throw new Error('Invalid speed: must be positive');
223
- }
224
- mappedName = 'set_game_speed';
225
- mappedArgs = {
226
- speed: args.speed
227
- };
228
- break;
229
- case 'eject':
230
- mappedName = 'eject_from_pawn';
231
- mappedArgs = {};
232
- break;
233
- case 'possess':
234
- mappedName = 'possess_pawn';
235
- mappedArgs = {};
236
- break;
237
- case 'set_camera':
238
- // Allow either location or rotation or both
239
- // Don't require both to be present
240
- mappedName = 'set_camera';
241
- mappedArgs = {
242
- location: args.location,
243
- rotation: args.rotation
244
- };
245
- break;
246
- case 'set_view_mode':
247
- // Validate view mode parameter
248
- if (!args.viewMode) {
249
- throw new Error('Missing required parameter: viewMode');
250
- }
251
- if (typeof args.viewMode !== 'string' || args.viewMode.trim() === '') {
252
- throw new Error('Invalid viewMode: must be a non-empty string');
253
- }
254
-
255
- // Normalize view mode to match what debug.ts expects
256
- const validModes = ['lit', 'unlit', 'wireframe', 'detail_lighting', 'lighting_only',
257
- 'light_complexity', 'shader_complexity', 'lightmap_density',
258
- 'stationary_light_overlap', 'reflections', 'visualize_buffer',
259
- 'collision_pawn', 'collision_visibility', 'lod_coloration', 'quad_overdraw'];
260
- const normalizedMode = args.viewMode.toLowerCase().replace(/_/g, '');
261
-
262
- // Map to proper case for debug.ts
263
- let mappedMode = '';
264
- switch(normalizedMode) {
265
- case 'lit': mappedMode = 'Lit'; break;
266
- case 'unlit': mappedMode = 'Unlit'; break;
267
- case 'wireframe': mappedMode = 'Wireframe'; break;
268
- case 'detaillighting': mappedMode = 'DetailLighting'; break;
269
- case 'lightingonly': mappedMode = 'LightingOnly'; break;
270
- case 'lightcomplexity': mappedMode = 'LightComplexity'; break;
271
- case 'shadercomplexity': mappedMode = 'ShaderComplexity'; break;
272
- case 'lightmapdensity': mappedMode = 'LightmapDensity'; break;
273
- case 'stationarylightoverlap': mappedMode = 'StationaryLightOverlap'; break;
274
- case 'reflections': mappedMode = 'ReflectionOverride'; break;
275
- case 'visualizebuffer': mappedMode = 'VisualizeBuffer'; break;
276
- case 'collisionpawn': mappedMode = 'CollisionPawn'; break;
277
- case 'collisionvisibility': mappedMode = 'CollisionVisibility'; break;
278
- case 'lodcoloration': mappedMode = 'LODColoration'; break;
279
- case 'quadoverdraw': mappedMode = 'QuadOverdraw'; break;
280
- default:
281
- throw new Error(`Invalid viewMode: '${args.viewMode}'. Valid modes are: ${validModes.join(', ')}`);
282
- }
283
-
284
- mappedName = 'set_view_mode';
285
- mappedArgs = {
286
- mode: mappedMode
287
- };
288
- break;
312
+ switch (requireAction(args)) {
313
+ case 'play': {
314
+ const res = await tools.editorTools.playInEditor();
315
+ return cleanObject(res);
316
+ }
317
+ case 'stop': {
318
+ const res = await tools.editorTools.stopPlayInEditor();
319
+ return cleanObject(res);
320
+ }
321
+ case 'pause': {
322
+ const res = await tools.editorTools.pausePlayInEditor();
323
+ return cleanObject(res);
324
+ }
325
+ case 'set_game_speed': {
326
+ const speed = requirePositiveNumber(args.speed, 'speed', 'Invalid speed: must be a positive number');
327
+ // Use console command via bridge
328
+ const res = await tools.bridge.executeConsoleCommand(`slomo ${speed}`);
329
+ return cleanObject(res);
330
+ }
331
+ case 'eject': {
332
+ const res = await tools.bridge.executeConsoleCommand('eject');
333
+ return cleanObject(res);
334
+ }
335
+ case 'possess': {
336
+ const res = await tools.bridge.executeConsoleCommand('viewself');
337
+ return cleanObject(res);
338
+ }
339
+ case 'set_camera': {
340
+ const res = await tools.editorTools.setViewportCamera(args.location, args.rotation);
341
+ return cleanObject(res);
342
+ }
343
+ case 'set_view_mode': {
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);
358
+ return cleanObject(res);
359
+ }
289
360
  default:
290
361
  throw new Error(`Unknown editor action: ${args.action}`);
291
362
  }
292
- break;
293
363
 
294
364
  // 4. LEVEL MANAGER
295
- case 'manage_level':
296
- switch (args.action) {
297
- case 'load':
298
- mappedName = 'load_level';
299
- mappedArgs = {
300
- levelPath: args.levelPath
301
- };
302
- if (args.streaming !== undefined) {
303
- mappedArgs.streaming = args.streaming;
304
- }
305
- break;
306
- case 'save':
307
- mappedName = 'save_level';
308
- mappedArgs = {
309
- levelName: args.levelName
310
- };
311
- if (args.savePath !== undefined) {
312
- mappedArgs.savePath = args.savePath;
313
- }
314
- break;
315
- case 'stream':
316
- mappedName = 'stream_level';
317
- mappedArgs = {
318
- levelName: args.levelName,
319
- shouldBeLoaded: args.shouldBeLoaded,
320
- shouldBeVisible: args.shouldBeVisible
321
- };
322
- break;
323
- case 'create_light':
324
- // Validate light type
325
- if (!args.lightType) {
326
- throw new Error('Missing required parameter: lightType');
327
- }
328
- const validLightTypes = ['directional', 'point', 'spot', 'rect', 'sky'];
329
- const normalizedLightType = String(args.lightType).toLowerCase();
330
- if (!validLightTypes.includes(normalizedLightType)) {
331
- throw new Error(`Invalid lightType: '${args.lightType}'. Valid types are: ${validLightTypes.join(', ')}`);
332
- }
333
-
334
- // Validate name
335
- if (!args.name) {
336
- throw new Error('Missing required parameter: name');
337
- }
338
- if (typeof args.name !== 'string' || args.name.trim() === '') {
339
- throw new Error('Invalid name: must be a non-empty string');
340
- }
341
-
342
- // Validate intensity if provided
343
- if (args.intensity !== undefined) {
344
- if (typeof args.intensity !== 'number' || !isFinite(args.intensity)) {
345
- throw new Error(`Invalid intensity: must be a finite number, got ${typeof args.intensity}`);
365
+ case 'manage_level':
366
+ switch (requireAction(args)) {
367
+ case 'load': {
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
+ }
346
378
  }
347
- if (args.intensity < 0) {
348
- throw new Error('Invalid intensity: must be non-negative');
379
+ );
380
+ const levelPath = requireNonEmptyString(args.levelPath, 'levelPath', 'Missing required parameter: levelPath');
381
+ const res = await tools.levelTools.loadLevel({ levelPath, streaming: !!args.streaming });
382
+ return cleanObject(res);
383
+ }
384
+ case 'save': {
385
+ const res = await tools.levelTools.saveLevel({ levelName: args.levelName, savePath: args.savePath });
386
+ return cleanObject(res);
387
+ }
388
+ case 'stream': {
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
+ }
349
399
  }
350
- }
351
-
352
- // Validate location if provided
353
- if (args.location !== undefined && args.location !== null) {
354
- if (!Array.isArray(args.location) && typeof args.location !== 'object') {
355
- throw new Error('Invalid location: must be an array [x,y,z] or object {x,y,z}');
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 });
403
+ return cleanObject(res);
404
+ }
405
+ case 'create_light': {
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
+ }
356
421
  }
357
- }
358
-
359
- mappedName = 'create_light';
360
- mappedArgs = {
361
- lightType: args.lightType,
362
- name: args.name,
363
- location: args.location,
364
- intensity: args.intensity
365
- };
366
- break;
367
- case 'build_lighting':
368
- mappedName = 'build_lighting';
369
- mappedArgs = {
370
- quality: args.quality
371
- };
372
- break;
373
- default:
374
- throw new Error(`Unknown level action: ${args.action}`);
375
- }
376
- break;
377
-
378
- // 5. ANIMATION & PHYSICS
379
- case 'animation_physics':
380
- // Validate action exists
381
- if (!args.action) {
382
- throw new Error('Missing required parameter: action');
383
- }
384
-
385
- switch (args.action) {
386
- case 'create_animation_bp':
387
- // Validate required parameters
388
- if (args.name === undefined || args.name === null) {
389
- throw new Error('Missing required parameter: name');
390
- }
391
- if (typeof args.name !== 'string') {
392
- throw new Error('Invalid name: must be a string');
393
- }
394
- if (args.name.trim() === '') {
395
- throw new Error('Invalid name: cannot be empty');
396
- }
397
-
398
- if (args.skeletonPath === undefined || args.skeletonPath === null) {
399
- throw new Error('Missing required parameter: skeletonPath');
400
- }
401
- if (typeof args.skeletonPath !== 'string') {
402
- throw new Error('Invalid skeletonPath: must be a string');
403
- }
404
- if (args.skeletonPath.trim() === '') {
405
- throw new Error('Invalid skeletonPath: cannot be empty');
406
- }
407
-
408
- // Optional savePath validation
409
- if (args.savePath !== undefined && args.savePath !== null) {
410
- if (typeof args.savePath !== 'string') {
411
- throw new Error('Invalid savePath: must be a string');
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];
412
429
  }
413
- }
414
-
415
- mappedName = 'create_animation_blueprint';
416
- mappedArgs = {
417
- name: args.name,
418
- skeletonPath: args.skeletonPath,
419
- savePath: args.savePath
430
+ if (value && typeof value === 'object') {
431
+ return [Number(value.x) || 0, Number(value.y) || 0, Number(value.z) || 0];
432
+ }
433
+ return fallback;
420
434
  };
421
- break;
422
-
423
- case 'play_montage':
424
- // Validate required parameters
425
- if (args.actorName === undefined || args.actorName === null) {
426
- throw new Error('Missing required parameter: actorName');
427
- }
428
- if (typeof args.actorName !== 'string') {
429
- throw new Error('Invalid actorName: must be a string');
430
- }
431
- if (args.actorName.trim() === '') {
432
- throw new Error('Invalid actorName: cannot be empty');
433
- }
434
-
435
- // Check for montagePath or animationPath
436
- const montagePath = args.montagePath || args.animationPath;
437
- if (montagePath === undefined || montagePath === null) {
438
- throw new Error('Missing required parameter: montagePath or animationPath');
439
- }
440
- if (typeof montagePath !== 'string') {
441
- throw new Error('Invalid montagePath: must be a string');
442
- }
443
- if (montagePath.trim() === '') {
444
- throw new Error('Invalid montagePath: cannot be empty');
445
- }
446
-
447
- // Optional playRate validation
448
- if (args.playRate !== undefined && args.playRate !== null) {
449
- if (typeof args.playRate !== 'number') {
450
- throw new Error('Invalid playRate: must be a number');
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];
451
438
  }
452
- if (isNaN(args.playRate)) {
453
- throw new Error('Invalid playRate: cannot be NaN');
439
+ if (value && typeof value === 'object') {
440
+ return [Number(value.pitch) || 0, Number(value.yaw) || 0, Number(value.roll) || 0];
454
441
  }
455
- if (!isFinite(args.playRate)) {
456
- throw new Error('Invalid playRate: must be finite');
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];
457
447
  }
458
- }
459
-
460
- mappedName = 'play_animation_montage';
461
- mappedArgs = {
462
- actorName: args.actorName,
463
- montagePath: montagePath,
464
- playRate: args.playRate
448
+ if (value && typeof value === 'object') {
449
+ return [Number(value.r) || 0, Number(value.g) || 0, Number(value.b) || 0];
450
+ }
451
+ return undefined;
465
452
  };
466
- break;
467
-
468
- case 'setup_ragdoll':
469
- // Validate required parameters
470
- if (args.skeletonPath === undefined || args.skeletonPath === null) {
471
- throw new Error('Missing required parameter: skeletonPath');
472
- }
473
- if (typeof args.skeletonPath !== 'string') {
474
- throw new Error('Invalid skeletonPath: must be a string');
475
- }
476
- if (args.skeletonPath.trim() === '') {
477
- throw new Error('Invalid skeletonPath: cannot be empty');
478
- }
479
-
480
- if (args.physicsAssetName === undefined || args.physicsAssetName === null) {
481
- throw new Error('Missing required parameter: physicsAssetName');
482
- }
483
- if (typeof args.physicsAssetName !== 'string') {
484
- throw new Error('Invalid physicsAssetName: must be a string');
485
- }
486
- if (args.physicsAssetName.trim() === '') {
487
- throw new Error('Invalid physicsAssetName: cannot be empty');
488
- }
489
-
490
- // Optional blendWeight validation
491
- if (args.blendWeight !== undefined && args.blendWeight !== null) {
492
- if (typeof args.blendWeight !== 'number') {
493
- throw new Error('Invalid blendWeight: must be a number');
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');
494
485
  }
495
- if (isNaN(args.blendWeight)) {
496
- throw new Error('Invalid blendWeight: cannot be NaN');
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}`);
519
+ }
520
+ case 'build_lighting': {
521
+ const res = await tools.lightingTools.buildLighting({ quality: args.quality || 'High', buildReflectionCaptures: true });
522
+ return cleanObject(res);
523
+ }
524
+ default:
525
+ throw new Error(`Unknown level action: ${args.action}`);
526
+ }
527
+
528
+ // 5. ANIMATION & PHYSICS
529
+ case 'animation_physics':
530
+ switch (requireAction(args)) {
531
+ case 'create_animation_bp': {
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
+ }
497
547
  }
498
- if (!isFinite(args.blendWeight)) {
499
- throw new Error('Invalid blendWeight: must be finite');
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 });
552
+ return cleanObject(res);
553
+ }
554
+ case 'play_montage': {
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
+ }
500
570
  }
501
- }
502
-
503
- // Optional savePath validation
504
- if (args.savePath !== undefined && args.savePath !== null) {
505
- if (typeof args.savePath !== 'string') {
506
- throw new Error('Invalid savePath: must be a string');
571
+ );
572
+ const actorName = requireNonEmptyString(args.actorName, 'actorName', 'Invalid actorName');
573
+ const montagePath = args.montagePath || args.animationPath;
574
+ const validatedMontage = requireNonEmptyString(montagePath, 'montagePath', 'Invalid montagePath');
575
+ const res = await tools.animationTools.playAnimation({ actorName, animationType: 'Montage', animationPath: validatedMontage, playRate: args.playRate });
576
+ return cleanObject(res);
577
+ }
578
+ case 'setup_ragdoll': {
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
+ }
507
594
  }
508
- }
509
-
510
- mappedName = 'setup_ragdoll';
511
- mappedArgs = {
512
- skeletonPath: args.skeletonPath,
513
- physicsAssetName: args.physicsAssetName,
514
- blendWeight: args.blendWeight,
515
- savePath: args.savePath
516
- };
517
- break;
518
-
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 });
599
+ return cleanObject(res);
600
+ }
519
601
  default:
520
602
  throw new Error(`Unknown animation/physics action: ${args.action}`);
521
603
  }
522
- break;
523
604
 
524
- // 6. EFFECTS SYSTEM
605
+ // 6. EFFECTS SYSTEM
525
606
  case 'create_effect':
526
- switch (args.action) {
527
- case 'particle':
528
- mappedName = 'create_particle_effect';
529
- mappedArgs = {
530
- effectType: args.effectType,
531
- name: args.name,
532
- location: args.location
533
- };
534
- break;
535
- case 'niagara':
536
- mappedName = 'spawn_niagara_system';
537
- mappedArgs = {
538
- systemPath: args.systemPath,
539
- location: args.location,
607
+ switch (requireAction(args)) {
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
+ );
621
+ const res = await tools.niagaraTools.createEffect({ effectType: args.effectType, name: args.name, location: args.location, scale: args.scale, customParameters: args.customParameters });
622
+ return cleanObject(res);
623
+ }
624
+ case '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,
540
654
  scale: args.scale
541
- };
542
- break;
543
- case 'debug_shape':
544
- mappedName = 'draw_debug_shape';
545
- // Convert location object to array for position
546
- const pos = args.location || args.position;
547
- mappedArgs = {
548
- shape: args.shape,
549
- position: pos ? [pos.x || 0, pos.y || 0, pos.z || 0] : [0, 0, 0],
550
- size: args.size,
551
- color: args.color,
552
- duration: args.duration
553
- };
554
- break;
655
+ });
656
+ return cleanObject(res);
657
+ }
658
+ case 'debug_shape': {
659
+ const shapeInput = args.shape ?? 'Sphere';
660
+ const shape = String(shapeInput).trim().toLowerCase();
661
+ const originalShapeLabel = String(shapeInput).trim() || 'shape';
662
+ const loc = args.location || { x: 0, y: 0, z: 0 };
663
+ const size = args.size || 100;
664
+ const color = args.color || [255, 0, 0, 255];
665
+ const duration = args.duration || 5;
666
+ if (shape === 'line') {
667
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
668
+ return cleanObject(await tools.debugTools.drawDebugLine({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
669
+ } else if (shape === 'box') {
670
+ const extent = [size, size, size];
671
+ return cleanObject(await tools.debugTools.drawDebugBox({ center: [loc.x, loc.y, loc.z], extent, color, duration }));
672
+ } else if (shape === 'sphere') {
673
+ return cleanObject(await tools.debugTools.drawDebugSphere({ center: [loc.x, loc.y, loc.z], radius: size, color, duration }));
674
+ } else if (shape === 'capsule') {
675
+ return cleanObject(await tools.debugTools.drawDebugCapsule({ center: [loc.x, loc.y, loc.z], halfHeight: size, radius: Math.max(10, size/3), color, duration }));
676
+ } else if (shape === 'cone') {
677
+ 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 }));
678
+ } else if (shape === 'arrow') {
679
+ const end = args.end || { x: loc.x + size, y: loc.y, z: loc.z };
680
+ return cleanObject(await tools.debugTools.drawDebugArrow({ start: [loc.x, loc.y, loc.z], end: [end.x, end.y, end.z], color, duration }));
681
+ } else if (shape === 'point') {
682
+ return cleanObject(await tools.debugTools.drawDebugPoint({ location: [loc.x, loc.y, loc.z], size, color, duration }));
683
+ } else if (shape === 'text' || shape === 'string') {
684
+ const text = args.text || 'Debug';
685
+ return cleanObject(await tools.debugTools.drawDebugString({ location: [loc.x, loc.y, loc.z], text, color, duration }));
686
+ }
687
+ // Default fallback
688
+ return cleanObject({ success: false, error: `Unsupported debug shape: ${originalShapeLabel}` });
689
+ }
555
690
  default:
556
691
  throw new Error(`Unknown effect action: ${args.action}`);
557
692
  }
558
- break;
559
693
 
560
- // 7. BLUEPRINT MANAGER
694
+ // 7. BLUEPRINT MANAGER
561
695
  case 'manage_blueprint':
562
- switch (args.action) {
563
- case 'create':
564
- mappedName = 'create_blueprint';
565
- mappedArgs = {
696
+ switch (requireAction(args)) {
697
+ case 'create': {
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({
566
716
  name: args.name,
567
- blueprintType: args.blueprintType,
568
- savePath: args.savePath
569
- };
570
- break;
571
- case 'add_component':
572
- mappedName = 'add_blueprint_component';
573
- mappedArgs = {
574
- blueprintName: args.name,
575
- componentType: args.componentType,
576
- componentName: args.componentName
577
- };
578
- break;
717
+ blueprintType: args.blueprintType || 'Actor',
718
+ savePath: args.savePath,
719
+ parentClass: args.parentClass
720
+ });
721
+ return cleanObject(res);
722
+ }
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
+ );
746
+ const res = await tools.blueprintTools.addComponent({ blueprintName: args.name, componentType: args.componentType, componentName: args.componentName });
747
+ return cleanObject(res);
748
+ }
579
749
  default:
580
750
  throw new Error(`Unknown blueprint action: ${args.action}`);
581
751
  }
582
- break;
583
752
 
584
- // 8. ENVIRONMENT BUILDER
753
+ // 8. ENVIRONMENT BUILDER
585
754
  case 'build_environment':
586
- switch (args.action) {
587
- case 'create_landscape':
588
- mappedName = 'create_landscape';
589
- mappedArgs = {
590
- name: args.name,
755
+ switch (requireAction(args)) {
756
+ case 'create_landscape': {
757
+ const res = await tools.landscapeTools.createLandscape({ name: args.name, sizeX: args.sizeX, sizeY: args.sizeY, materialPath: args.materialPath });
758
+ return cleanObject(res);
759
+ }
760
+ case 'sculpt': {
761
+ const res = await tools.landscapeTools.sculptLandscape({ landscapeName: args.name, tool: args.tool, brushSize: args.brushSize, strength: args.strength });
762
+ return cleanObject(res);
763
+ }
764
+ case 'add_foliage': {
765
+ const res = await tools.foliageTools.addFoliageType({ name: args.name, meshPath: args.meshPath, density: args.density });
766
+ return cleanObject(res);
767
+ }
768
+ case 'paint_foliage': {
769
+ const pos = args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0,0,0];
770
+ const res = await tools.foliageTools.paintFoliage({ foliageType: args.foliageType, position: pos, brushSize: args.brushSize, paintDensity: args.paintDensity, eraseMode: args.eraseMode });
771
+ return cleanObject(res);
772
+ }
773
+ case 'create_procedural_terrain': {
774
+ const loc = args.location ? [args.location.x||0, args.location.y||0, args.location.z||0] : [0,0,0];
775
+ const res = await tools.buildEnvAdvanced.createProceduralTerrain({
776
+ name: args.name || 'ProceduralTerrain',
777
+ location: loc as [number,number,number],
591
778
  sizeX: args.sizeX,
592
779
  sizeY: args.sizeY,
593
- materialPath: args.materialPath
594
- };
595
- break;
596
- case 'sculpt':
597
- mappedName = 'sculpt_landscape';
598
- mappedArgs = {
599
- landscapeName: args.name,
600
- tool: args.tool,
601
- brushSize: args.brushSize,
602
- strength: args.strength
780
+ subdivisions: args.subdivisions,
781
+ heightFunction: args.heightFunction,
782
+ material: args.materialPath
783
+ });
784
+ return cleanObject(res);
785
+ }
786
+ case 'create_procedural_foliage': {
787
+ if (!args.bounds || !args.bounds.location || !args.bounds.size) throw new Error('bounds.location and bounds.size are required');
788
+ const bounds = {
789
+ location: [args.bounds.location.x||0, args.bounds.location.y||0, args.bounds.location.z||0] as [number,number,number],
790
+ size: [args.bounds.size.x||1000, args.bounds.size.y||1000, args.bounds.size.z||100] as [number,number,number]
603
791
  };
604
- break;
605
- case 'add_foliage':
606
- // Validate foliage creation parameters to avoid bad console commands / engine warnings
607
- if (args.name === undefined || args.name === null || typeof args.name !== 'string' || args.name.trim() === '' || String(args.name).toLowerCase() === 'undefined' || String(args.name).toLowerCase() === 'any') {
608
- throw new Error(`Invalid foliage name: '${args.name}'`);
609
- }
610
- if (args.meshPath === undefined || args.meshPath === null || typeof args.meshPath !== 'string' || args.meshPath.trim() === '' || String(args.meshPath).toLowerCase() === 'undefined') {
611
- throw new Error(`Invalid meshPath: '${args.meshPath}'`);
612
- }
613
- if (args.density !== undefined) {
614
- if (typeof args.density !== 'number' || !isFinite(args.density) || args.density < 0) {
615
- throw new Error(`Invalid density: '${args.density}' (must be non-negative finite number)`);
616
- }
617
- }
618
- mappedName = 'add_foliage_type';
619
- mappedArgs = {
620
- name: args.name,
792
+ const res = await tools.buildEnvAdvanced.createProceduralFoliage({
793
+ name: args.name || 'ProceduralFoliage',
794
+ bounds,
795
+ foliageTypes: args.foliageTypes || [],
796
+ seed: args.seed
797
+ });
798
+ return cleanObject(res);
799
+ }
800
+ case 'add_foliage_instances': {
801
+ if (!args.foliageType) throw new Error('foliageType is required');
802
+ if (!Array.isArray(args.transforms)) throw new Error('transforms array is required');
803
+ const transforms = (args.transforms as any[]).map(t => ({
804
+ location: [t.location?.x||0, t.location?.y||0, t.location?.z||0] as [number,number,number],
805
+ rotation: t.rotation ? [t.rotation.pitch||0, t.rotation.yaw||0, t.rotation.roll||0] as [number,number,number] : undefined,
806
+ scale: t.scale ? [t.scale.x||1, t.scale.y||1, t.scale.z||1] as [number,number,number] : undefined
807
+ }));
808
+ const res = await tools.buildEnvAdvanced.addFoliageInstances({ foliageType: args.foliageType, transforms });
809
+ return cleanObject(res);
810
+ }
811
+ case 'create_landscape_grass_type': {
812
+ const res = await tools.buildEnvAdvanced.createLandscapeGrassType({
813
+ name: args.name || 'GrassType',
621
814
  meshPath: args.meshPath,
622
- density: args.density
623
- };
624
- break;
625
- case 'paint_foliage':
626
- // Validate paint parameters
627
- if (args.foliageType === undefined || args.foliageType === null || typeof args.foliageType !== 'string' || args.foliageType.trim() === '' || String(args.foliageType).toLowerCase() === 'undefined' || String(args.foliageType).toLowerCase() === 'any') {
628
- throw new Error(`Invalid foliageType: '${args.foliageType}'`);
629
- }
630
- // Convert position object to array if needed
631
- let positionArray;
632
- if (args.position) {
633
- if (Array.isArray(args.position)) {
634
- positionArray = args.position;
635
- } else if (typeof args.position === 'object') {
636
- positionArray = [args.position.x || 0, args.position.y || 0, args.position.z || 0];
637
- } else {
638
- positionArray = [0, 0, 0];
639
- }
640
- } else {
641
- positionArray = [0, 0, 0];
642
- }
643
- // Validate numbers in position
644
- if (!Array.isArray(positionArray) || positionArray.length !== 3 || positionArray.some(v => typeof v !== 'number' || !isFinite(v))) {
645
- throw new Error(`Invalid position: '${JSON.stringify(args.position)}'`);
646
- }
647
- if (args.brushSize !== undefined) {
648
- if (typeof args.brushSize !== 'number' || !isFinite(args.brushSize) || args.brushSize < 0) {
649
- throw new Error(`Invalid brushSize: '${args.brushSize}' (must be non-negative finite number)`);
650
- }
651
- }
652
- mappedName = 'paint_foliage';
653
- mappedArgs = {
654
- foliageType: args.foliageType,
655
- position: positionArray,
656
- brushSize: args.brushSize
657
- };
658
- break;
815
+ density: args.density,
816
+ minScale: args.minScale,
817
+ maxScale: args.maxScale
818
+ });
819
+ return cleanObject(res);
820
+ }
659
821
  default:
660
822
  throw new Error(`Unknown environment action: ${args.action}`);
661
823
  }
662
- break;
663
824
 
664
825
  // 9. SYSTEM CONTROL
665
- case 'system_control':
666
- // Validate args is not null/undefined
667
- if (args === null || args === undefined) {
668
- throw new Error('Invalid arguments: null or undefined');
669
- }
670
- if (typeof args !== 'object') {
671
- throw new Error('Invalid arguments: must be an object');
672
- }
673
- // Validate action exists
674
- if (!args.action) {
675
- throw new Error('Missing required parameter: action');
676
- }
677
-
678
- switch (args.action) {
679
- case 'profile':
680
- // Validate profile type
681
- const validProfileTypes = ['CPU', 'GPU', 'Memory', 'RenderThread', 'GameThread', 'All'];
682
- if (!args.profileType || !validProfileTypes.includes(args.profileType)) {
683
- throw new Error(`Invalid profileType: '${args.profileType}'. Valid types: ${validProfileTypes.join(', ')}`);
684
- }
685
- mappedName = 'start_profiling';
686
- mappedArgs = {
687
- type: args.profileType,
688
- duration: args.duration
689
- };
690
- break;
691
-
692
- case 'show_fps':
693
- // Validate enabled is boolean
694
- if (args.enabled !== undefined && typeof args.enabled !== 'boolean') {
695
- throw new Error(`Invalid enabled: must be boolean, got ${typeof args.enabled}`);
696
- }
697
- mappedName = 'show_fps';
698
- mappedArgs = {
699
- enabled: args.enabled,
700
- verbose: args.verbose
701
- };
702
- break;
703
-
704
- case 'set_quality':
705
- // Validate category - normalize aliases and singular forms used by sg.*Quality
706
- const validCategories = ['ViewDistance', 'AntiAliasing', 'PostProcessing', 'PostProcess',
707
- 'Shadows', 'Shadow', 'GlobalIllumination', 'Reflections', 'Reflection', 'Textures', 'Texture',
708
- 'Effects', 'Foliage', 'Shading'];
709
- if (!args.category || !validCategories.includes(args.category)) {
710
- throw new Error(`Invalid category: '${args.category}'. Valid categories: ${validCategories.join(', ')}`);
711
- }
712
- // Validate level
713
- if (args.level === undefined || args.level === null) {
714
- throw new Error('Missing required parameter: level');
715
- }
716
- if (typeof args.level !== 'number' || !Number.isInteger(args.level) || args.level < 0 || args.level > 4) {
717
- throw new Error(`Invalid level: must be integer 0-4, got ${args.level}`);
718
- }
719
- // Normalize category to sg.<Base>Quality base (singular where needed)
720
- const map: Record<string, string> = {
721
- ViewDistance: 'ViewDistance',
722
- AntiAliasing: 'AntiAliasing',
723
- PostProcessing: 'PostProcess',
724
- PostProcess: 'PostProcess',
725
- Shadows: 'Shadow',
726
- Shadow: 'Shadow',
727
- GlobalIllumination: 'GlobalIllumination',
728
- Reflections: 'Reflection',
729
- Reflection: 'Reflection',
730
- Textures: 'Texture',
731
- Texture: 'Texture',
732
- Effects: 'Effects',
733
- Foliage: 'Foliage',
734
- Shading: 'Shading',
735
- };
736
- const categoryName = map[String(args.category)] || args.category;
737
- mappedName = 'set_scalability';
738
- mappedArgs = {
739
- category: categoryName,
740
- level: args.level
741
- };
742
- break;
743
-
744
- case 'play_sound':
745
- // Validate sound path
746
- if (!args.soundPath || typeof args.soundPath !== 'string') {
747
- throw new Error('Invalid soundPath: must be a non-empty string');
748
- }
749
- // Validate volume if provided
750
- if (args.volume !== undefined) {
751
- if (typeof args.volume !== 'number' || args.volume < 0 || args.volume > 1) {
752
- throw new Error(`Invalid volume: must be 0-1, got ${args.volume}`);
826
+ case 'system_control':
827
+ switch (requireAction(args)) {
828
+ case 'profile': {
829
+ const res = await tools.performanceTools.startProfiling({ type: args.profileType, duration: args.duration });
830
+ return cleanObject(res);
831
+ }
832
+ case 'show_fps': {
833
+ const res = await tools.performanceTools.showFPS({ enabled: !!args.enabled, verbose: !!args.verbose });
834
+ return cleanObject(res);
835
+ }
836
+ case 'set_quality': {
837
+ const res = await tools.performanceTools.setScalability({ category: args.category, level: args.level });
838
+ return cleanObject(res);
839
+ }
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
+ }
753
851
  }
754
- }
755
- mappedName = 'play_sound';
756
- mappedArgs = {
757
- soundPath: args.soundPath,
758
- location: args.location,
759
- volume: args.volume,
760
- is3D: args.is3D
761
- };
762
- break;
763
-
764
- case 'create_widget':
765
- // Validate widget name
766
- if (!args.widgetName || typeof args.widgetName !== 'string' || args.widgetName.trim() === '') {
767
- throw new Error('Invalid widgetName: must be a non-empty string');
768
- }
769
- mappedName = 'create_widget';
770
- mappedArgs = {
771
- name: args.widgetName,
772
- type: args.widgetType,
773
- savePath: args.savePath
774
- };
775
- break;
776
-
777
- case 'show_widget':
778
- // Validate widget name
779
- if (!args.widgetName || typeof args.widgetName !== 'string') {
780
- throw new Error('Invalid widgetName: must be a non-empty string');
781
- }
782
- // Validate visible is boolean (default to true if not provided)
783
- const isVisible = args.visible !== undefined ? args.visible : true;
784
- if (typeof isVisible !== 'boolean') {
785
- throw new Error(`Invalid visible: must be boolean, got ${typeof isVisible}`);
786
- }
787
- mappedName = 'show_widget';
788
- mappedArgs = {
789
- widgetName: args.widgetName,
790
- visible: isVisible
791
- };
792
- break;
793
-
794
- case 'screenshot':
795
- mappedName = 'take_screenshot';
796
- mappedArgs = { resolution: args.resolution };
797
- break;
798
-
799
- case 'engine_start':
800
- mappedName = 'launch_editor';
801
- mappedArgs = { editorExe: args.editorExe, projectPath: args.projectPath };
802
- break;
803
-
804
- case 'engine_quit':
805
- mappedName = 'quit_editor';
806
- mappedArgs = {};
807
- break;
808
-
852
+ );
853
+ const soundPath = requireNonEmptyString(args.soundPath, 'soundPath', 'Missing required parameter: soundPath');
854
+ if (args.location && typeof args.location === 'object') {
855
+ const loc = [args.location.x || 0, args.location.y || 0, args.location.z || 0];
856
+ const res = await tools.audioTools.playSoundAtLocation({ soundPath, location: loc as [number, number, number], volume: args.volume, pitch: args.pitch, startTime: args.startTime });
857
+ return cleanObject(res);
858
+ }
859
+ const res = await tools.audioTools.playSound2D({ soundPath, volume: args.volume, pitch: args.pitch, startTime: args.startTime });
860
+ return cleanObject(res);
861
+ }
862
+ case 'create_widget': {
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 });
883
+ return cleanObject(res);
884
+ }
885
+ case 'show_widget': {
886
+ const res = await tools.uiTools.setWidgetVisibility({ widgetName: args.widgetName, visible: args.visible !== false });
887
+ return cleanObject(res);
888
+ }
889
+ case 'screenshot': {
890
+ const res = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
891
+ return cleanObject(res);
892
+ }
893
+ case 'engine_start': {
894
+ const res = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
895
+ return cleanObject(res);
896
+ }
897
+ case 'engine_quit': {
898
+ const res = await tools.engineTools.quitEditor();
899
+ return cleanObject(res);
900
+ }
809
901
  default:
810
902
  throw new Error(`Unknown system action: ${args.action}`);
811
903
  }
812
- break;
813
904
 
814
905
  // 10. CONSOLE COMMAND - handle validation here
815
- case 'console_command':
816
- // Handle empty/invalid commands gracefully
817
- if (args.command === undefined || args.command === null || args.command === '' || typeof args.command !== 'string') {
818
- return {
819
- content: [{
820
- type: 'text',
821
- text: 'Empty command ignored'
822
- }],
823
- isError: false,
824
- success: true,
825
- message: 'Empty command'
826
- };
906
+ case 'console_command':
907
+ if (!args.command || typeof args.command !== 'string' || args.command.trim() === '') {
908
+ return { success: true, message: 'Empty command' } as any;
827
909
  }
828
-
829
- const cmdTrimmed = args.command.trim();
830
- if (cmdTrimmed.length === 0) {
831
- return {
832
- content: [{
833
- type: 'text',
834
- text: 'Empty command ignored'
835
- }],
836
- isError: false,
837
- success: true,
838
- message: 'Empty command'
839
- };
910
+ // Basic safety filter
911
+ const cmd = String(args.command).trim();
912
+ const blocked = [/\bquit\b/i, /\bexit\b/i, /debugcrash/i];
913
+ if (blocked.some(r => r.test(cmd))) {
914
+ return { success: false, error: 'Command blocked for safety' } as any;
915
+ }
916
+ try {
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
+ });
933
+ } catch (e: any) {
934
+ return cleanObject({ success: false, command: cmd, error: e?.message || String(e) });
840
935
  }
841
936
 
842
- mappedName = 'console_command';
843
- mappedArgs = args;
844
- break;
845
937
 
846
938
  // 11. REMOTE CONTROL PRESETS - Direct implementation
847
939
  case 'manage_rc':
848
- if (!args.action) throw new Error('Missing required parameter: action');
849
-
850
940
  // Handle RC operations directly through RcTools
851
941
  let rcResult: any;
852
-
853
- switch (args.action) {
942
+ const rcAction = requireAction(args);
943
+ switch (rcAction) {
854
944
  // Support both 'create_preset' and 'create' for compatibility
855
945
  case 'create_preset':
856
946
  case 'create':
@@ -877,8 +967,10 @@ export async function handleConsolidatedToolCall(
877
967
  break;
878
968
 
879
969
  case 'delete':
880
- if (!args.presetId) throw new Error('Missing required parameter: presetId');
881
- 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);
882
974
  if (rcResult.success) {
883
975
  rcResult.message = 'Preset deleted successfully';
884
976
  }
@@ -975,7 +1067,7 @@ export async function handleConsolidatedToolCall(
975
1067
  break;
976
1068
 
977
1069
  default:
978
- 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`);
979
1071
  }
980
1072
 
981
1073
  // Return result directly - MCP formatting will be handled by response validator
@@ -984,14 +1076,13 @@ export async function handleConsolidatedToolCall(
984
1076
 
985
1077
  // 12. SEQUENCER / CINEMATICS
986
1078
  case 'manage_sequence':
987
- if (!args.action) throw new Error('Missing required parameter: action');
988
-
989
1079
  // Direct handling for sequence operations
990
1080
  const seqResult = await (async () => {
991
1081
  const sequenceTools = tools.sequenceTools;
992
1082
  if (!sequenceTools) throw new Error('Sequence tools not available');
1083
+ const action = requireAction(args);
993
1084
 
994
- switch (args.action) {
1085
+ switch (action) {
995
1086
  case 'create':
996
1087
  return await sequenceTools.create({ name: args.name, path: args.path });
997
1088
  case 'open':
@@ -1031,7 +1122,7 @@ export async function handleConsolidatedToolCall(
1031
1122
  if (args.speed === undefined) throw new Error('Missing required parameter: speed');
1032
1123
  return await sequenceTools.setPlaybackSpeed({ speed: args.speed });
1033
1124
  default:
1034
- throw new Error(`Unknown sequence action: ${args.action}`);
1125
+ throw new Error(`Unknown sequence action: ${action}`);
1035
1126
  }
1036
1127
  })();
1037
1128
 
@@ -1039,42 +1130,27 @@ export async function handleConsolidatedToolCall(
1039
1130
  // Clean to prevent circular references
1040
1131
  return cleanObject(seqResult);
1041
1132
  // 13. INTROSPECTION
1042
- case 'inspect':
1043
- if (!args.action) throw new Error('Missing required parameter: action');
1044
- switch (args.action) {
1045
- case 'inspect_object':
1046
- mappedName = 'inspect_object';
1047
- mappedArgs = { objectPath: args.objectPath };
1048
- break;
1049
- case 'set_property':
1050
- mappedName = 'inspect_set_property';
1051
- mappedArgs = { objectPath: args.objectPath, propertyName: args.propertyName, value: args.value };
1052
- break;
1133
+ case 'inspect':
1134
+ const inspectAction = requireAction(args);
1135
+ switch (inspectAction) {
1136
+ case 'inspect_object': {
1137
+ const res = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath, detailed: args.detailed });
1138
+ return cleanObject(res);
1139
+ }
1140
+ case 'set_property': {
1141
+ const res = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
1142
+ return cleanObject(res);
1143
+ }
1053
1144
  default:
1054
- throw new Error(`Unknown inspect action: ${args.action}`);
1145
+ throw new Error(`Unknown inspect action: ${inspectAction}`);
1055
1146
  }
1056
- break;
1147
+
1057
1148
 
1058
1149
  default:
1059
1150
  throw new Error(`Unknown consolidated tool: ${name}`);
1060
1151
  }
1061
1152
 
1062
- // Call the original handler with mapped name and args with timeout
1063
- const TOOL_TIMEOUT = 15000; // 15 seconds timeout for tool execution
1064
-
1065
- const toolPromise = handleToolCall(mappedName, mappedArgs, tools);
1066
- const timeoutPromise = new Promise<never>((_, reject) => {
1067
- setTimeout(() => {
1068
- reject(new Error(`Tool execution timeout after ${TOOL_TIMEOUT}ms`));
1069
- }, TOOL_TIMEOUT);
1070
- });
1071
-
1072
- const result = await Promise.race([toolPromise, timeoutPromise]);
1073
- const duration = Date.now() - startTime;
1074
- console.log(`[ConsolidatedToolHandler] Completed execution of ${name} in ${duration}ms`);
1075
-
1076
- // Clean the result to prevent circular reference errors
1077
- return cleanObject(result);
1153
+ // All cases return (or throw) above; this is a type guard for exhaustiveness.
1078
1154
 
1079
1155
  } catch (err: any) {
1080
1156
  const duration = Date.now() - startTime;