unreal-engine-mcp-server 0.3.1 → 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.
@@ -20,13 +20,11 @@ export class DebugVisualizationTools {
20
20
  async pyDraw(scriptBody) {
21
21
  const script = `
22
22
  import unreal
23
- # Use modern UnrealEditorSubsystem instead of deprecated EditorLevelLibrary
23
+ # Strict modern API: require UnrealEditorSubsystem (UE 5.1+)
24
24
  ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
25
- if ues:
26
- world = ues.get_editor_world()
27
- else:
28
- # Fallback to deprecated API if subsystem not available
29
- world = unreal.EditorLevelLibrary.get_editor_world()
25
+ if not ues:
26
+ raise Exception('UnrealEditorSubsystem not available')
27
+ world = ues.get_editor_world()
30
28
  ${scriptBody}
31
29
  `.trim()
32
30
  .replace(/\r?\n/g, '\n');
package/dist/tools/rc.js CHANGED
@@ -142,7 +142,7 @@ except Exception as e:
142
142
  }
143
143
  // Expose an actor by label/name into a preset
144
144
  async exposeActor(params) {
145
- const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
145
+ const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n # Expose with a default-initialized optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
146
146
  const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeActor');
147
147
  const result = this.parsePythonResult(resp, 'exposeActor');
148
148
  // Clear cache for this preset to force refresh
@@ -153,7 +153,7 @@ except Exception as e:
153
153
  }
154
154
  // Expose a property on an object into a preset
155
155
  async exposeProperty(params) {
156
- const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
156
+ const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n # Expose with default optional args struct (cannot pass None)\n args = unreal.RemoteControlOptionalExposeArgs()\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, args)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
157
157
  const resp = await this.executeWithRetry(() => this.bridge.executePython(python), 'exposeProperty');
158
158
  const result = this.parsePythonResult(resp, 'exposeProperty');
159
159
  // Clear cache for this preset to force refresh
@@ -282,12 +282,31 @@ except Exception as e:
282
282
  * Play the current level sequence
283
283
  */
284
284
  async play(params) {
285
+ const loop = params?.loopMode || '';
285
286
  const py = `
286
287
  import unreal, json
288
+
289
+ # Helper to resolve SequencerLoopMode from a friendly string
290
+ def _resolve_loop_mode(mode_str):
291
+ try:
292
+ m = str(mode_str).lower()
293
+ slm = unreal.SequencerLoopMode
294
+ if m in ('once','noloop','no_loop'):
295
+ return getattr(slm, 'SLM_NoLoop', getattr(slm, 'NoLoop'))
296
+ if m in ('loop',):
297
+ return getattr(slm, 'SLM_Loop', getattr(slm, 'Loop'))
298
+ if m in ('pingpong','ping_pong'):
299
+ return getattr(slm, 'SLM_PingPong', getattr(slm, 'PingPong'))
300
+ except Exception:
301
+ pass
302
+ return None
303
+
287
304
  try:
288
305
  unreal.LevelSequenceEditorBlueprintLibrary.play()
289
- ${params?.loopMode ? `unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode('${params.loopMode}')` : ''}
290
- print('RESULT:' + json.dumps({'success': True, 'playing': True}))
306
+ loop_mode = _resolve_loop_mode('${loop}')
307
+ if loop_mode is not None:
308
+ unreal.LevelSequenceEditorBlueprintLibrary.set_loop_mode(loop_mode)
309
+ print('RESULT:' + json.dumps({'success': True, 'playing': True, 'loopMode': '${loop || 'default'}'}))
291
310
  except Exception as e:
292
311
  print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
293
312
  `.trim();
@@ -19,7 +19,12 @@ export declare class ResponseValidator {
19
19
  structuredContent?: any;
20
20
  };
21
21
  /**
22
- * Wrap a tool response with validation
22
+ * Wrap a tool response with validation and MCP-compliant content shape.
23
+ *
24
+ * MCP tools/call responses must contain a `content` array. Many internal
25
+ * handlers return structured JSON objects (e.g., { success, message, ... }).
26
+ * This wrapper serializes such objects into a single text block while keeping
27
+ * existing `content` responses intact.
23
28
  */
24
29
  wrapResponse(toolName: string, response: any): any;
25
30
  /**
@@ -76,14 +76,17 @@ export class ResponseValidator {
76
76
  };
77
77
  }
78
78
  /**
79
- * Wrap a tool response with validation
79
+ * Wrap a tool response with validation and MCP-compliant content shape.
80
+ *
81
+ * MCP tools/call responses must contain a `content` array. Many internal
82
+ * handlers return structured JSON objects (e.g., { success, message, ... }).
83
+ * This wrapper serializes such objects into a single text block while keeping
84
+ * existing `content` responses intact.
80
85
  */
81
86
  wrapResponse(toolName, response) {
82
87
  // Ensure response is safe to serialize first
83
88
  try {
84
- // The response should already be cleaned, but double-check
85
89
  if (response && typeof response === 'object') {
86
- // Make sure we can serialize it
87
90
  JSON.stringify(response);
88
91
  }
89
92
  }
@@ -91,21 +94,45 @@ export class ResponseValidator {
91
94
  log.error(`Response for ${toolName} contains circular references, cleaning...`);
92
95
  response = cleanObject(response);
93
96
  }
94
- const validation = this.validateResponse(toolName, response);
95
- // Add validation metadata
97
+ // If handler already returned MCP content, keep it as-is (still validate)
98
+ const alreadyMcpShaped = response && typeof response === 'object' && Array.isArray(response.content);
99
+ // Choose the payload to validate: if already MCP-shaped, validate the
100
+ // structured content extracted from text; otherwise validate the object directly.
101
+ const validationTarget = alreadyMcpShaped ? response : response;
102
+ const validation = this.validateResponse(toolName, validationTarget);
96
103
  if (!validation.valid) {
97
104
  log.warn(`Tool ${toolName} response validation failed:`, validation.errors);
98
- // Add warning to response but don't fail
99
- if (response && typeof response === 'object') {
100
- response._validation = {
101
- valid: false,
102
- errors: validation.errors
103
- };
105
+ }
106
+ // If it's already MCP-shaped, return as-is (optionally append validation meta)
107
+ if (alreadyMcpShaped) {
108
+ if (!validation.valid) {
109
+ try {
110
+ response._validation = { valid: false, errors: validation.errors };
111
+ }
112
+ catch { }
104
113
  }
114
+ return response;
115
+ }
116
+ // Otherwise, wrap structured result into MCP content
117
+ let text;
118
+ try {
119
+ // Pretty-print small objects for readability
120
+ text = typeof response === 'string'
121
+ ? response
122
+ : JSON.stringify(response ?? { success: true }, null, 2);
123
+ }
124
+ catch (_e) {
125
+ text = String(response);
126
+ }
127
+ const wrapped = {
128
+ content: [
129
+ { type: 'text', text }
130
+ ]
131
+ };
132
+ if (!validation.valid) {
133
+ wrapped._validation = { valid: false, errors: validation.errors };
105
134
  }
106
- // Don't add structuredContent to the response - it's for internal validation only
107
- // Adding it can cause circular references
108
- return response;
135
+ return wrapped;
109
136
  }
110
137
  /**
111
138
  * Get validation statistics
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unreal-engine-mcp-server",
3
- "version": "0.3.1",
4
- "description": "Production-ready MCP server for Unreal Engine integration with consolidated and individual tool modes",
3
+ "version": "0.4.0",
4
+ "description": "Production-ready MCP server for Unreal Engine integration using consolidated tools only",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -34,9 +34,9 @@
34
34
  "license": "MIT",
35
35
  "mcpName": "io.github.ChiR24/unreal-engine-mcp",
36
36
  "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.4.0",
37
+ "@modelcontextprotocol/sdk": "^1.18.1",
38
38
  "ajv": "^8.12.0",
39
- "axios": "^1.7.2",
39
+ "axios": "^1.12.2",
40
40
  "dotenv": "^16.4.5",
41
41
  "ws": "^8.18.0",
42
42
  "yargs": "^17.7.2",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3
3
  "name": "io.github.ChiR24/unreal-engine-mcp",
4
4
  "description": "Production-ready MCP server for Unreal Engine with comprehensive game development tools",
5
- "version": "0.3.1",
5
+ "version": "0.4.0",
6
6
  "packages": [
7
7
  {
8
8
  "registry_type": "npm",
9
9
  "registry_base_url": "https://registry.npmjs.org",
10
10
  "identifier": "unreal-engine-mcp-server",
11
- "version": "0.3.1",
11
+ "version": "0.4.0",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import { BlueprintTools } from './tools/blueprint.js';
17
17
  import { LevelTools } from './tools/level.js';
18
18
  import { LightingTools } from './tools/lighting.js';
19
19
  import { LandscapeTools } from './tools/landscape.js';
20
+ import { BuildEnvironmentAdvanced } from './tools/build_environment_advanced.js';
20
21
  import { FoliageTools } from './tools/foliage.js';
21
22
  import { DebugVisualizationTools } from './tools/debug.js';
22
23
  import { PerformanceTools } from './tools/performance.js';
@@ -27,8 +28,6 @@ import { SequenceTools } from './tools/sequence.js';
27
28
  import { IntrospectionTools } from './tools/introspection.js';
28
29
  import { VisualTools } from './tools/visual.js';
29
30
  import { EngineTools } from './tools/engine.js';
30
- import { toolDefinitions } from './tools/tool-definitions.js';
31
- import { handleToolCall } from './tools/tool-handlers.js';
32
31
  import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
33
32
  import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
34
33
  import { prompts } from './prompts/index.js';
@@ -81,14 +80,13 @@ let lastHealthSuccessAt = 0;
81
80
 
82
81
  // Configuration
83
82
  const CONFIG = {
84
- // Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
85
- USE_CONSOLIDATED_TOOLS: process.env.USE_CONSOLIDATED_TOOLS !== 'false',
83
+ // Tooling: use consolidated tools only (13 tools)
86
84
  // Connection retry settings
87
85
  MAX_RETRY_ATTEMPTS: 3,
88
86
  RETRY_DELAY_MS: 2000,
89
87
  // Server info
90
88
  SERVER_NAME: 'unreal-engine-mcp',
91
- SERVER_VERSION: '0.3.1',
89
+ SERVER_VERSION: '0.4.0',
92
90
  // Monitoring
93
91
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
94
92
  };
@@ -149,9 +147,9 @@ export async function createServer() {
149
147
  // Disable auto-reconnect loops; connect only on-demand
150
148
  bridge.setAutoReconnectEnabled(false);
151
149
 
152
- // Initialize response validation with schemas
150
+ // Initialize response validation with schemas
153
151
  log.debug('Initializing response validation...');
154
- const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
152
+ const toolDefs = consolidatedToolDefinitions;
155
153
  toolDefs.forEach((tool: any) => {
156
154
  if (tool.outputSchema) {
157
155
  responseValidator.registerSchema(tool.name, tool.outputSchema);
@@ -227,6 +225,7 @@ export async function createServer() {
227
225
  const lightingTools = new LightingTools(bridge);
228
226
  const landscapeTools = new LandscapeTools(bridge);
229
227
  const foliageTools = new FoliageTools(bridge);
228
+ const buildEnvAdvanced = new BuildEnvironmentAdvanced(bridge);
230
229
  const debugTools = new DebugVisualizationTools(bridge);
231
230
  const performanceTools = new PerformanceTools(bridge);
232
231
  const audioTools = new AudioTools(bridge);
@@ -433,15 +432,15 @@ export async function createServer() {
433
432
  throw new Error(`Unknown resource: ${uri}`);
434
433
  });
435
434
 
436
- // Handle tool listing - switch between consolidated (13) or individual (36) tools
435
+ // Handle tool listing - consolidated tools only
437
436
  server.setRequestHandler(ListToolsRequestSchema, async () => {
438
- log.info(`Serving ${CONFIG.USE_CONSOLIDATED_TOOLS ? 'consolidated' : 'individual'} tools`);
437
+ log.info('Serving consolidated tools');
439
438
  return {
440
- tools: CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions
439
+ tools: consolidatedToolDefinitions
441
440
  };
442
441
  });
443
442
 
444
- // Handle tool calls - switch between consolidated (13) or individual (36) tools
443
+ // Handle tool calls - consolidated tools only (13)
445
444
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
446
445
  const { name, arguments: args } = request.params;
447
446
  const startTime = Date.now();
@@ -460,7 +459,7 @@ export async function createServer() {
460
459
  return notConnected;
461
460
  }
462
461
 
463
- // Create tools object for handler
462
+ // Create tools object for handler
464
463
  const tools = {
465
464
  actorTools,
466
465
  assetTools,
@@ -474,6 +473,7 @@ export async function createServer() {
474
473
  lightingTools,
475
474
  landscapeTools,
476
475
  foliageTools,
476
+ buildEnvAdvanced,
477
477
  debugTools,
478
478
  performanceTools,
479
479
  audioTools,
@@ -483,19 +483,18 @@ export async function createServer() {
483
483
  introspectionTools,
484
484
  visualTools,
485
485
  engineTools,
486
+ // Resources for listing and info
487
+ assetResources,
488
+ actorResources,
489
+ levelResources,
486
490
  bridge
487
491
  };
488
492
 
489
- // Use consolidated or individual handler based on configuration
493
+ // Execute consolidated tool handler
490
494
  try {
491
495
  log.debug(`Executing tool: ${name}`);
492
496
 
493
- let result;
494
- if (CONFIG.USE_CONSOLIDATED_TOOLS) {
495
- result = await handleConsolidatedToolCall(name, args, tools);
496
- } else {
497
- result = await handleToolCall(name, args, tools);
498
- }
497
+ let result = await handleConsolidatedToolCall(name, args, tools);
499
498
 
500
499
  log.debug(`Tool ${name} returned result`);
501
500
 
@@ -10,11 +10,35 @@ export class AssetResources {
10
10
  return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
11
  }
12
12
 
13
+ // Normalize UE content paths:
14
+ // - Map '/Content' -> '/Game'
15
+ // - Ensure forward slashes
16
+ private normalizeDir(dir: string): string {
17
+ try {
18
+ if (!dir || typeof dir !== 'string') return '/Game';
19
+ let d = dir.replace(/\\/g, '/');
20
+ if (!d.startsWith('/')) d = '/' + d;
21
+ if (d.toLowerCase().startsWith('/content')) {
22
+ d = '/Game' + d.substring('/Content'.length);
23
+ }
24
+ // Collapse multiple slashes
25
+ d = d.replace(/\/+/g, '/');
26
+ // Remove trailing slash except root
27
+ if (d.length > 1) d = d.replace(/\/$/, '');
28
+ return d;
29
+ } catch {
30
+ return '/Game';
31
+ }
32
+ }
33
+
13
34
  async list(dir = '/Game', _recursive = false, limit = 50) {
14
35
  // ALWAYS use non-recursive listing to show only immediate children
15
36
  // This prevents timeouts and makes navigation clearer
16
37
  _recursive = false; // Force non-recursive
17
38
 
39
+ // Normalize directory first
40
+ dir = this.normalizeDir(dir);
41
+
18
42
  // Cache fast-path
19
43
  try {
20
44
  const key = this.makeKey(dir, false);
@@ -50,7 +74,8 @@ export class AssetResources {
50
74
  const safePageSize = Math.min(pageSize, 50);
51
75
  const offset = page * safePageSize;
52
76
 
53
- // Check cache for this specific page
77
+ // Normalize directory and check cache for this specific page
78
+ dir = this.normalizeDir(dir);
54
79
  const cacheKey = this.makeKey(dir, recursive, page);
55
80
  const cached = this.cache.get(cacheKey);
56
81
  if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
@@ -102,8 +127,8 @@ export class AssetResources {
102
127
  }
103
128
 
104
129
  /**
105
- * Directory-based listing for paths with too many assets
106
- * Shows only immediate children (folders and files) to avoid timeouts
130
+ * Directory-based listing of immediate children using AssetRegistry.
131
+ * Returns both subfolders and assets at the given path.
107
132
  */
108
133
  private async listDirectoryOnly(dir: string, _recursive: boolean, limit: number) {
109
134
  // Always return only immediate children to avoid timeout and improve navigation
@@ -112,65 +137,43 @@ export class AssetResources {
112
137
  import unreal
113
138
  import json
114
139
 
115
- _dir = r"${dir}"
140
+ _dir = r"${this.normalizeDir(dir)}"
116
141
 
117
142
  try:
118
- # ALWAYS non-recursive - get only immediate children
119
- all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
120
-
121
- # Organize into immediate children only
122
- immediate_folders = set()
123
- immediate_assets = []
124
-
125
- for path in all_paths:
126
- # Remove the base directory to get relative path
127
- relative = path.replace(_dir, '').strip('/')
128
- if not relative:
129
- continue
130
-
131
- # Split to check depth
132
- parts = relative.split('/')
133
-
134
- if len(parts) == 1:
135
- # This is an immediate child asset
136
- immediate_assets.append(path)
137
- elif len(parts) > 1:
138
- # This indicates a subfolder exists
139
- immediate_folders.add(parts[0])
140
-
141
- result = []
142
-
143
- # Add folders first
144
- for folder in sorted(immediate_folders):
145
- result.append({
146
- 'n': folder,
147
- 'p': _dir + '/' + folder,
148
- 'c': 'Folder',
149
- 'isFolder': True
150
- })
151
-
152
- # Add immediate assets (limit to prevent socket issues)
153
- for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
154
- name = asset_path.split('/')[-1].split('.')[0]
155
- result.append({
156
- 'n': name,
157
- 'p': asset_path,
158
- 'c': 'Asset'
159
- })
160
-
161
- # Always showing immediate children only
162
- note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
163
-
143
+ ar = unreal.AssetRegistryHelpers.get_asset_registry()
144
+ # Immediate subfolders
145
+ sub_paths = ar.get_sub_paths(_dir, False)
146
+ folders_list = []
147
+ for p in sub_paths:
148
+ try:
149
+ name = p.split('/')[-1]
150
+ folders_list.append({'n': name, 'p': p})
151
+ except Exception:
152
+ pass
153
+
154
+ # Immediate assets at this path
155
+ assets_data = ar.get_assets_by_path(_dir, False)
156
+ assets = []
157
+ for a in assets_data[:${limit}]:
158
+ try:
159
+ assets.append({
160
+ 'n': str(a.asset_name),
161
+ 'p': str(a.object_path),
162
+ 'c': str(a.asset_class)
163
+ })
164
+ except Exception:
165
+ pass
166
+
164
167
  print("RESULT:" + json.dumps({
165
- 'success': True,
166
- 'assets': result,
167
- 'count': len(result),
168
- 'folders': len(immediate_folders),
169
- 'files': len(immediate_assets),
170
- 'note': note
168
+ 'success': True,
169
+ 'path': _dir,
170
+ 'folders': len(folders_list),
171
+ 'files': len(assets),
172
+ 'folders_list': folders_list,
173
+ 'assets': assets
171
174
  }))
172
175
  except Exception as e:
173
- print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
176
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
174
177
  `.trim();
175
178
 
176
179
  const resp = await this.bridge.executePython(py);
@@ -188,21 +191,37 @@ except Exception as e:
188
191
  try {
189
192
  const parsed = JSON.parse(m[1]);
190
193
  if (parsed.success) {
191
- // Transform to standard format
192
- const assets = parsed.assets.map((a: any) => ({
194
+ // Map folders and assets to a clear response
195
+ const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f: any) => ({
196
+ Name: f.n,
197
+ Path: f.p,
198
+ Class: 'Folder',
199
+ isFolder: true
200
+ })) : [];
201
+
202
+ const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a: any) => ({
193
203
  Name: a.n,
194
204
  Path: a.p,
195
- Class: a.c,
196
- isFolder: a.isFolder || false
197
- }));
198
-
205
+ Class: a.c || 'Asset',
206
+ isFolder: false
207
+ })) : [];
208
+
209
+ const total = foldersArr.length + assetsArr.length;
210
+ const summary = {
211
+ total,
212
+ folders: foldersArr.length,
213
+ assets: assetsArr.length
214
+ };
215
+
199
216
  return {
200
- assets,
201
- count: parsed.count,
202
- folders: parsed.folders,
203
- files: parsed.files,
204
- note: parsed.note,
205
- method: 'directory_listing'
217
+ success: true,
218
+ path: parsed.path || this.normalizeDir(dir),
219
+ summary,
220
+ foldersList: foldersArr,
221
+ assets: assetsArr,
222
+ count: total,
223
+ note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
224
+ method: 'asset_registry_listing'
206
225
  };
207
226
  }
208
227
  } catch {}
@@ -213,10 +232,13 @@ except Exception as e:
213
232
 
214
233
  // Fallback: return empty with explanation
215
234
  return {
235
+ success: true,
236
+ path: this.normalizeDir(dir),
237
+ summary: { total: 0, folders: 0, assets: 0 },
238
+ foldersList: [],
216
239
  assets: [],
217
- warning: 'Directory contains too many assets. Showing immediate children only.',
218
- suggestion: 'Navigate to specific subdirectories for detailed listings.',
219
- method: 'directory_timeout_fallback'
240
+ warning: 'No items at this path or failed to query AssetRegistry.',
241
+ method: 'asset_registry_fallback'
220
242
  };
221
243
  }
222
244
 
@@ -226,9 +248,11 @@ except Exception as e:
226
248
  return false;
227
249
  }
228
250
 
251
+ // Normalize asset path (support users passing /Content/...)
252
+ const ap = this.normalizeDir(assetPath);
229
253
  const py = `
230
254
  import unreal
231
- apath = r"${assetPath}"
255
+ apath = r"${ap}"
232
256
  try:
233
257
  exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
234
258
  print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
@@ -56,9 +56,9 @@ export class LevelResources {
56
56
  }
57
57
 
58
58
  async saveCurrentLevel() {
59
- // Prefer Python save (or LevelEditorSubsystem) then fallback
59
+ // Strict modern API: require LevelEditorSubsystem
60
60
  try {
61
- const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les: les.save_current_level()\n else: unreal.EditorLevelLibrary.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
61
+ const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if not les:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': \'LevelEditorSubsystem not available\'}))\n else:\n les.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
62
62
  const resp: any = await this.bridge.executePython(py);
63
63
  // Handle LogOutput format from executePython
64
64
  let out = '';
@@ -7,8 +7,12 @@ export class AssetTools {
7
7
 
8
8
  async importAsset(sourcePath: string, destinationPath: string) {
9
9
  try {
10
- // Sanitize destination path (remove trailing slash)
11
- const cleanDest = destinationPath.replace(/\/$/, '');
10
+ // Sanitize destination path (remove trailing slash) and normalize UE path
11
+ let cleanDest = destinationPath.replace(/\/$/, '');
12
+ // Map /Content -> /Game for UE asset destinations
13
+ if (/^\/?content(\/|$)/i.test(cleanDest)) {
14
+ cleanDest = '/Game' + cleanDest.replace(/^\/?content/i, '');
15
+ }
12
16
 
13
17
  // Create test FBX file if it's a test file
14
18
  if (sourcePath.includes('test_model.fbx')) {