unreal-engine-mcp-server 0.2.1

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