unreal-engine-mcp-server 0.4.0 → 0.4.4

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 (135) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +3 -2
  4. package/README.md +21 -5
  5. package/dist/index.js +124 -31
  6. package/dist/prompts/index.d.ts +10 -3
  7. package/dist/prompts/index.js +186 -7
  8. package/dist/resources/actors.d.ts +19 -1
  9. package/dist/resources/actors.js +55 -64
  10. package/dist/resources/assets.js +46 -62
  11. package/dist/resources/levels.d.ts +21 -3
  12. package/dist/resources/levels.js +29 -54
  13. package/dist/tools/actors.d.ts +3 -14
  14. package/dist/tools/actors.js +246 -302
  15. package/dist/tools/animation.d.ts +57 -102
  16. package/dist/tools/animation.js +429 -450
  17. package/dist/tools/assets.d.ts +13 -2
  18. package/dist/tools/assets.js +52 -44
  19. package/dist/tools/audio.d.ts +22 -13
  20. package/dist/tools/audio.js +467 -121
  21. package/dist/tools/blueprint.d.ts +32 -13
  22. package/dist/tools/blueprint.js +699 -448
  23. package/dist/tools/build_environment_advanced.d.ts +0 -1
  24. package/dist/tools/build_environment_advanced.js +190 -45
  25. package/dist/tools/consolidated-tool-definitions.js +78 -252
  26. package/dist/tools/consolidated-tool-handlers.js +506 -133
  27. package/dist/tools/debug.d.ts +72 -10
  28. package/dist/tools/debug.js +167 -31
  29. package/dist/tools/editor.d.ts +9 -2
  30. package/dist/tools/editor.js +30 -44
  31. package/dist/tools/foliage.d.ts +34 -15
  32. package/dist/tools/foliage.js +97 -107
  33. package/dist/tools/introspection.js +19 -21
  34. package/dist/tools/landscape.d.ts +1 -2
  35. package/dist/tools/landscape.js +311 -168
  36. package/dist/tools/level.d.ts +3 -28
  37. package/dist/tools/level.js +642 -192
  38. package/dist/tools/lighting.d.ts +14 -3
  39. package/dist/tools/lighting.js +236 -123
  40. package/dist/tools/materials.d.ts +25 -7
  41. package/dist/tools/materials.js +102 -79
  42. package/dist/tools/niagara.d.ts +10 -12
  43. package/dist/tools/niagara.js +74 -94
  44. package/dist/tools/performance.d.ts +12 -4
  45. package/dist/tools/performance.js +38 -79
  46. package/dist/tools/physics.d.ts +34 -10
  47. package/dist/tools/physics.js +364 -292
  48. package/dist/tools/rc.js +97 -23
  49. package/dist/tools/sequence.d.ts +1 -0
  50. package/dist/tools/sequence.js +125 -22
  51. package/dist/tools/ui.d.ts +31 -4
  52. package/dist/tools/ui.js +83 -66
  53. package/dist/tools/visual.d.ts +11 -0
  54. package/dist/tools/visual.js +245 -30
  55. package/dist/types/tool-types.d.ts +0 -6
  56. package/dist/types/tool-types.js +1 -8
  57. package/dist/unreal-bridge.d.ts +32 -2
  58. package/dist/unreal-bridge.js +621 -127
  59. package/dist/utils/elicitation.d.ts +57 -0
  60. package/dist/utils/elicitation.js +104 -0
  61. package/dist/utils/error-handler.d.ts +0 -33
  62. package/dist/utils/error-handler.js +4 -111
  63. package/dist/utils/http.d.ts +2 -22
  64. package/dist/utils/http.js +12 -75
  65. package/dist/utils/normalize.d.ts +4 -4
  66. package/dist/utils/normalize.js +15 -7
  67. package/dist/utils/python-output.d.ts +18 -0
  68. package/dist/utils/python-output.js +290 -0
  69. package/dist/utils/python.d.ts +2 -0
  70. package/dist/utils/python.js +4 -0
  71. package/dist/utils/response-validator.js +28 -2
  72. package/dist/utils/result-helpers.d.ts +27 -0
  73. package/dist/utils/result-helpers.js +147 -0
  74. package/dist/utils/safe-json.d.ts +0 -2
  75. package/dist/utils/safe-json.js +0 -43
  76. package/dist/utils/validation.d.ts +16 -0
  77. package/dist/utils/validation.js +70 -7
  78. package/mcp-config-example.json +2 -2
  79. package/package.json +10 -9
  80. package/server.json +37 -14
  81. package/src/index.ts +130 -33
  82. package/src/prompts/index.ts +211 -13
  83. package/src/resources/actors.ts +59 -44
  84. package/src/resources/assets.ts +48 -51
  85. package/src/resources/levels.ts +35 -45
  86. package/src/tools/actors.ts +269 -313
  87. package/src/tools/animation.ts +556 -539
  88. package/src/tools/assets.ts +53 -43
  89. package/src/tools/audio.ts +507 -113
  90. package/src/tools/blueprint.ts +778 -462
  91. package/src/tools/build_environment_advanced.ts +266 -64
  92. package/src/tools/consolidated-tool-definitions.ts +90 -264
  93. package/src/tools/consolidated-tool-handlers.ts +630 -121
  94. package/src/tools/debug.ts +176 -33
  95. package/src/tools/editor.ts +35 -37
  96. package/src/tools/foliage.ts +110 -104
  97. package/src/tools/introspection.ts +24 -22
  98. package/src/tools/landscape.ts +334 -181
  99. package/src/tools/level.ts +683 -182
  100. package/src/tools/lighting.ts +244 -123
  101. package/src/tools/materials.ts +114 -83
  102. package/src/tools/niagara.ts +87 -81
  103. package/src/tools/performance.ts +49 -88
  104. package/src/tools/physics.ts +393 -299
  105. package/src/tools/rc.ts +102 -24
  106. package/src/tools/sequence.ts +136 -28
  107. package/src/tools/ui.ts +101 -70
  108. package/src/tools/visual.ts +250 -29
  109. package/src/types/tool-types.ts +0 -9
  110. package/src/unreal-bridge.ts +658 -140
  111. package/src/utils/elicitation.ts +129 -0
  112. package/src/utils/error-handler.ts +4 -159
  113. package/src/utils/http.ts +16 -115
  114. package/src/utils/normalize.ts +20 -10
  115. package/src/utils/python-output.ts +351 -0
  116. package/src/utils/python.ts +3 -0
  117. package/src/utils/response-validator.ts +25 -2
  118. package/src/utils/result-helpers.ts +193 -0
  119. package/src/utils/safe-json.ts +0 -50
  120. package/src/utils/validation.ts +94 -7
  121. package/tests/run-unreal-tool-tests.mjs +720 -0
  122. package/tsconfig.json +2 -2
  123. package/dist/python-utils.d.ts +0 -29
  124. package/dist/python-utils.js +0 -54
  125. package/dist/types/index.d.ts +0 -323
  126. package/dist/types/index.js +0 -28
  127. package/dist/utils/cache-manager.d.ts +0 -64
  128. package/dist/utils/cache-manager.js +0 -176
  129. package/dist/utils/errors.d.ts +0 -133
  130. package/dist/utils/errors.js +0 -256
  131. package/src/python/editor_compat.py +0 -181
  132. package/src/python-utils.ts +0 -57
  133. package/src/types/index.ts +0 -414
  134. package/src/utils/cache-manager.ts +0 -213
  135. package/src/utils/errors.ts +0 -312
@@ -1,3 +1,4 @@
1
+ import { bestEffortInterpretedText, coerceBoolean, coerceNumber, coerceString, interpretStandardResult } from '../utils/result-helpers.js';
1
2
  export class FoliageTools {
2
3
  bridge;
3
4
  constructor(bridge) {
@@ -190,36 +191,36 @@ except Exception as e:
190
191
  print('RESULT:' + json.dumps(res))
191
192
  `.trim();
192
193
  const pyResp = await this.bridge.executePython(py);
193
- let out = '';
194
- if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput))
195
- out = pyResp.LogOutput.map((l) => l.Output || '').join('');
196
- else if (typeof pyResp === 'string')
197
- out = pyResp;
198
- else
199
- out = JSON.stringify(pyResp);
200
- const m = out.match(/RESULT:({.*})/);
201
- if (m) {
202
- try {
203
- const parsed = JSON.parse(m[1]);
204
- if (!parsed.success) {
205
- return { success: false, error: parsed.note || 'Add foliage type failed' };
206
- }
207
- return {
208
- success: true,
209
- created: parsed.created,
210
- exists: parsed.exists_after,
211
- method: parsed.method,
212
- assetPath: parsed.asset_path,
213
- usedMesh: parsed.used_mesh,
214
- note: parsed.note,
215
- message: parsed.exists_after ? `Foliage type '${name}' ready (${parsed.method || 'Unknown'})` : `Created foliage '${name}' but verification did not find it yet`
216
- };
217
- }
218
- catch {
219
- return { success: false, error: 'Failed to parse Python result' };
220
- }
221
- }
222
- return { success: false, error: 'No parseable result from Python' };
194
+ const interpreted = interpretStandardResult(pyResp, {
195
+ successMessage: `Foliage type '${name}' processed`,
196
+ failureMessage: 'Add foliage type failed'
197
+ });
198
+ if (!interpreted.success) {
199
+ return {
200
+ success: false,
201
+ error: coerceString(interpreted.payload.note) ?? interpreted.error ?? 'Add foliage type failed',
202
+ note: coerceString(interpreted.payload.note) ?? bestEffortInterpretedText(interpreted)
203
+ };
204
+ }
205
+ const payload = interpreted.payload;
206
+ const created = coerceBoolean(payload.created, false) ?? false;
207
+ const exists = coerceBoolean(payload.exists_after, false) ?? created;
208
+ const method = coerceString(payload.method) ?? 'Unknown';
209
+ const assetPath = coerceString(payload.asset_path);
210
+ const usedMesh = coerceString(payload.used_mesh);
211
+ const note = coerceString(payload.note);
212
+ return {
213
+ success: true,
214
+ created,
215
+ exists,
216
+ method,
217
+ assetPath,
218
+ usedMesh,
219
+ note,
220
+ message: exists
221
+ ? `Foliage type '${name}' ready (${method})`
222
+ : `Created foliage '${name}' but verification did not find it yet`
223
+ };
223
224
  }
224
225
  // Paint foliage by placing HISM instances (editor-only)
225
226
  async paintFoliage(params) {
@@ -255,26 +256,34 @@ px, py, pz = ${pos[0]}, ${pos[1]}, ${pos[2]}
255
256
  radius = float(${brush}) / 2.0
256
257
 
257
258
  try:
258
- actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
259
- all_actors = actor_sub.get_all_level_actors() if actor_sub else []
259
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
260
+ if not actor_subsystem:
261
+ raise RuntimeError('EditorActorSubsystem unavailable. Enable Editor Scripting Utilities plugin.')
260
262
 
261
- # Find or create a container actor
262
- label = f"FoliageContainer_{foliage_type_name}"
263
- container = None
264
- for a in all_actors:
265
- try:
266
- if a.get_actor_label() == label:
267
- container = a
268
- break
269
- except Exception:
270
- pass
263
+ all_actors = actor_subsystem.get_all_level_actors()
264
+
265
+ # Find or create a container actor using modern EditorActorSubsystem
266
+ label = f"FoliageContainer_{foliage_type_name}"
267
+ container = None
268
+ for a in all_actors:
269
+ try:
270
+ if a and a.get_actor_label() == label:
271
+ container = a
272
+ break
273
+ except Exception:
274
+ pass
275
+
276
+ if not container:
277
+ container = actor_subsystem.spawn_actor_from_class(
278
+ unreal.StaticMeshActor,
279
+ unreal.Vector(px, py, pz)
280
+ )
271
281
  if not container:
272
- # Spawn actor that can hold components
273
- container = unreal.EditorLevelLibrary.spawn_actor_from_class(unreal.StaticMeshActor, unreal.Vector(px, py, pz))
274
- try:
275
- container.set_actor_label(label)
276
- except Exception:
277
- pass
282
+ raise RuntimeError('Failed to spawn foliage container actor via EditorActorSubsystem')
283
+ try:
284
+ container.set_actor_label(label)
285
+ except Exception:
286
+ pass
278
287
 
279
288
  # Resolve mesh from FoliageType asset
280
289
  mesh = None
@@ -302,12 +311,12 @@ try:
302
311
  r = random.random() * radius
303
312
  x, y, z = px + math.cos(ang) * r, py + math.sin(ang) * r, pz
304
313
  try:
305
- # Spawn static mesh actor at position
306
- inst_actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
307
- unreal.StaticMeshActor,
308
- unreal.Vector(x, y, z),
309
- unreal.Rotator(0, random.random()*360.0, 0)
310
- )
314
+ # Spawn static mesh actor at position using modern subsystem
315
+ inst_actor = actor_subsystem.spawn_actor_from_class(
316
+ unreal.StaticMeshActor,
317
+ unreal.Vector(x, y, z),
318
+ unreal.Rotator(0, random.random()*360.0, 0)
319
+ )
311
320
  if inst_actor and mesh:
312
321
  # Set mesh on the actor's component
313
322
  try:
@@ -334,35 +343,32 @@ except Exception as e:
334
343
  print('RESULT:' + json.dumps(res))
335
344
  `.trim();
336
345
  const pyResp = await this.bridge.executePython(py);
337
- let out = '';
338
- if (pyResp?.LogOutput && Array.isArray(pyResp.LogOutput))
339
- out = pyResp.LogOutput.map((l) => l.Output || '').join('');
340
- else if (typeof pyResp === 'string')
341
- out = pyResp;
342
- else
343
- out = JSON.stringify(pyResp);
344
- const m = out.match(/RESULT:({.*})/);
345
- if (m) {
346
- try {
347
- const parsed = JSON.parse(m[1]);
348
- if (!parsed.success) {
349
- return { success: false, error: parsed.note || 'Paint foliage failed' };
350
- }
351
- return {
352
- success: true,
353
- added: parsed.added,
354
- actor: parsed.actor,
355
- component: parsed.component,
356
- usedMesh: parsed.used_mesh,
357
- note: parsed.note,
358
- message: `Painted ${parsed.added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
359
- };
360
- }
361
- catch {
362
- return { success: false, error: 'Failed to parse Python result' };
363
- }
364
- }
365
- return { success: false, error: 'No parseable result from Python' };
346
+ const interpreted = interpretStandardResult(pyResp, {
347
+ successMessage: `Painted foliage for '${foliageType}'`,
348
+ failureMessage: 'Paint foliage failed'
349
+ });
350
+ if (!interpreted.success) {
351
+ return {
352
+ success: false,
353
+ error: coerceString(interpreted.payload.note) ?? interpreted.error ?? 'Paint foliage failed',
354
+ note: coerceString(interpreted.payload.note) ?? bestEffortInterpretedText(interpreted)
355
+ };
356
+ }
357
+ const payload = interpreted.payload;
358
+ const added = coerceNumber(payload.added) ?? 0;
359
+ const actor = coerceString(payload.actor);
360
+ const component = coerceString(payload.component);
361
+ const usedMesh = coerceString(payload.used_mesh);
362
+ const note = coerceString(payload.note);
363
+ return {
364
+ success: true,
365
+ added,
366
+ actor,
367
+ component,
368
+ usedMesh,
369
+ note,
370
+ message: `Painted ${added} instances for '${foliageType}' around (${pos[0]}, ${pos[1]}, ${pos[2]})`
371
+ };
366
372
  }
367
373
  // Create instanced mesh
368
374
  async createInstancedMesh(params) {
@@ -379,9 +385,7 @@ print('RESULT:' + json.dumps(res))
379
385
  if (params.cullDistance !== undefined) {
380
386
  commands.push(`SetInstanceCullDistance ${params.name} ${params.cullDistance}`);
381
387
  }
382
- for (const cmd of commands) {
383
- await this.bridge.executeConsoleCommand(cmd);
384
- }
388
+ await this.bridge.executeConsoleCommands(commands);
385
389
  return { success: true, message: `Instanced mesh ${params.name} created with ${params.instances.length} instances` };
386
390
  }
387
391
  // Set foliage LOD
@@ -393,9 +397,7 @@ print('RESULT:' + json.dumps(res))
393
397
  if (params.screenSize) {
394
398
  commands.push(`SetFoliageLODScreenSize ${params.foliageType} ${params.screenSize.join(' ')}`);
395
399
  }
396
- for (const cmd of commands) {
397
- await this.bridge.executeConsoleCommand(cmd);
398
- }
400
+ await this.bridge.executeConsoleCommands(commands);
399
401
  return { success: true, message: 'Foliage LOD settings updated' };
400
402
  }
401
403
  // Create procedural foliage
@@ -412,9 +414,7 @@ print('RESULT:' + json.dumps(res))
412
414
  commands.push(`SetProceduralTileSize ${params.volumeName} ${params.tileSize}`);
413
415
  }
414
416
  commands.push(`GenerateProceduralFoliage ${params.volumeName}`);
415
- for (const cmd of commands) {
416
- await this.bridge.executeConsoleCommand(cmd);
417
- }
417
+ await this.bridge.executeConsoleCommands(commands);
418
418
  return { success: true, message: `Procedural foliage volume ${params.volumeName} created` };
419
419
  }
420
420
  // Set foliage collision
@@ -429,9 +429,7 @@ print('RESULT:' + json.dumps(res))
429
429
  if (params.generateOverlapEvents !== undefined) {
430
430
  commands.push(`SetFoliageOverlapEvents ${params.foliageType} ${params.generateOverlapEvents}`);
431
431
  }
432
- for (const cmd of commands) {
433
- await this.bridge.executeConsoleCommand(cmd);
434
- }
432
+ await this.bridge.executeConsoleCommands(commands);
435
433
  return { success: true, message: 'Foliage collision settings updated' };
436
434
  }
437
435
  // Create grass system
@@ -449,9 +447,7 @@ print('RESULT:' + json.dumps(res))
449
447
  if (params.windSpeed !== undefined) {
450
448
  commands.push(`SetGrassWindSpeed ${params.name} ${params.windSpeed}`);
451
449
  }
452
- for (const cmd of commands) {
453
- await this.bridge.executeConsoleCommand(cmd);
454
- }
450
+ await this.bridge.executeConsoleCommands(commands);
455
451
  return { success: true, message: `Grass system ${params.name} created` };
456
452
  }
457
453
  // Remove foliage instances
@@ -483,9 +479,7 @@ print('RESULT:' + json.dumps(res))
483
479
  commands.push(`UpdateFoliageMesh ${params.foliageType} ${params.newMeshPath}`);
484
480
  }
485
481
  commands.push(`RefreshFoliage ${params.foliageType}`);
486
- for (const cmd of commands) {
487
- await this.bridge.executeConsoleCommand(cmd);
488
- }
482
+ await this.bridge.executeConsoleCommands(commands);
489
483
  return { success: true, message: 'Foliage instances updated' };
490
484
  }
491
485
  // Create foliage spawner
@@ -497,9 +491,7 @@ print('RESULT:' + json.dumps(res))
497
491
  commands.push(`AddFoliageExclusionArea ${params.name} ${area.join(' ')}`);
498
492
  }
499
493
  }
500
- for (const cmd of commands) {
501
- await this.bridge.executeConsoleCommand(cmd);
502
- }
494
+ await this.bridge.executeConsoleCommands(commands);
503
495
  return { success: true, message: `Foliage spawner ${params.name} created` };
504
496
  }
505
497
  // Optimize foliage
@@ -516,9 +508,7 @@ print('RESULT:' + json.dumps(res))
516
508
  commands.push('OptimizeFoliageDrawCalls');
517
509
  }
518
510
  commands.push('RebuildFoliageTree');
519
- for (const cmd of commands) {
520
- await this.bridge.executeConsoleCommand(cmd);
521
- }
511
+ await this.bridge.executeConsoleCommands(commands);
522
512
  return { success: true, message: 'Foliage optimized' };
523
513
  }
524
514
  }
@@ -1,4 +1,5 @@
1
1
  import { Logger } from '../utils/logger.js';
2
+ import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
2
3
  export class IntrospectionTools {
3
4
  bridge;
4
5
  log = new Logger('IntrospectionTools');
@@ -31,33 +32,30 @@ export class IntrospectionTools {
31
32
  * Parse Python execution result with better error handling
32
33
  */
33
34
  parsePythonResult(resp, operationName) {
34
- let out = '';
35
- if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
36
- out = resp.LogOutput.map((l) => l.Output || '').join('');
35
+ const interpreted = interpretStandardResult(resp, {
36
+ successMessage: `${operationName} succeeded`,
37
+ failureMessage: `${operationName} failed`
38
+ });
39
+ if (interpreted.success) {
40
+ return {
41
+ ...interpreted.payload,
42
+ success: true
43
+ };
37
44
  }
38
- else if (typeof resp === 'string') {
39
- out = resp;
45
+ const output = bestEffortInterpretedText(interpreted) ?? '';
46
+ if (output) {
47
+ this.log.error(`Failed to parse ${operationName} result: ${output}`);
40
48
  }
41
- else {
42
- out = JSON.stringify(resp);
43
- }
44
- const m = out.match(/RESULT:({.*})/);
45
- if (m) {
46
- try {
47
- return JSON.parse(m[1]);
48
- }
49
- catch (e) {
50
- this.log.error(`Failed to parse ${operationName} result: ${e}`);
51
- }
52
- }
53
- // Check for common error patterns
54
- if (out.includes('ModuleNotFoundError')) {
49
+ if (output.includes('ModuleNotFoundError')) {
55
50
  return { success: false, error: 'Reflection module not available.' };
56
51
  }
57
- if (out.includes('AttributeError')) {
52
+ if (output.includes('AttributeError')) {
58
53
  return { success: false, error: 'Reflection API method not found. Check Unreal Engine version compatibility.' };
59
54
  }
60
- return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
55
+ return {
56
+ success: false,
57
+ error: `${interpreted.error ?? `${operationName} did not return a valid result`}: ${output.substring(0, 200)}`
58
+ };
61
59
  }
62
60
  /**
63
61
  * Convert Unreal property value to JavaScript-friendly format
@@ -2,7 +2,6 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class LandscapeTools {
3
3
  private bridge;
4
4
  constructor(bridge: UnrealBridge);
5
- private _executeCommand;
6
5
  createLandscape(params: {
7
6
  name: string;
8
7
  location?: [number, number, number];
@@ -16,7 +15,7 @@ export declare class LandscapeTools {
16
15
  runtimeGrid?: string;
17
16
  isSpatiallyLoaded?: boolean;
18
17
  dataLayers?: string[];
19
- }): Promise<any>;
18
+ }): Promise<Record<string, unknown>>;
20
19
  sculptLandscape(_params: {
21
20
  landscapeName: string;
22
21
  tool: 'Sculpt' | 'Smooth' | 'Flatten' | 'Ramp' | 'Erosion' | 'Hydro' | 'Noise' | 'Retopologize';