unreal-engine-mcp-server 0.3.1 → 0.4.3

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 (144) hide show
  1. package/.env.production +1 -1
  2. package/.github/copilot-instructions.md +45 -0
  3. package/.github/workflows/publish-mcp.yml +1 -1
  4. package/README.md +22 -7
  5. package/dist/index.js +137 -46
  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.d.ts +3 -2
  11. package/dist/resources/assets.js +117 -109
  12. package/dist/resources/levels.d.ts +21 -3
  13. package/dist/resources/levels.js +31 -56
  14. package/dist/tools/actors.d.ts +3 -14
  15. package/dist/tools/actors.js +246 -302
  16. package/dist/tools/animation.d.ts +57 -102
  17. package/dist/tools/animation.js +429 -450
  18. package/dist/tools/assets.d.ts +13 -2
  19. package/dist/tools/assets.js +58 -46
  20. package/dist/tools/audio.d.ts +22 -13
  21. package/dist/tools/audio.js +467 -121
  22. package/dist/tools/blueprint.d.ts +32 -13
  23. package/dist/tools/blueprint.js +699 -448
  24. package/dist/tools/build_environment_advanced.d.ts +0 -1
  25. package/dist/tools/build_environment_advanced.js +236 -87
  26. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  27. package/dist/tools/consolidated-tool-definitions.js +124 -255
  28. package/dist/tools/consolidated-tool-handlers.js +749 -766
  29. package/dist/tools/debug.d.ts +72 -10
  30. package/dist/tools/debug.js +170 -36
  31. package/dist/tools/editor.d.ts +9 -2
  32. package/dist/tools/editor.js +30 -44
  33. package/dist/tools/foliage.d.ts +34 -15
  34. package/dist/tools/foliage.js +97 -107
  35. package/dist/tools/introspection.js +19 -21
  36. package/dist/tools/landscape.d.ts +1 -2
  37. package/dist/tools/landscape.js +311 -168
  38. package/dist/tools/level.d.ts +3 -28
  39. package/dist/tools/level.js +642 -192
  40. package/dist/tools/lighting.d.ts +14 -3
  41. package/dist/tools/lighting.js +236 -123
  42. package/dist/tools/materials.d.ts +25 -7
  43. package/dist/tools/materials.js +102 -79
  44. package/dist/tools/niagara.d.ts +10 -12
  45. package/dist/tools/niagara.js +74 -94
  46. package/dist/tools/performance.d.ts +12 -4
  47. package/dist/tools/performance.js +38 -79
  48. package/dist/tools/physics.d.ts +34 -10
  49. package/dist/tools/physics.js +364 -292
  50. package/dist/tools/rc.js +98 -24
  51. package/dist/tools/sequence.d.ts +1 -0
  52. package/dist/tools/sequence.js +146 -24
  53. package/dist/tools/ui.d.ts +31 -4
  54. package/dist/tools/ui.js +83 -66
  55. package/dist/tools/visual.d.ts +11 -0
  56. package/dist/tools/visual.js +245 -30
  57. package/dist/types/tool-types.d.ts +0 -6
  58. package/dist/types/tool-types.js +1 -8
  59. package/dist/unreal-bridge.d.ts +32 -2
  60. package/dist/unreal-bridge.js +621 -127
  61. package/dist/utils/elicitation.d.ts +57 -0
  62. package/dist/utils/elicitation.js +104 -0
  63. package/dist/utils/error-handler.d.ts +0 -33
  64. package/dist/utils/error-handler.js +4 -111
  65. package/dist/utils/http.d.ts +2 -22
  66. package/dist/utils/http.js +12 -75
  67. package/dist/utils/normalize.d.ts +4 -4
  68. package/dist/utils/normalize.js +15 -7
  69. package/dist/utils/python-output.d.ts +18 -0
  70. package/dist/utils/python-output.js +290 -0
  71. package/dist/utils/python.d.ts +2 -0
  72. package/dist/utils/python.js +4 -0
  73. package/dist/utils/response-validator.d.ts +6 -1
  74. package/dist/utils/response-validator.js +66 -13
  75. package/dist/utils/result-helpers.d.ts +27 -0
  76. package/dist/utils/result-helpers.js +147 -0
  77. package/dist/utils/safe-json.d.ts +0 -2
  78. package/dist/utils/safe-json.js +0 -43
  79. package/dist/utils/validation.d.ts +16 -0
  80. package/dist/utils/validation.js +70 -7
  81. package/mcp-config-example.json +2 -2
  82. package/package.json +11 -10
  83. package/server.json +37 -14
  84. package/src/index.ts +146 -50
  85. package/src/prompts/index.ts +211 -13
  86. package/src/resources/actors.ts +59 -44
  87. package/src/resources/assets.ts +123 -102
  88. package/src/resources/levels.ts +37 -47
  89. package/src/tools/actors.ts +269 -313
  90. package/src/tools/animation.ts +556 -539
  91. package/src/tools/assets.ts +59 -45
  92. package/src/tools/audio.ts +507 -113
  93. package/src/tools/blueprint.ts +778 -462
  94. package/src/tools/build_environment_advanced.ts +312 -106
  95. package/src/tools/consolidated-tool-definitions.ts +136 -267
  96. package/src/tools/consolidated-tool-handlers.ts +871 -795
  97. package/src/tools/debug.ts +179 -38
  98. package/src/tools/editor.ts +35 -37
  99. package/src/tools/foliage.ts +110 -104
  100. package/src/tools/introspection.ts +24 -22
  101. package/src/tools/landscape.ts +334 -181
  102. package/src/tools/level.ts +683 -182
  103. package/src/tools/lighting.ts +244 -123
  104. package/src/tools/materials.ts +114 -83
  105. package/src/tools/niagara.ts +87 -81
  106. package/src/tools/performance.ts +49 -88
  107. package/src/tools/physics.ts +393 -299
  108. package/src/tools/rc.ts +103 -25
  109. package/src/tools/sequence.ts +157 -30
  110. package/src/tools/ui.ts +101 -70
  111. package/src/tools/visual.ts +250 -29
  112. package/src/types/tool-types.ts +0 -9
  113. package/src/unreal-bridge.ts +658 -140
  114. package/src/utils/elicitation.ts +129 -0
  115. package/src/utils/error-handler.ts +4 -159
  116. package/src/utils/http.ts +16 -115
  117. package/src/utils/normalize.ts +20 -10
  118. package/src/utils/python-output.ts +351 -0
  119. package/src/utils/python.ts +3 -0
  120. package/src/utils/response-validator.ts +68 -17
  121. package/src/utils/result-helpers.ts +193 -0
  122. package/src/utils/safe-json.ts +0 -50
  123. package/src/utils/validation.ts +94 -7
  124. package/tests/run-unreal-tool-tests.mjs +720 -0
  125. package/tsconfig.json +2 -2
  126. package/dist/python-utils.d.ts +0 -29
  127. package/dist/python-utils.js +0 -54
  128. package/dist/tools/tool-definitions.d.ts +0 -4919
  129. package/dist/tools/tool-definitions.js +0 -1065
  130. package/dist/tools/tool-handlers.d.ts +0 -47
  131. package/dist/tools/tool-handlers.js +0 -863
  132. package/dist/types/index.d.ts +0 -323
  133. package/dist/types/index.js +0 -28
  134. package/dist/utils/cache-manager.d.ts +0 -64
  135. package/dist/utils/cache-manager.js +0 -176
  136. package/dist/utils/errors.d.ts +0 -133
  137. package/dist/utils/errors.js +0 -256
  138. package/src/python/editor_compat.py +0 -181
  139. package/src/python-utils.ts +0 -57
  140. package/src/tools/tool-definitions.ts +0 -1081
  141. package/src/tools/tool-handlers.ts +0 -973
  142. package/src/types/index.ts +0 -414
  143. package/src/utils/cache-manager.ts +0 -213
  144. 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';