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
@@ -9,10 +9,36 @@ export class AssetResources {
9
9
  makeKey(dir, recursive, page) {
10
10
  return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
11
  }
12
+ // Normalize UE content paths:
13
+ // - Map '/Content' -> '/Game'
14
+ // - Ensure forward slashes
15
+ normalizeDir(dir) {
16
+ try {
17
+ if (!dir || typeof dir !== 'string')
18
+ return '/Game';
19
+ let d = dir.replace(/\\/g, '/');
20
+ if (!d.startsWith('/'))
21
+ d = '/' + d;
22
+ if (d.toLowerCase().startsWith('/content')) {
23
+ d = '/Game' + d.substring('/Content'.length);
24
+ }
25
+ // Collapse multiple slashes
26
+ d = d.replace(/\/+/g, '/');
27
+ // Remove trailing slash except root
28
+ if (d.length > 1)
29
+ d = d.replace(/\/$/, '');
30
+ return d;
31
+ }
32
+ catch {
33
+ return '/Game';
34
+ }
35
+ }
12
36
  async list(dir = '/Game', _recursive = false, limit = 50) {
13
37
  // ALWAYS use non-recursive listing to show only immediate children
14
38
  // This prevents timeouts and makes navigation clearer
15
39
  _recursive = false; // Force non-recursive
40
+ // Normalize directory first
41
+ dir = this.normalizeDir(dir);
16
42
  // Cache fast-path
17
43
  try {
18
44
  const key = this.makeKey(dir, false);
@@ -45,7 +71,8 @@ export class AssetResources {
45
71
  // Ensure pageSize doesn't exceed safe limit
46
72
  const safePageSize = Math.min(pageSize, 50);
47
73
  const offset = page * safePageSize;
48
- // Check cache for this specific page
74
+ // Normalize directory and check cache for this specific page
75
+ dir = this.normalizeDir(dir);
49
76
  const cacheKey = this.makeKey(dir, recursive, page);
50
77
  const cached = this.cache.get(cacheKey);
51
78
  if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
@@ -91,8 +118,8 @@ export class AssetResources {
91
118
  };
92
119
  }
93
120
  /**
94
- * Directory-based listing for paths with too many assets
95
- * Shows only immediate children (folders and files) to avoid timeouts
121
+ * Directory-based listing of immediate children using AssetRegistry.
122
+ * Returns both subfolders and assets at the given path.
96
123
  */
97
124
  async listDirectoryOnly(dir, _recursive, limit) {
98
125
  // Always return only immediate children to avoid timeout and improve navigation
@@ -101,65 +128,43 @@ export class AssetResources {
101
128
  import unreal
102
129
  import json
103
130
 
104
- _dir = r"${dir}"
131
+ _dir = r"${this.normalizeDir(dir)}"
105
132
 
106
133
  try:
107
- # ALWAYS non-recursive - get only immediate children
108
- all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
109
-
110
- # Organize into immediate children only
111
- immediate_folders = set()
112
- immediate_assets = []
113
-
114
- for path in all_paths:
115
- # Remove the base directory to get relative path
116
- relative = path.replace(_dir, '').strip('/')
117
- if not relative:
118
- continue
119
-
120
- # Split to check depth
121
- parts = relative.split('/')
122
-
123
- if len(parts) == 1:
124
- # This is an immediate child asset
125
- immediate_assets.append(path)
126
- elif len(parts) > 1:
127
- # This indicates a subfolder exists
128
- immediate_folders.add(parts[0])
129
-
130
- result = []
131
-
132
- # Add folders first
133
- for folder in sorted(immediate_folders):
134
- result.append({
135
- 'n': folder,
136
- 'p': _dir + '/' + folder,
137
- 'c': 'Folder',
138
- 'isFolder': True
139
- })
140
-
141
- # Add immediate assets (limit to prevent socket issues)
142
- for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
143
- name = asset_path.split('/')[-1].split('.')[0]
144
- result.append({
145
- 'n': name,
146
- 'p': asset_path,
147
- 'c': 'Asset'
148
- })
149
-
150
- # Always showing immediate children only
151
- note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
152
-
134
+ ar = unreal.AssetRegistryHelpers.get_asset_registry()
135
+ # Immediate subfolders
136
+ sub_paths = ar.get_sub_paths(_dir, False)
137
+ folders_list = []
138
+ for p in sub_paths:
139
+ try:
140
+ name = p.split('/')[-1]
141
+ folders_list.append({'n': name, 'p': p})
142
+ except Exception:
143
+ pass
144
+
145
+ # Immediate assets at this path
146
+ assets_data = ar.get_assets_by_path(_dir, False)
147
+ assets = []
148
+ for a in assets_data[:${limit}]:
149
+ try:
150
+ assets.append({
151
+ 'n': str(a.asset_name),
152
+ 'p': str(a.object_path),
153
+ 'c': str(a.asset_class)
154
+ })
155
+ except Exception:
156
+ pass
157
+
153
158
  print("RESULT:" + json.dumps({
154
- 'success': True,
155
- 'assets': result,
156
- 'count': len(result),
157
- 'folders': len(immediate_folders),
158
- 'files': len(immediate_assets),
159
- 'note': note
159
+ 'success': True,
160
+ 'path': _dir,
161
+ 'folders': len(folders_list),
162
+ 'files': len(assets),
163
+ 'folders_list': folders_list,
164
+ 'assets': assets
160
165
  }))
161
166
  except Exception as e:
162
- print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
167
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'path': _dir}))
163
168
  `.trim();
164
169
  const resp = await this.bridge.executePython(py);
165
170
  let output = '';
@@ -177,20 +182,34 @@ except Exception as e:
177
182
  try {
178
183
  const parsed = JSON.parse(m[1]);
179
184
  if (parsed.success) {
180
- // Transform to standard format
181
- const assets = parsed.assets.map((a) => ({
185
+ // Map folders and assets to a clear response
186
+ const foldersArr = Array.isArray(parsed.folders_list) ? parsed.folders_list.map((f) => ({
187
+ Name: f.n,
188
+ Path: f.p,
189
+ Class: 'Folder',
190
+ isFolder: true
191
+ })) : [];
192
+ const assetsArr = Array.isArray(parsed.assets) ? parsed.assets.map((a) => ({
182
193
  Name: a.n,
183
194
  Path: a.p,
184
- Class: a.c,
185
- isFolder: a.isFolder || false
186
- }));
195
+ Class: a.c || 'Asset',
196
+ isFolder: false
197
+ })) : [];
198
+ const total = foldersArr.length + assetsArr.length;
199
+ const summary = {
200
+ total,
201
+ folders: foldersArr.length,
202
+ assets: assetsArr.length
203
+ };
187
204
  return {
188
- assets,
189
- count: parsed.count,
190
- folders: parsed.folders,
191
- files: parsed.files,
192
- note: parsed.note,
193
- method: 'directory_listing'
205
+ success: true,
206
+ path: parsed.path || this.normalizeDir(dir),
207
+ summary,
208
+ foldersList: foldersArr,
209
+ assets: assetsArr,
210
+ count: total,
211
+ note: `Immediate children of ${parsed.path || this.normalizeDir(dir)}: ${foldersArr.length} folder(s), ${assetsArr.length} asset(s)`,
212
+ method: 'asset_registry_listing'
194
213
  };
195
214
  }
196
215
  }
@@ -202,10 +221,13 @@ except Exception as e:
202
221
  }
203
222
  // Fallback: return empty with explanation
204
223
  return {
224
+ success: true,
225
+ path: this.normalizeDir(dir),
226
+ summary: { total: 0, folders: 0, assets: 0 },
227
+ foldersList: [],
205
228
  assets: [],
206
- warning: 'Directory contains too many assets. Showing immediate children only.',
207
- suggestion: 'Navigate to specific subdirectories for detailed listings.',
208
- method: 'directory_timeout_fallback'
229
+ warning: 'No items at this path or failed to query AssetRegistry.',
230
+ method: 'asset_registry_fallback'
209
231
  };
210
232
  }
211
233
  async find(assetPath) {
@@ -213,9 +235,11 @@ except Exception as e:
213
235
  if (!assetPath || typeof assetPath !== 'string' || assetPath.trim() === '' || assetPath.endsWith('/')) {
214
236
  return false;
215
237
  }
238
+ // Normalize asset path (support users passing /Content/...)
239
+ const ap = this.normalizeDir(assetPath);
216
240
  const py = `
217
241
  import unreal
218
- apath = r"${assetPath}"
242
+ apath = r"${ap}"
219
243
  try:
220
244
  exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
221
245
  print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
@@ -62,9 +62,9 @@ export class LevelResources {
62
62
  }
63
63
  }
64
64
  async saveCurrentLevel() {
65
- // Prefer Python save (or LevelEditorSubsystem) then fallback
65
+ // Strict modern API: require LevelEditorSubsystem
66
66
  try {
67
- 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();
67
+ 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();
68
68
  const resp = await this.bridge.executePython(py);
69
69
  // Handle LogOutput format from executePython
70
70
  let out = '';
@@ -7,8 +7,12 @@ export class AssetTools {
7
7
  }
8
8
  async importAsset(sourcePath, destinationPath) {
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
  // Create test FBX file if it's a test file
13
17
  if (sourcePath.includes('test_model.fbx')) {
14
18
  // Create the file outside of Python, before import
@@ -45,17 +45,8 @@ try:
45
45
  proc_mesh_comp = proc_actor.get_component_by_class(unreal.ProceduralMeshComponent)
46
46
  except:
47
47
  # Fallback: Create empty actor and add ProceduralMeshComponent
48
- proc_actor = subsys.spawn_actor_from_class(
49
- unreal.Actor,
50
- location,
51
- unreal.Rotator(0, 0, 0)
52
- )
53
- proc_actor.set_actor_label(f"{name}_ProceduralTerrain")
54
-
55
- # Add procedural mesh component
56
- proc_mesh_comp = unreal.ProceduralMeshComponent()
57
- proc_actor.add_instance_component(proc_mesh_comp)
58
- proc_mesh_comp.register_component()
48
+ # If spawning ProceduralMeshActor failed, surface a clear error about the plugin requirement
49
+ raise Exception("Failed to spawn ProceduralMeshActor. Ensure the 'Procedural Mesh Component' plugin is enabled and available.")
59
50
 
60
51
  if proc_mesh_comp:
61
52
  # Generate terrain mesh
@@ -170,6 +161,10 @@ try:
170
161
  subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
171
162
  asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
172
163
 
164
+ # Validate Procedural Foliage plugin/classes are available
165
+ if not hasattr(unreal, 'ProceduralFoliageVolume') or not hasattr(unreal, 'ProceduralFoliageSpawner'):
166
+ raise Exception("Procedural Foliage plugin not available. Please enable the 'Procedural Foliage' plugin and try again.")
167
+
173
168
  # Create ProceduralFoliageVolume
174
169
  volume_actor = subsys.spawn_actor_from_class(
175
170
  unreal.ProceduralFoliageVolume,
@@ -177,7 +172,7 @@ try:
177
172
  unreal.Rotator(0, 0, 0)
178
173
  )
179
174
  volume_actor.set_actor_label(f"{name}_ProceduralFoliageVolume")
180
- volume_actor.set_actor_scale3d(bounds_size / 100) # Scale is in meters
175
+ 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
181
176
 
182
177
  # Get the procedural component
183
178
  proc_comp = volume_actor.procedural_component
@@ -208,13 +203,14 @@ try:
208
203
  )
209
204
 
210
205
  if spawner:
211
- # Configure spawner
212
- spawner.random_seed = seed
213
- spawner.tile_size = max(bounds_size.x, bounds_size.y)
206
+ # Configure spawner (use set_editor_property for read-only attributes)
207
+ spawner.set_editor_property('random_seed', seed)
208
+ spawner.set_editor_property('tile_size', max(bounds_size.x, bounds_size.y))
214
209
 
215
210
  # Create foliage types
216
211
  foliage_types = []
217
- for ft_params in ${JSON.stringify(params.foliageTypes)}:
212
+ ft_input = json.loads(r'''${JSON.stringify(params.foliageTypes)}''')
213
+ for ft_params in ft_input:
218
214
  # Load mesh
219
215
  mesh = unreal.EditorAssetLibrary.load_asset(ft_params['meshPath'])
220
216
  if mesh:
@@ -233,26 +229,26 @@ try:
233
229
  ft_asset = unreal.FoliageType_InstancedStaticMesh()
234
230
 
235
231
  if ft_asset:
236
- # Configure foliage type
237
- ft_asset.mesh = mesh
238
- ft_asset.density = ft_params.get('density', 1.0)
239
- ft_asset.random_yaw = ft_params.get('randomYaw', True)
240
- ft_asset.align_to_normal = ft_params.get('alignToNormal', True)
232
+ # Configure foliage type (use set_editor_property)
233
+ ft_asset.set_editor_property('mesh', mesh)
234
+ ft_asset.set_editor_property('density', ft_params.get('density', 1.0))
235
+ ft_asset.set_editor_property('random_yaw', ft_params.get('randomYaw', True))
236
+ ft_asset.set_editor_property('align_to_normal', ft_params.get('alignToNormal', True))
241
237
 
242
238
  min_scale = ft_params.get('minScale', 0.8)
243
239
  max_scale = ft_params.get('maxScale', 1.2)
244
- ft_asset.scale_x = unreal.FloatInterval(min_scale, max_scale)
245
- ft_asset.scale_y = unreal.FloatInterval(min_scale, max_scale)
246
- ft_asset.scale_z = unreal.FloatInterval(min_scale, max_scale)
240
+ ft_asset.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
241
+ ft_asset.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
242
+ ft_asset.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
247
243
 
248
- ft_obj.foliage_type_object = ft_asset
244
+ ft_obj.set_editor_property('foliage_type_object', ft_asset)
249
245
  foliage_types.append(ft_obj)
250
246
 
251
247
  # Set foliage types on spawner
252
- spawner.foliage_types = foliage_types
248
+ spawner.set_editor_property('foliage_types', foliage_types)
253
249
 
254
250
  # Assign spawner to component
255
- proc_comp.foliage_spawner = spawner
251
+ proc_comp.set_editor_property('foliage_spawner', spawner)
256
252
 
257
253
  # Save spawner asset
258
254
  unreal.EditorAssetLibrary.save_asset(spawner.get_path_name())
@@ -413,23 +409,31 @@ try:
413
409
  # Load mesh
414
410
  mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
415
411
  if mesh:
416
- # Configure grass type
412
+ # Configure grass type (use set_editor_property)
417
413
  grass_variety = unreal.GrassVariety()
418
- grass_variety.grass_mesh = mesh
419
- grass_variety.grass_density = density * 100 # Convert to per square meter
420
- grass_variety.use_grid = True
421
- grass_variety.placement_jitter = 1.0
422
- grass_variety.start_cull_distance = 10000
423
- grass_variety.end_cull_distance = 20000
424
- grass_variety.min_lod = -1
425
- grass_variety.scaling = unreal.GrassScaling.UNIFORM
426
- grass_variety.scale_x = unreal.FloatInterval(min_scale, max_scale)
427
- grass_variety.scale_y = unreal.FloatInterval(min_scale, max_scale)
428
- grass_variety.scale_z = unreal.FloatInterval(min_scale, max_scale)
429
- grass_variety.random_rotation = True
430
- grass_variety.align_to_surface = True
414
+ grass_variety.set_editor_property('grass_mesh', mesh)
415
+ # GrassDensity is PerPlatformFloat in UE5+; set via struct instance
416
+ pp_density = unreal.PerPlatformFloat()
417
+ pp_density.set_editor_property('Default', float(density * 100.0))
418
+ grass_variety.set_editor_property('grass_density', pp_density)
419
+ grass_variety.set_editor_property('use_grid', True)
420
+ grass_variety.set_editor_property('placement_jitter', 1.0)
421
+ # Set cull distances as PerPlatformInt and LOD as int (engine uses mixed types here)
422
+ pp_start = unreal.PerPlatformInt()
423
+ pp_start.set_editor_property('Default', 10000)
424
+ grass_variety.set_editor_property('start_cull_distance', pp_start)
425
+ pp_end = unreal.PerPlatformInt()
426
+ pp_end.set_editor_property('Default', 20000)
427
+ grass_variety.set_editor_property('end_cull_distance', pp_end)
428
+ grass_variety.set_editor_property('min_lod', -1)
429
+ grass_variety.set_editor_property('scaling', unreal.GrassScaling.UNIFORM)
430
+ grass_variety.set_editor_property('scale_x', unreal.FloatInterval(min_scale, max_scale))
431
+ grass_variety.set_editor_property('scale_y', unreal.FloatInterval(min_scale, max_scale))
432
+ grass_variety.set_editor_property('scale_z', unreal.FloatInterval(min_scale, max_scale))
433
+ grass_variety.set_editor_property('random_rotation', True)
434
+ grass_variety.set_editor_property('align_to_surface', True)
431
435
 
432
- grass_type.grass_varieties = [grass_variety]
436
+ grass_type.set_editor_property('grass_varieties', [grass_variety])
433
437
 
434
438
  # Save asset
435
439
  unreal.EditorAssetLibrary.save_asset(grass_type.get_path_name())