unreal-engine-mcp-server 0.3.0 → 0.4.0

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