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,863 +0,0 @@
1
- import { Logger } from '../utils/logger.js';
2
- import { toVec3Object, toRotObject, toVec3Array } from '../utils/normalize.js';
3
- import { cleanObject } from '../utils/safe-json.js';
4
- const log = new Logger('ToolHandler');
5
- export async function handleToolCall(name, args, tools) {
6
- try {
7
- let result;
8
- let message;
9
- switch (name) {
10
- // Asset Tools
11
- case 'list_assets':
12
- // Initialize message variable
13
- message = '';
14
- // Validate directory argument
15
- if (args.directory === null) {
16
- result = { assets: [], error: 'Directory cannot be null' };
17
- message = 'Failed to list assets: directory cannot be null';
18
- break;
19
- }
20
- if (args.directory === undefined) {
21
- result = { assets: [], error: 'Directory cannot be undefined' };
22
- message = 'Failed to list assets: directory cannot be undefined';
23
- break;
24
- }
25
- if (typeof args.directory !== 'string') {
26
- result = { assets: [], error: `Invalid directory type: expected string, got ${typeof args.directory}` };
27
- message = `Failed to list assets: directory must be a string path, got ${typeof args.directory}`;
28
- break;
29
- }
30
- else if (args.directory.trim() === '') {
31
- result = { assets: [], error: 'Directory path cannot be empty' };
32
- message = 'Failed to list assets: directory path cannot be empty';
33
- break;
34
- }
35
- // Normalize virtual content path: map /Content -> /Game (case-insensitive)
36
- const rawDir = String(args.directory).trim();
37
- let normDir = rawDir.replace(/^\/?content(\/|$)/i, '/Game$1');
38
- // Ensure leading slash
39
- if (!normDir.startsWith('/'))
40
- normDir = '/' + normDir;
41
- // Collapse duplicate slashes
42
- normDir = normDir.replace(/\\+/g, '/').replace(/\/+/g, '/');
43
- // Try multiple approaches to list assets
44
- try {
45
- console.log('[list_assets] Starting asset listing for directory:', normDir);
46
- // First try: Use Python for most reliable listing (recursive if /Game)
47
- const pythonCode = `
48
- import unreal
49
- import json
50
-
51
- directory = '${normDir || '/Game'}'
52
- # Use recursive for /Game to find assets in subdirectories, but limit depth
53
- recursive = True if directory == '/Game' else False
54
-
55
- try:
56
- asset_registry = unreal.AssetRegistryHelpers.get_asset_registry()
57
-
58
- # Create filter - ARFilter properties must be set in constructor
59
- filter = unreal.ARFilter(
60
- package_paths=[directory],
61
- recursive_paths=recursive
62
- )
63
-
64
- # Get all assets in the directory (limit to prevent timeout)
65
- all_assets = asset_registry.get_assets(filter)
66
- # Limit results to prevent timeout
67
- assets = all_assets[:100] if len(all_assets) > 100 else all_assets
68
-
69
- # Format asset information
70
- asset_list = []
71
- for asset in assets:
72
- asset_info = {
73
- "Name": str(asset.asset_name),
74
- "Path": str(asset.package_path) + "/" + str(asset.asset_name),
75
- "Class": str(asset.asset_class_path.asset_name if hasattr(asset.asset_class_path, "asset_name") else asset.asset_class),
76
- "PackagePath": str(asset.package_path)
77
- }
78
- asset_list.append(asset_info)
79
- print("Asset: " + asset_info["Path"])
80
-
81
- result = {
82
- "success": True,
83
- "count": len(asset_list),
84
- "assets": asset_list
85
- }
86
- print("RESULT:" + json.dumps(result))
87
- except Exception as e:
88
- # Fallback to EditorAssetLibrary if ARFilter fails
89
- try:
90
- import unreal
91
- # For /Game, use recursive to find assets in subdirectories
92
- recursive_fallback = True if directory == '/Game' else False
93
- asset_paths = unreal.EditorAssetLibrary.list_assets(directory, recursive_fallback, False)
94
- asset_list = []
95
- for path in asset_paths:
96
- asset_list.append({
97
- "Name": path.split("/")[-1].split(".")[0],
98
- "Path": path,
99
- "PackagePath": "/".join(path.split("/")[:-1])
100
- })
101
- result = {
102
- "success": True,
103
- "count": len(asset_list),
104
- "assets": asset_list,
105
- "method": "EditorAssetLibrary"
106
- }
107
- print("RESULT:" + json.dumps(result))
108
- except Exception as e2:
109
- print("Error listing assets: " + str(e) + " | Fallback error: " + str(e2))
110
- print("RESULT:" + json.dumps({"success": False, "error": str(e), "assets": []}))
111
- `.trim();
112
- console.log('[list_assets] Executing Python code...');
113
- const pyResponse = await tools.bridge.executePython(pythonCode);
114
- console.log('[list_assets] Python response type:', typeof pyResponse);
115
- // Parse Python output
116
- let outputStr = '';
117
- if (typeof pyResponse === 'object' && pyResponse !== null) {
118
- if (pyResponse.LogOutput && Array.isArray(pyResponse.LogOutput)) {
119
- outputStr = pyResponse.LogOutput
120
- .map((log) => log.Output || '')
121
- .join('');
122
- }
123
- else {
124
- outputStr = JSON.stringify(pyResponse);
125
- }
126
- }
127
- else {
128
- outputStr = String(pyResponse || '');
129
- }
130
- // Extract result from Python output
131
- console.log('[list_assets] Python output sample:', outputStr.substring(0, 200));
132
- const resultMatch = outputStr.match(/RESULT:({.*})/);
133
- if (resultMatch) {
134
- console.log('[list_assets] Found RESULT in Python output');
135
- try {
136
- const listResult = JSON.parse(resultMatch[1]);
137
- if (listResult.success && listResult.assets) {
138
- result = { assets: listResult.assets };
139
- // Success - skip fallback methods
140
- }
141
- }
142
- catch {
143
- // Fall through to HTTP method
144
- }
145
- }
146
- // Fallback: Use the search API if Python didn't succeed
147
- if (!result || !result.assets) {
148
- try {
149
- const searchResult = await tools.bridge.httpCall('/remote/search/assets', 'PUT', {
150
- Query: '', // Empty query to match all (wildcard doesn't work)
151
- Filter: {
152
- PackagePaths: [normDir || '/Game'],
153
- // Recursively search so we actually find assets in subfolders
154
- RecursivePaths: true,
155
- ClassNames: [], // Empty to get all types
156
- RecursiveClasses: true
157
- },
158
- Limit: 1000, // Increase limit
159
- Start: 0
160
- });
161
- if (searchResult?.Assets && Array.isArray(searchResult.Assets)) {
162
- result = { assets: searchResult.Assets };
163
- }
164
- }
165
- catch {
166
- // Continue to next fallback
167
- }
168
- }
169
- // Third try: Use console command to get asset registry (only if still no results)
170
- if (!result || !result.assets || result.assets.length === 0) {
171
- try {
172
- await tools.bridge.httpCall('/remote/object/call', 'PUT', {
173
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
174
- functionName: 'ExecuteConsoleCommand',
175
- parameters: {
176
- WorldContextObject: null,
177
- Command: `AssetRegistry.DumpAssets ${normDir || '/Game'}`,
178
- SpecificPlayer: null
179
- },
180
- generateTransaction: false
181
- });
182
- }
183
- catch {
184
- // Console command attempt failed, continue
185
- }
186
- // If all else fails, set empty result
187
- if (!result || !result.assets) {
188
- result = { assets: [], note: 'Asset listing requires proper Remote Control configuration' };
189
- }
190
- }
191
- }
192
- catch (err) {
193
- result = { assets: [], error: String(err) };
194
- message = `Failed to list assets: ${err}`;
195
- }
196
- // Include full asset list with details in the message
197
- console.log('[list_assets] Formatting message - result has assets?', !!(result && result.assets), 'count:', result?.assets?.length);
198
- if (result && result.assets && result.assets.length > 0) {
199
- // If Python/HTTP gives only paths, generate Name from path
200
- result.assets = result.assets.map((asset) => {
201
- if (!asset.Name && asset.Path) {
202
- const base = String(asset.Path).split('/').pop() || '';
203
- const name = base.includes('.') ? base.split('.').pop() : base;
204
- return { ...asset, Name: name };
205
- }
206
- return asset;
207
- });
208
- // Group assets by type for better organization
209
- result.assets = result.assets.map((a) => {
210
- if (a && !a.Path && a.AssetPath) {
211
- return { ...a, Path: a.AssetPath, PackagePath: a.AssetPath.split('/').slice(0, -1).join('/') };
212
- }
213
- return a;
214
- });
215
- }
216
- if (result && result.assets && result.assets.length > 0) {
217
- // Group assets by type for better organization
218
- const assetsByType = {};
219
- result.assets.forEach((asset) => {
220
- const type = asset.Class || asset.class || 'Unknown';
221
- if (!assetsByType[type]) {
222
- assetsByType[type] = [];
223
- }
224
- assetsByType[type].push(asset);
225
- });
226
- // Format output with proper structure
227
- let assetDetails = `📁 Asset Directory: ${normDir || '/Game'}\n`;
228
- assetDetails += `📊 Total Assets: ${result.assets.length}\n\n`;
229
- // Sort types alphabetically
230
- const sortedTypes = Object.keys(assetsByType).sort();
231
- sortedTypes.forEach((type, typeIndex) => {
232
- const assets = assetsByType[type];
233
- assetDetails += `${typeIndex + 1}. ${type} (${assets.length} items)\n`;
234
- assets.forEach((asset, index) => {
235
- const name = asset.Name || asset.name || 'Unknown';
236
- const path = asset.Path || asset.path || asset.PackagePath || '';
237
- const packagePath = asset.PackagePath || '';
238
- assetDetails += ` ${index + 1}. ${name}\n`;
239
- if (path !== packagePath && packagePath) {
240
- assetDetails += ` 📍 Path: ${path}\n`;
241
- assetDetails += ` 📦 Package: ${packagePath}\n`;
242
- }
243
- else {
244
- assetDetails += ` 📍 ${path}\n`;
245
- }
246
- });
247
- assetDetails += '\n';
248
- });
249
- message = assetDetails;
250
- // Also keep the structured data in the result for programmatic access
251
- }
252
- else {
253
- message = `No assets found in ${normDir || '/Game'}`;
254
- }
255
- break;
256
- case 'import_asset':
257
- result = await tools.assetTools.importAsset(args.sourcePath, args.destinationPath);
258
- // Check if import actually succeeded
259
- if (result.error) {
260
- message = result.error;
261
- }
262
- else if (result.success && result.paths && result.paths.length > 0) {
263
- message = result.message || `Successfully imported ${result.paths.length} asset(s) to ${args.destinationPath}`;
264
- }
265
- else {
266
- message = result.message || result.error || `Import did not report success for source ${args.sourcePath}`;
267
- }
268
- break;
269
- // Actor Tools
270
- case 'spawn_actor':
271
- // Normalize transforms: accept object or array
272
- if (args.location !== undefined && args.location !== null) {
273
- const loc = toVec3Object(args.location);
274
- if (!loc)
275
- throw new Error('Invalid location: expected {x,y,z} or [x,y,z]');
276
- args.location = loc;
277
- }
278
- if (args.rotation !== undefined && args.rotation !== null) {
279
- const rot = toRotObject(args.rotation);
280
- if (!rot)
281
- throw new Error('Invalid rotation: expected {pitch,yaw,roll} or [pitch,yaw,roll]');
282
- args.rotation = rot;
283
- }
284
- result = await tools.actorTools.spawn(args);
285
- message = `Actor spawned: ${JSON.stringify(result)}`;
286
- break;
287
- case 'delete_actor':
288
- // Use EditorActorSubsystem instead of deprecated EditorLevelLibrary
289
- try {
290
- const pythonCmd = `
291
- import unreal
292
- import json
293
-
294
- result = {"success": False, "message": "", "deleted_count": 0, "deleted_actors": []}
295
-
296
- try:
297
- actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
298
- actors = actor_subsystem.get_all_level_actors()
299
- deleted_actors = []
300
- search_name = "${args.actorName}"
301
-
302
- for actor in actors:
303
- if actor:
304
- # Check both actor name and label
305
- actor_name = actor.get_name()
306
- actor_label = actor.get_actor_label()
307
-
308
- # Case-insensitive partial matching
309
- if (search_name.lower() in actor_label.lower() or
310
- actor_label.lower().startswith(search_name.lower() + "_") or
311
- actor_label.lower() == search_name.lower() or
312
- actor_name.lower() == search_name.lower()):
313
- actor_subsystem.destroy_actor(actor)
314
- deleted_actors.append(actor_label)
315
-
316
- if deleted_actors:
317
- result["success"] = True
318
- result["deleted_count"] = len(deleted_actors)
319
- result["deleted_actors"] = deleted_actors
320
- result["message"] = f"Deleted {len(deleted_actors)} actor(s): {deleted_actors}"
321
- else:
322
- result["message"] = f"No actors found matching: {search_name}"
323
- # List available actors for debugging
324
- all_labels = [a.get_actor_label() for a in actors[:10] if a]
325
- result["available_actors"] = all_labels
326
-
327
- except Exception as e:
328
- result["message"] = f"Error deleting actors: {e}"
329
-
330
- print(f"RESULT:{json.dumps(result)}")
331
- `.trim();
332
- const response = await tools.bridge.executePython(pythonCmd);
333
- // Extract output from Python response
334
- let outputStr = '';
335
- if (typeof response === 'object' && response !== null) {
336
- // Check if it has LogOutput (standard Python execution response)
337
- if (response.LogOutput && Array.isArray(response.LogOutput)) {
338
- // Concatenate all log outputs
339
- outputStr = response.LogOutput
340
- .map((log) => log.Output || '')
341
- .join('');
342
- }
343
- else if ('result' in response) {
344
- outputStr = String(response.result);
345
- }
346
- else {
347
- outputStr = JSON.stringify(response);
348
- }
349
- }
350
- else {
351
- outputStr = String(response || '');
352
- }
353
- // Parse the result
354
- const resultMatch = outputStr.match(/RESULT:(\{.*\})/);
355
- if (resultMatch) {
356
- try {
357
- const deleteResult = JSON.parse(resultMatch[1]);
358
- if (!deleteResult.success) {
359
- throw new Error(deleteResult.message);
360
- }
361
- result = deleteResult;
362
- message = deleteResult.message;
363
- }
364
- catch {
365
- // Fallback to checking output
366
- if (outputStr.includes('Deleted')) {
367
- result = { success: true, message: outputStr };
368
- message = `Actor deleted: ${args.actorName}`;
369
- }
370
- else {
371
- throw new Error(outputStr || 'Delete failed');
372
- }
373
- }
374
- }
375
- else {
376
- // Check for error patterns
377
- if (outputStr.includes('No actors found') || outputStr.includes('Error')) {
378
- throw new Error(outputStr || 'Delete failed - no actors found');
379
- }
380
- // Only report success if clear indication
381
- if (outputStr.includes('Deleted')) {
382
- result = { success: true, message: outputStr };
383
- message = `Actor deleted: ${args.actorName}`;
384
- }
385
- else {
386
- throw new Error('No valid result from Python delete operation');
387
- }
388
- }
389
- }
390
- catch (pyErr) {
391
- // Fallback to console command
392
- const consoleResult = await tools.bridge.executeConsoleCommand(`DestroyActor ${args.actorName}`);
393
- // Check console command result
394
- if (consoleResult && typeof consoleResult === 'object') {
395
- // Console commands don't reliably report success/failure
396
- // Return an error to avoid false positives
397
- throw new Error(`Delete operation uncertain via console command for '${args.actorName}'. Python execution failed: ${pyErr}`);
398
- }
399
- // If we're here, we can't guarantee the delete worked
400
- result = {
401
- success: false,
402
- message: `Console command fallback attempted for '${args.actorName}', but result uncertain`,
403
- fallback: true
404
- };
405
- message = result.message;
406
- }
407
- break;
408
- // Material Tools
409
- case 'create_material':
410
- result = await tools.materialTools.createMaterial(args.name, args.path);
411
- message = result.success ? `Material created: ${result.path}` : result.error;
412
- break;
413
- case 'apply_material_to_actor':
414
- result = await tools.materialTools.applyMaterialToActor(args.actorPath, args.materialPath, args.slotIndex !== undefined ? args.slotIndex : 0);
415
- message = result.success ? result.message : result.error;
416
- break;
417
- // Editor Tools
418
- case 'play_in_editor':
419
- result = await tools.editorTools.playInEditor();
420
- message = result.message || 'PIE started';
421
- break;
422
- case 'stop_play_in_editor':
423
- result = await tools.editorTools.stopPlayInEditor();
424
- message = result.message || 'PIE stopped';
425
- break;
426
- case 'set_camera':
427
- result = await tools.editorTools.setViewportCamera(args.location, args.rotation);
428
- message = result.message || 'Camera set';
429
- break;
430
- // Animation Tools
431
- case 'create_animation_blueprint':
432
- result = await tools.animationTools.createAnimationBlueprint(args);
433
- message = result.message || `Animation blueprint ${args.name} created`;
434
- break;
435
- case 'play_animation_montage':
436
- result = await tools.animationTools.playAnimation({
437
- actorName: args.actorName,
438
- animationType: 'Montage',
439
- animationPath: args.montagePath,
440
- playRate: args.playRate
441
- });
442
- message = result.message || `Playing montage ${args.montagePath}`;
443
- break;
444
- // Physics Tools
445
- case 'setup_ragdoll':
446
- result = await tools.physicsTools.setupRagdoll(args);
447
- message = result.message || 'Ragdoll physics configured';
448
- break;
449
- case 'apply_force':
450
- // Normalize force vector
451
- const forceVec = toVec3Array(args.force);
452
- if (!forceVec)
453
- throw new Error('Invalid force: expected {x,y,z} or [x,y,z]');
454
- // Map the simple force schema to PhysicsTools expected format
455
- result = await tools.physicsTools.applyForce({
456
- actorName: args.actorName,
457
- forceType: 'Force', // Default to 'Force' type
458
- vector: forceVec,
459
- isLocal: false // World space by default
460
- });
461
- // Check if the result indicates an error
462
- if (result.error || (result.success === false)) {
463
- throw new Error(result.error || result.message || `Failed to apply force to ${args.actorName}`);
464
- }
465
- message = result.message || `Force applied to ${args.actorName}`;
466
- break;
467
- // Niagara Tools
468
- case 'create_particle_effect':
469
- result = await tools.niagaraTools.createEffect(args);
470
- message = result.message || `${args.effectType} effect created`;
471
- break;
472
- case 'spawn_niagara_system':
473
- result = await tools.niagaraTools.spawnEffect({
474
- systemPath: args.systemPath,
475
- location: args.location,
476
- scale: args.scale ? [args.scale, args.scale, args.scale] : undefined
477
- });
478
- message = result.message || 'Niagara system spawned';
479
- break;
480
- // Blueprint Tools
481
- case 'create_blueprint':
482
- result = await tools.blueprintTools.createBlueprint(args);
483
- message = result.message || `Blueprint ${args.name} created`;
484
- break;
485
- case 'add_blueprint_component':
486
- result = await tools.blueprintTools.addComponent(args);
487
- message = result.message || `Component ${args.componentName} added`;
488
- break;
489
- // Level Tools
490
- case 'load_level':
491
- result = await tools.levelTools.loadLevel(args);
492
- message = result.message || `Level ${args.levelPath} loaded`;
493
- break;
494
- case 'save_level':
495
- result = await tools.levelTools.saveLevel(args);
496
- message = result.message || 'Level saved';
497
- break;
498
- case 'stream_level':
499
- result = await tools.levelTools.streamLevel(args);
500
- message = result.message || 'Level streaming updated';
501
- break;
502
- // Lighting Tools
503
- case 'create_light':
504
- // Normalize transforms
505
- const lightLocObj = args.location ? (toVec3Object(args.location) || { x: 0, y: 0, z: 0 }) : { x: 0, y: 0, z: 0 };
506
- const lightLoc = [lightLocObj.x, lightLocObj.y, lightLocObj.z];
507
- const lightRotObj = args.rotation ? (toRotObject(args.rotation) || { pitch: 0, yaw: 0, roll: 0 }) : { pitch: 0, yaw: 0, roll: 0 };
508
- const lightRot = [lightRotObj.pitch, lightRotObj.yaw, lightRotObj.roll];
509
- switch (args.lightType?.toLowerCase()) {
510
- case 'directional':
511
- result = await tools.lightingTools.createDirectionalLight({
512
- name: args.name,
513
- intensity: args.intensity,
514
- rotation: lightRot
515
- });
516
- break;
517
- case 'point':
518
- result = await tools.lightingTools.createPointLight({
519
- name: args.name,
520
- location: lightLoc,
521
- intensity: args.intensity
522
- });
523
- break;
524
- case 'spot':
525
- result = await tools.lightingTools.createSpotLight({
526
- name: args.name,
527
- location: lightLoc,
528
- rotation: lightRot,
529
- intensity: args.intensity
530
- });
531
- break;
532
- case 'rect':
533
- result = await tools.lightingTools.createRectLight({
534
- name: args.name,
535
- location: lightLoc,
536
- rotation: lightRot,
537
- intensity: args.intensity
538
- });
539
- break;
540
- case 'sky':
541
- result = await tools.lightingTools.createSkyLight({
542
- name: args.name,
543
- intensity: args.intensity,
544
- recapture: true
545
- });
546
- break;
547
- default:
548
- throw new Error(`Unknown light type: ${args.lightType}`);
549
- }
550
- message = result.message || `${args.lightType} light created`;
551
- break;
552
- case 'build_lighting':
553
- result = await tools.lightingTools.buildLighting(args);
554
- message = result.message || 'Lighting built';
555
- break;
556
- // Landscape Tools
557
- case 'create_landscape':
558
- result = await tools.landscapeTools.createLandscape(args);
559
- // Never claim success unless the tool says so; prefer error/message from UE/Python
560
- if (result && typeof result === 'object') {
561
- message = result.message || result.error || (result.success ? `Landscape ${args.name} created` : `Failed to create landscape ${args.name}`);
562
- }
563
- else {
564
- message = `Failed to create landscape ${args.name}`;
565
- }
566
- break;
567
- case 'sculpt_landscape':
568
- result = await tools.landscapeTools.sculptLandscape(args);
569
- message = result.message || 'Landscape sculpted';
570
- break;
571
- // Foliage Tools
572
- case 'add_foliage_type':
573
- result = await tools.foliageTools.addFoliageType(args);
574
- message = result.message || `Foliage type ${args.name} added`;
575
- break;
576
- case 'paint_foliage':
577
- result = await tools.foliageTools.paintFoliage(args);
578
- message = result.message || 'Foliage painted';
579
- break;
580
- // Debug Visualization Tools
581
- case 'draw_debug_shape':
582
- // Convert position object to array if needed
583
- const position = Array.isArray(args.position) ? args.position :
584
- (args.position ? [args.position.x || 0, args.position.y || 0, args.position.z || 0] : [0, 0, 0]);
585
- switch (args.shape?.toLowerCase()) {
586
- case 'line':
587
- result = await tools.debugTools.drawDebugLine({
588
- start: position,
589
- end: args.end || [position[0] + 100, position[1], position[2]],
590
- color: args.color,
591
- duration: args.duration
592
- });
593
- break;
594
- case 'box':
595
- result = await tools.debugTools.drawDebugBox({
596
- center: position,
597
- extent: [args.size, args.size, args.size],
598
- color: args.color,
599
- duration: args.duration
600
- });
601
- break;
602
- case 'sphere':
603
- result = await tools.debugTools.drawDebugSphere({
604
- center: position,
605
- radius: args.size || 50,
606
- color: args.color,
607
- duration: args.duration
608
- });
609
- break;
610
- default:
611
- throw new Error(`Unknown debug shape: ${args.shape}`);
612
- }
613
- message = `Debug ${args.shape} drawn`;
614
- break;
615
- case 'set_view_mode':
616
- result = await tools.debugTools.setViewMode(args);
617
- message = `View mode set to ${args.mode}`;
618
- break;
619
- // Performance Tools
620
- case 'start_profiling':
621
- result = await tools.performanceTools.startProfiling(args);
622
- message = result.message || `${args.type} profiling started`;
623
- break;
624
- case 'show_fps':
625
- result = await tools.performanceTools.showFPS(args);
626
- message = `FPS display ${args.enabled ? 'enabled' : 'disabled'}`;
627
- break;
628
- case 'set_scalability':
629
- result = await tools.performanceTools.setScalability(args);
630
- message = `${args.category} quality set to level ${args.level}`;
631
- break;
632
- // Audio Tools
633
- case 'play_sound':
634
- // Check if sound exists first
635
- const soundCheckPy = `
636
- import unreal, json
637
- path = r"${args.soundPath}"
638
- try:
639
- exists = unreal.EditorAssetLibrary.does_asset_exist(path)
640
- print('SOUNDCHECK:' + json.dumps({'exists': bool(exists)}))
641
- except Exception as e:
642
- print('SOUNDCHECK:' + json.dumps({'exists': False, 'error': str(e)}))
643
- `.trim();
644
- let soundExists = false;
645
- try {
646
- const checkResp = await tools.bridge.executePython(soundCheckPy);
647
- const checkOut = typeof checkResp === 'string' ? checkResp : JSON.stringify(checkResp);
648
- const checkMatch = checkOut.match(/SOUNDCHECK:({.*})/);
649
- if (checkMatch) {
650
- const checkParsed = JSON.parse(checkMatch[1]);
651
- soundExists = checkParsed.exists === true;
652
- }
653
- }
654
- catch { }
655
- if (!soundExists && !args.soundPath.includes('/Engine/')) {
656
- throw new Error(`Sound asset not found: ${args.soundPath}`);
657
- }
658
- if (args.is3D !== false && args.location) {
659
- result = await tools.audioTools.playSoundAtLocation({
660
- soundPath: args.soundPath,
661
- location: args.location,
662
- volume: args.volume,
663
- pitch: args.pitch
664
- });
665
- }
666
- else {
667
- result = await tools.audioTools.playSound2D({
668
- soundPath: args.soundPath,
669
- volume: args.volume,
670
- pitch: args.pitch
671
- });
672
- }
673
- message = `Playing sound: ${args.soundPath}`;
674
- break;
675
- case 'create_ambient_sound':
676
- result = await tools.audioTools.createAmbientSound(args);
677
- message = result.message || `Ambient sound ${args.name} created`;
678
- break;
679
- // UI Tools
680
- case 'create_widget':
681
- result = await tools.uiTools.createWidget(args);
682
- message = `Widget ${args.name} created`;
683
- break;
684
- case 'show_widget':
685
- result = await tools.uiTools.setWidgetVisibility(args);
686
- message = `Widget ${args.widgetName} ${args.visible ? 'shown' : 'hidden'}`;
687
- break;
688
- case 'create_hud':
689
- result = await tools.uiTools.createHUD(args);
690
- message = result.message || `HUD ${args.name} created`;
691
- break;
692
- // Console command execution
693
- case 'console_command':
694
- // Validate command parameter
695
- if (!args.command || typeof args.command !== 'string') {
696
- throw new Error('Invalid command: must be a non-empty string');
697
- }
698
- const command = args.command.trim();
699
- if (command.length === 0) {
700
- // Handle empty command gracefully
701
- result = { success: true, message: 'Empty command ignored' };
702
- message = 'Empty command ignored';
703
- break;
704
- }
705
- // Known problematic patterns that will generate warnings
706
- const problematicPatterns = [
707
- // /^stat fps$/i, // Removed - allow stat fps as user requested
708
- /^invalid_/i,
709
- /^this_is_not/i,
710
- /^\d+$/, // Just numbers
711
- /^[^a-zA-Z]/, // Doesn't start with letter
712
- ];
713
- // Check for known invalid commands
714
- const cmdLower = command.toLowerCase();
715
- const knownInvalid = [
716
- 'invalid_command_xyz',
717
- 'this_is_not_a_valid_command',
718
- 'stat invalid_stat',
719
- 'viewmode invalid_mode',
720
- 'r.invalidcvar',
721
- 'sg.invalidquality'
722
- ];
723
- const isKnownInvalid = knownInvalid.some(invalid => cmdLower === invalid.toLowerCase() || cmdLower.includes(invalid));
724
- // Allow stat fps without replacement - user knows what they want
725
- // if (cmdLower === 'stat fps') {
726
- // command = 'stat unit';
727
- // log.info('Replacing "stat fps" with "stat unit" to avoid warnings');
728
- // }
729
- // Handle commands with special characters that might fail
730
- if (command.includes(';')) {
731
- // Split compound commands
732
- const commands = command.split(';').map((c) => c.trim()).filter((c) => c.length > 0);
733
- if (commands.length > 1) {
734
- // Execute each command separately
735
- const results = [];
736
- for (const cmd of commands) {
737
- try {
738
- await tools.bridge.executeConsoleCommand(cmd);
739
- results.push({ command: cmd, success: true });
740
- }
741
- catch (e) {
742
- results.push({ command: cmd, success: false, error: e.message });
743
- }
744
- }
745
- result = { multiCommand: true, results };
746
- message = `Executed ${results.length} commands`;
747
- break;
748
- }
749
- }
750
- try {
751
- result = await tools.bridge.executeConsoleCommand(command);
752
- if (isKnownInvalid) {
753
- message = `Command executed (likely unrecognized): ${command}`;
754
- result = { ...result, warning: 'Command may not be recognized by Unreal Engine' };
755
- }
756
- else if (problematicPatterns.some(p => p.test(command))) {
757
- message = `Command executed (may have warnings): ${command}`;
758
- result = { ...result, info: 'Command may generate console warnings' };
759
- }
760
- else {
761
- message = `Console command executed: ${command}`;
762
- }
763
- }
764
- catch (error) {
765
- // Don't throw for console commands - they often "succeed" even when unrecognized
766
- log.warn(`Console command error for '${command}':`, error.message);
767
- // Return a warning result instead of failing
768
- result = {
769
- success: false,
770
- command: command,
771
- error: error.message,
772
- warning: 'Command may have failed or been unrecognized'
773
- };
774
- message = `Console command attempted: ${command} (may have failed)`;
775
- }
776
- break;
777
- // New tools implemented here (also used by consolidated handler)
778
- case 'rc_create_preset':
779
- result = await tools.rcTools.createPreset({ name: args.name, path: args.path });
780
- message = result.message || (result.success ? `Preset created at ${result.presetPath}` : result.error);
781
- break;
782
- case 'rc_expose_actor':
783
- result = await tools.rcTools.exposeActor({ presetPath: args.presetPath, actorName: args.actorName });
784
- message = result.message || (result.success ? 'Actor exposed' : result.error);
785
- break;
786
- case 'rc_expose_property':
787
- result = await tools.rcTools.exposeProperty({ presetPath: args.presetPath, objectPath: args.objectPath, propertyName: args.propertyName });
788
- message = result.message || (result.success ? 'Property exposed' : result.error);
789
- break;
790
- case 'rc_list_fields':
791
- result = await tools.rcTools.listFields({ presetPath: args.presetPath });
792
- message = result.message || (result.success ? `Found ${(result.fields || []).length} fields` : result.error);
793
- break;
794
- case 'rc_set_property':
795
- result = await tools.rcTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
796
- message = result.message || (result.success ? 'Property set' : result.error);
797
- break;
798
- case 'rc_get_property':
799
- result = await tools.rcTools.getProperty({ objectPath: args.objectPath, propertyName: args.propertyName });
800
- message = result.message || (result.success ? 'Property retrieved' : result.error);
801
- break;
802
- case 'seq_create':
803
- result = await tools.sequenceTools.create({ name: args.name, path: args.path });
804
- message = result.message || (result.success ? `Sequence created at ${result.sequencePath}` : result.error);
805
- break;
806
- case 'seq_open':
807
- result = await tools.sequenceTools.open({ path: args.path });
808
- message = result.message || (result.success ? 'Sequence opened' : result.error);
809
- break;
810
- case 'seq_add_camera':
811
- result = await tools.sequenceTools.addCamera({ spawnable: args.spawnable });
812
- message = result.message || (result.success ? 'Camera added to sequence' : result.error);
813
- break;
814
- case 'seq_add_actor':
815
- result = await tools.sequenceTools.addActor({ actorName: args.actorName });
816
- message = result.message || (result.success ? 'Actor added to sequence' : result.error);
817
- break;
818
- case 'inspect_object':
819
- result = await tools.introspectionTools.inspectObject({ objectPath: args.objectPath });
820
- message = result.message || (result.success ? 'Object inspected' : result.error);
821
- break;
822
- case 'inspect_set_property':
823
- result = await tools.introspectionTools.setProperty({ objectPath: args.objectPath, propertyName: args.propertyName, value: args.value });
824
- message = result.message || (result.success ? 'Property set' : result.error);
825
- break;
826
- case 'take_screenshot':
827
- result = await tools.visualTools.takeScreenshot({ resolution: args.resolution });
828
- message = result.message || (result.success ? 'Screenshot captured' : result.error);
829
- break;
830
- case 'launch_editor':
831
- result = await tools.engineTools.launchEditor({ editorExe: args.editorExe, projectPath: args.projectPath });
832
- message = result.message || (result.success ? 'Launch requested' : result.error);
833
- break;
834
- case 'quit_editor':
835
- result = await tools.engineTools.quitEditor();
836
- message = result.message || (result.success ? 'Quit requested' : result.error);
837
- break;
838
- default:
839
- throw new Error(`Unknown tool: ${name}`);
840
- }
841
- // Clean the result to prevent circular references
842
- const cleanedResult = result && typeof result === 'object' ? cleanObject(result) : result;
843
- // Return MCP-compliant response format
844
- return {
845
- content: [{
846
- type: 'text',
847
- text: message
848
- }],
849
- // Include result data as metadata for debugging
850
- ...(cleanedResult && typeof cleanedResult === 'object' ? cleanedResult : {})
851
- };
852
- }
853
- catch (err) {
854
- return {
855
- content: [{
856
- type: 'text',
857
- text: `Failed to execute ${name}: ${err}`
858
- }],
859
- isError: true
860
- };
861
- }
862
- }
863
- //# sourceMappingURL=tool-handlers.js.map