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
@@ -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')) {
@@ -53,17 +53,8 @@ try:
53
53
  proc_mesh_comp = proc_actor.get_component_by_class(unreal.ProceduralMeshComponent)
54
54
  except:
55
55
  # Fallback: Create empty actor and add ProceduralMeshComponent
56
- proc_actor = subsys.spawn_actor_from_class(
57
- unreal.Actor,
58
- location,
59
- unreal.Rotator(0, 0, 0)
60
- )
61
- proc_actor.set_actor_label(f"{name}_ProceduralTerrain")
62
-
63
- # Add procedural mesh component
64
- proc_mesh_comp = unreal.ProceduralMeshComponent()
65
- proc_actor.add_instance_component(proc_mesh_comp)
66
- proc_mesh_comp.register_component()
56
+ # If spawning ProceduralMeshActor failed, surface a clear error about the plugin requirement
57
+ raise Exception("Failed to spawn ProceduralMeshActor. Ensure the 'Procedural Mesh Component' plugin is enabled and available.")
67
58
 
68
59
  if proc_mesh_comp:
69
60
  # Generate terrain mesh
@@ -193,6 +184,10 @@ try:
193
184
  subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
194
185
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
195
186
 
187
+ # Validate Procedural Foliage plugin/classes are available
188
+ if not hasattr(unreal, 'ProceduralFoliageVolume') or not hasattr(unreal, 'ProceduralFoliageSpawner'):
189
+ raise Exception("Procedural Foliage plugin not available. Please enable the 'Procedural Foliage' plugin and try again.")
190
+
196
191
  # Create ProceduralFoliageVolume
197
192
  volume_actor = subsys.spawn_actor_from_class(
198
193
  unreal.ProceduralFoliageVolume,
@@ -200,7 +195,7 @@ try:
200
195
  unreal.Rotator(0, 0, 0)
201
196
  )
202
197
  volume_actor.set_actor_label(f"{name}_ProceduralFoliageVolume")
203
- volume_actor.set_actor_scale3d(bounds_size / 100) # Scale is in meters
198
+ volume_actor.set_actor_scale3d(unreal.Vector(bounds_size.x/100.0, bounds_size.y/100.0, bounds_size.z/100.0)) # Scale is in meters
204
199
 
205
200
  # Get the procedural component
206
201
  proc_comp = volume_actor.procedural_component
@@ -231,13 +226,14 @@ try:
231
226
  )
232
227
 
233
228
  if spawner:
234
- # Configure spawner
235
- spawner.random_seed = seed
236
- spawner.tile_size = max(bounds_size.x, bounds_size.y)
229
+ # Configure spawner (use set_editor_property for read-only attributes)
230
+ spawner.set_editor_property('random_seed', seed)
231
+ spawner.set_editor_property('tile_size', max(bounds_size.x, bounds_size.y))
237
232
 
238
233
  # Create foliage types
239
234
  foliage_types = []
240
- for ft_params in ${JSON.stringify(params.foliageTypes)}:
235
+ ft_input = json.loads(r'''${JSON.stringify(params.foliageTypes)}''')
236
+ for ft_params in ft_input:
241
237
  # Load mesh
242
238
  mesh = unreal.EditorAssetLibrary.load_asset(ft_params['meshPath'])
243
239
  if mesh:
@@ -256,26 +252,26 @@ try:
256
252
  ft_asset = unreal.FoliageType_InstancedStaticMesh()
257
253
 
258
254
  if ft_asset:
259
- # Configure foliage type
260
- ft_asset.mesh = mesh
261
- ft_asset.density = ft_params.get('density', 1.0)
262
- ft_asset.random_yaw = ft_params.get('randomYaw', True)
263
- ft_asset.align_to_normal = ft_params.get('alignToNormal', True)
255
+ # Configure foliage type (use set_editor_property)
256
+ ft_asset.set_editor_property('mesh', mesh)
257
+ ft_asset.set_editor_property('density', ft_params.get('density', 1.0))
258
+ ft_asset.set_editor_property('random_yaw', ft_params.get('randomYaw', True))
259
+ ft_asset.set_editor_property('align_to_normal', ft_params.get('alignToNormal', True))
264
260
 
265
261
  min_scale = ft_params.get('minScale', 0.8)
266
262
  max_scale = ft_params.get('maxScale', 1.2)
267
- ft_asset.scale_x = unreal.FloatInterval(min_scale, max_scale)
268
- ft_asset.scale_y = unreal.FloatInterval(min_scale, max_scale)
269
- ft_asset.scale_z = unreal.FloatInterval(min_scale, max_scale)
263
+ ft_asset.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
264
+ ft_asset.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
265
+ ft_asset.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
270
266
 
271
- ft_obj.foliage_type_object = ft_asset
267
+ ft_obj.set_editor_property('foliage_type_object', ft_asset)
272
268
  foliage_types.append(ft_obj)
273
269
 
274
270
  # Set foliage types on spawner
275
- spawner.foliage_types = foliage_types
271
+ spawner.set_editor_property('foliage_types', foliage_types)
276
272
 
277
273
  # Assign spawner to component
278
- proc_comp.foliage_spawner = spawner
274
+ proc_comp.set_editor_property('foliage_spawner', spawner)
279
275
 
280
276
  # Save spawner asset
281
277
  unreal.EditorAssetLibrary.save_asset(spawner.get_path_name())
@@ -455,23 +451,31 @@ try:
455
451
  # Load mesh
456
452
  mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
457
453
  if mesh:
458
- # Configure grass type
454
+ # Configure grass type (use set_editor_property)
459
455
  grass_variety = unreal.GrassVariety()
460
- grass_variety.grass_mesh = mesh
461
- grass_variety.grass_density = density * 100 # Convert to per square meter
462
- grass_variety.use_grid = True
463
- grass_variety.placement_jitter = 1.0
464
- grass_variety.start_cull_distance = 10000
465
- grass_variety.end_cull_distance = 20000
466
- grass_variety.min_lod = -1
467
- grass_variety.scaling = unreal.GrassScaling.UNIFORM
468
- grass_variety.scale_x = unreal.FloatInterval(min_scale, max_scale)
469
- grass_variety.scale_y = unreal.FloatInterval(min_scale, max_scale)
470
- grass_variety.scale_z = unreal.FloatInterval(min_scale, max_scale)
471
- grass_variety.random_rotation = True
472
- grass_variety.align_to_surface = True
456
+ grass_variety.set_editor_property('grass_mesh', mesh)
457
+ # GrassDensity is PerPlatformFloat in UE5+; set via struct instance
458
+ pp_density = unreal.PerPlatformFloat()
459
+ pp_density.set_editor_property('Default', float(density * 100.0))
460
+ grass_variety.set_editor_property('grass_density', pp_density)
461
+ grass_variety.set_editor_property('use_grid', True)
462
+ grass_variety.set_editor_property('placement_jitter', 1.0)
463
+ # Set cull distances as PerPlatformInt and LOD as int (engine uses mixed types here)
464
+ pp_start = unreal.PerPlatformInt()
465
+ pp_start.set_editor_property('Default', 10000)
466
+ grass_variety.set_editor_property('start_cull_distance', pp_start)
467
+ pp_end = unreal.PerPlatformInt()
468
+ pp_end.set_editor_property('Default', 20000)
469
+ grass_variety.set_editor_property('end_cull_distance', pp_end)
470
+ grass_variety.set_editor_property('min_lod', -1)
471
+ grass_variety.set_editor_property('scaling', unreal.GrassScaling.UNIFORM)
472
+ grass_variety.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
473
+ grass_variety.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
474
+ grass_variety.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
475
+ grass_variety.set_editor_property('random_rotation', True)
476
+ grass_variety.set_editor_property('align_to_surface', True)
473
477
 
474
- grass_type.grass_varieties = [grass_variety]
478
+ grass_type.set_editor_property('grass_varieties', [grass_variety])
475
479
 
476
480
  # Save asset
477
481
  unreal.EditorAssetLibrary.save_asset(grass_type.get_path_name())