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