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
package/dist/tools/ui.js CHANGED
@@ -1,20 +1,9 @@
1
+ import { bestEffortInterpretedText, interpretStandardResult } from '../utils/result-helpers.js';
1
2
  export class UITools {
2
3
  bridge;
3
4
  constructor(bridge) {
4
5
  this.bridge = bridge;
5
6
  }
6
- // Execute console command
7
- async _executeCommand(command) {
8
- return this.bridge.httpCall('/remote/object/call', 'PUT', {
9
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
10
- functionName: 'ExecuteConsoleCommand',
11
- parameters: {
12
- Command: command,
13
- SpecificPlayer: null
14
- },
15
- generateTransaction: false
16
- });
17
- }
18
7
  // Create widget blueprint
19
8
  async createWidget(params) {
20
9
  const path = params.savePath || '/Game/UI/Widgets';
@@ -51,16 +40,18 @@ except Exception as e:
51
40
  `.trim();
52
41
  try {
53
42
  const resp = await this.bridge.executePython(py);
54
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
55
- const m = out.match(/RESULT:({.*})/);
56
- if (m) {
57
- try {
58
- const parsed = JSON.parse(m[1]);
59
- return parsed.success ? { success: true, message: 'Widget blueprint created' } : { success: false, error: parsed.error };
60
- }
61
- catch { }
43
+ const interpreted = interpretStandardResult(resp, {
44
+ successMessage: 'Widget blueprint created',
45
+ failureMessage: 'Failed to create WidgetBlueprint'
46
+ });
47
+ if (interpreted.success) {
48
+ return { success: true, message: interpreted.message };
62
49
  }
63
- return { success: true, message: 'Widget blueprint creation attempted' };
50
+ return {
51
+ success: false,
52
+ error: interpreted.error ?? 'Failed to create widget blueprint',
53
+ details: bestEffortInterpretedText(interpreted)
54
+ };
64
55
  }
65
56
  catch (e) {
66
57
  return { success: false, error: `Failed to create widget blueprint: ${e}` };
@@ -84,9 +75,7 @@ except Exception as e:
84
75
  commands.push(`SetWidgetAlignment ${params.widgetName}.${params.componentName} ${params.slot.alignment.join(' ')}`);
85
76
  }
86
77
  }
87
- for (const cmd of commands) {
88
- await this.bridge.executeConsoleCommand(cmd);
89
- }
78
+ await this.bridge.executeConsoleCommands(commands);
90
79
  return { success: true, message: `Component ${params.componentName} added to widget` };
91
80
  }
92
81
  // Set text
@@ -102,9 +91,7 @@ except Exception as e:
102
91
  if (params.fontFamily) {
103
92
  commands.push(`SetWidgetFont ${params.widgetName}.${params.componentName} ${params.fontFamily}`);
104
93
  }
105
- for (const cmd of commands) {
106
- await this.bridge.executeConsoleCommand(cmd);
107
- }
94
+ await this.bridge.executeConsoleCommands(commands);
108
95
  return { success: true, message: 'Widget text updated' };
109
96
  }
110
97
  // Set image
@@ -117,9 +104,7 @@ except Exception as e:
117
104
  if (params.sizeToContent !== undefined) {
118
105
  commands.push(`SetWidgetSizeToContent ${params.widgetName}.${params.componentName} ${params.sizeToContent}`);
119
106
  }
120
- for (const cmd of commands) {
121
- await this.bridge.executeConsoleCommand(cmd);
122
- }
107
+ await this.bridge.executeConsoleCommands(commands);
123
108
  return { success: true, message: 'Widget image updated' };
124
109
  }
125
110
  // Create HUD
@@ -132,18 +117,50 @@ except Exception as e:
132
117
  commands.push(`AddHUDElement ${params.name} ${element.type} ${element.position.join(' ')} ${size.join(' ')}`);
133
118
  }
134
119
  }
135
- for (const cmd of commands) {
136
- await this.bridge.executeConsoleCommand(cmd);
137
- }
120
+ await this.bridge.executeConsoleCommands(commands);
138
121
  return { success: true, message: `HUD ${params.name} created` };
139
122
  }
140
123
  // Show/Hide widget
141
124
  async setWidgetVisibility(params) {
142
125
  const playerIndex = params.playerIndex ?? 0;
126
+ const widgetName = params.widgetName?.trim();
127
+ if (!widgetName) {
128
+ return { success: false, error: 'widgetName is required' };
129
+ }
130
+ const verifyScript = `
131
+ import unreal, json
132
+ name = r"${widgetName}"
133
+ candidates = []
134
+ if name.startswith('/Game/'):
135
+ candidates.append(name)
136
+ else:
137
+ candidates.append(f"/Game/UI/Widgets/{name}")
138
+ candidates.append(f"/Game/{name}")
139
+
140
+ found_path = ''
141
+ for path in candidates:
142
+ if unreal.EditorAssetLibrary.does_asset_exist(path):
143
+ found_path = path
144
+ break
145
+
146
+ print('RESULT:' + json.dumps({'success': bool(found_path), 'path': found_path, 'candidates': candidates}))
147
+ `.trim();
148
+ const verify = await this.bridge.executePythonWithResult(verifyScript);
149
+ if (!verify?.success) {
150
+ return { success: false, error: `Widget asset not found for ${widgetName}` };
151
+ }
143
152
  const command = params.visible
144
- ? `ShowWidget ${params.widgetName} ${playerIndex}`
145
- : `HideWidget ${params.widgetName} ${playerIndex}`;
146
- return this.bridge.executeConsoleCommand(command);
153
+ ? `ShowWidget ${widgetName} ${playerIndex}`
154
+ : `HideWidget ${widgetName} ${playerIndex}`;
155
+ const raw = await this.bridge.executeConsoleCommand(command);
156
+ const summary = this.bridge.summarizeConsoleCommand(command, raw);
157
+ return {
158
+ success: true,
159
+ message: params.visible ? `Widget ${widgetName} show command issued` : `Widget ${widgetName} hide command issued`,
160
+ command: summary.command,
161
+ output: summary.output || undefined,
162
+ logLines: summary.logLines?.length ? summary.logLines : undefined
163
+ };
147
164
  }
148
165
  // Add widget to viewport
149
166
  async addWidgetToViewport(params) {
@@ -168,10 +185,20 @@ try:
168
185
  # Get the generated class from the widget blueprint
169
186
  widget_class = widget_bp.generated_class() if hasattr(widget_bp, 'generated_class') else widget_bp
170
187
 
171
- # Get the world and player controller
172
- world = unreal.EditorLevelLibrary.get_editor_world()
173
- if not world:
174
- print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available'}))
188
+ # Get the world and player controller via modern subsystems
189
+ world = None
190
+ try:
191
+ world = unreal.EditorUtilityLibrary.get_editor_world()
192
+ except Exception:
193
+ pass
194
+
195
+ if not world:
196
+ editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
197
+ if editor_subsystem and hasattr(editor_subsystem, 'get_editor_world'):
198
+ world = editor_subsystem.get_editor_world()
199
+
200
+ if not world:
201
+ print('RESULT:' + json.dumps({'success': False, 'error': 'No editor world available. Start a PIE session or enable Editor Scripting Utilities.'}))
175
202
  else:
176
203
  # Try to get player controller
177
204
  try:
@@ -196,18 +223,18 @@ except Exception as e:
196
223
  `.trim();
197
224
  try {
198
225
  const resp = await this.bridge.executePython(py);
199
- const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
200
- const m = out.match(/RESULT:({.*})/);
201
- if (m) {
202
- try {
203
- const parsed = JSON.parse(m[1]);
204
- return parsed.success
205
- ? { success: true, message: `Widget added to viewport with z-order ${zOrder}` }
206
- : { success: false, error: parsed.error };
207
- }
208
- catch { }
226
+ const interpreted = interpretStandardResult(resp, {
227
+ successMessage: `Widget added to viewport with z-order ${zOrder}`,
228
+ failureMessage: 'Failed to add widget to viewport'
229
+ });
230
+ if (interpreted.success) {
231
+ return { success: true, message: interpreted.message };
209
232
  }
210
- return { success: true, message: 'Widget add to viewport attempted' };
233
+ return {
234
+ success: false,
235
+ error: interpreted.error ?? 'Failed to add widget to viewport',
236
+ details: bestEffortInterpretedText(interpreted)
237
+ };
211
238
  }
212
239
  catch (e) {
213
240
  return { success: false, error: `Failed to add widget to viewport: ${e}` };
@@ -229,9 +256,7 @@ except Exception as e:
229
256
  commands.push(`AddMenuButton ${params.name} "${button.text}" ${button.action} ${pos.join(' ')}`);
230
257
  }
231
258
  }
232
- for (const cmd of commands) {
233
- await this.bridge.executeConsoleCommand(cmd);
234
- }
259
+ await this.bridge.executeConsoleCommands(commands);
235
260
  return { success: true, message: `Menu ${params.name} created` };
236
261
  }
237
262
  // Set widget animation
@@ -247,9 +272,7 @@ except Exception as e:
247
272
  }
248
273
  }
249
274
  }
250
- for (const cmd of commands) {
251
- await this.bridge.executeConsoleCommand(cmd);
252
- }
275
+ await this.bridge.executeConsoleCommands(commands);
253
276
  return { success: true, message: `Animation ${params.animationName} created` };
254
277
  }
255
278
  // Play widget animation
@@ -277,9 +300,7 @@ except Exception as e:
277
300
  if (params.style.margin) {
278
301
  commands.push(`SetWidgetMargin ${params.widgetName}.${params.componentName} ${params.style.margin.join(' ')}`);
279
302
  }
280
- for (const cmd of commands) {
281
- await this.bridge.executeConsoleCommand(cmd);
282
- }
303
+ await this.bridge.executeConsoleCommands(commands);
283
304
  return { success: true, message: 'Widget style updated' };
284
305
  }
285
306
  // Bind widget event
@@ -297,9 +318,7 @@ except Exception as e:
297
318
  if (params.lockCursor !== undefined) {
298
319
  commands.push(`SetMouseLockMode ${params.lockCursor}`);
299
320
  }
300
- for (const cmd of commands) {
301
- await this.bridge.executeConsoleCommand(cmd);
302
- }
321
+ await this.bridge.executeConsoleCommands(commands);
303
322
  return { success: true, message: `Input mode set to ${params.mode}` };
304
323
  }
305
324
  // Create tooltip
@@ -320,9 +339,7 @@ except Exception as e:
320
339
  commands.push(`AddDropTarget ${params.widgetName}.${params.componentName} ${target}`);
321
340
  }
322
341
  }
323
- for (const cmd of commands) {
324
- await this.bridge.executeConsoleCommand(cmd);
325
- }
342
+ await this.bridge.executeConsoleCommands(commands);
326
343
  return { success: true, message: 'Drag and drop configured' };
327
344
  }
328
345
  // Create notification
@@ -2,6 +2,7 @@ import { UnrealBridge } from '../unreal-bridge.js';
2
2
  export declare class VisualTools {
3
3
  private bridge;
4
4
  private env;
5
+ private log;
5
6
  constructor(bridge: UnrealBridge);
6
7
  takeScreenshot(params: {
7
8
  resolution?: string;
@@ -9,12 +10,18 @@ export declare class VisualTools {
9
10
  success: boolean;
10
11
  message: string;
11
12
  imagePath?: undefined;
13
+ imageMimeType?: undefined;
14
+ imageSizeBytes?: undefined;
12
15
  imageBase64?: undefined;
16
+ imageBase64Truncated?: undefined;
13
17
  error?: undefined;
14
18
  } | {
15
19
  success: boolean;
16
20
  imagePath: string;
21
+ imageMimeType: string | undefined;
22
+ imageSizeBytes: number | undefined;
17
23
  imageBase64: string | undefined;
24
+ imageBase64Truncated: boolean;
18
25
  message?: undefined;
19
26
  error?: undefined;
20
27
  } | {
@@ -22,8 +29,12 @@ export declare class VisualTools {
22
29
  error: string;
23
30
  message?: undefined;
24
31
  imagePath?: undefined;
32
+ imageMimeType?: undefined;
33
+ imageSizeBytes?: undefined;
25
34
  imageBase64?: undefined;
35
+ imageBase64Truncated?: undefined;
26
36
  }>;
37
+ private getEngineScreenshotDirectories;
27
38
  private findLatestScreenshot;
28
39
  }
29
40
  //# sourceMappingURL=visual.d.ts.map
@@ -1,67 +1,282 @@
1
1
  import { loadEnv } from '../types/env.js';
2
- import fs from 'fs';
2
+ import { Logger } from '../utils/logger.js';
3
+ import { coerceStringArray, interpretStandardResult } from '../utils/result-helpers.js';
4
+ import { promises as fs } from 'fs';
3
5
  import path from 'path';
4
6
  export class VisualTools {
5
7
  bridge;
6
8
  env = loadEnv();
9
+ log = new Logger('VisualTools');
7
10
  constructor(bridge) {
8
11
  this.bridge = bridge;
9
12
  }
10
13
  // Take a screenshot of viewport (high res or standard). Returns path and base64 (truncated)
11
14
  async takeScreenshot(params) {
12
15
  const res = params.resolution && /^\d+x\d+$/i.test(params.resolution) ? params.resolution : '';
13
- const cmd = res ? `HighResShot ${res}` : 'HighResShot';
16
+ const primaryCommand = res ? `HighResShot ${res}` : 'Shot';
17
+ const captureStartedAt = Date.now();
14
18
  try {
15
- await this.bridge.executeConsoleCommand(cmd);
16
- // Give the engine a moment to write the file
19
+ const firstAttempt = await this.bridge.executeConsoleCommand(primaryCommand);
20
+ const firstSummary = this.bridge.summarizeConsoleCommand(primaryCommand, firstAttempt);
21
+ if (!res) {
22
+ const output = (firstSummary.output || '').toLowerCase();
23
+ const badInput = output.includes('bad input') || output.includes('unrecognized command');
24
+ const hasErrorLine = firstSummary.logLines?.some(line => /error:/i.test(line));
25
+ if (badInput || hasErrorLine) {
26
+ this.log.debug(`Screenshot primary command reported an error (${firstSummary.output || 'no output'})`);
27
+ }
28
+ }
29
+ // Give the engine a moment to write the file (UE can flush asynchronously)
17
30
  await new Promise(r => setTimeout(r, 1200));
18
- const p = await this.findLatestScreenshot();
19
- if (!p)
31
+ const p = await this.findLatestScreenshot(captureStartedAt);
32
+ if (!p) {
33
+ this.log.warn('Screenshot captured but output file was not found');
20
34
  return { success: true, message: 'Screenshot triggered, but could not locate output file' };
35
+ }
21
36
  let b64;
37
+ let truncated = false;
38
+ let sizeBytes;
39
+ let mime;
22
40
  try {
23
- const buf = fs.readFileSync(p);
24
- // Limit to ~1MB to avoid huge responses
25
- const max = 1024 * 1024;
26
- b64 = buf.length > max ? buf.subarray(0, max).toString('base64') : buf.toString('base64');
41
+ const stat = await fs.stat(p);
42
+ sizeBytes = stat.size;
43
+ const max = 1024 * 1024; // 1 MiB cap for inline payloads
44
+ if (stat.size <= max) {
45
+ const buf = await fs.readFile(p);
46
+ b64 = buf.toString('base64');
47
+ }
48
+ else {
49
+ truncated = true;
50
+ }
51
+ const ext = path.extname(p).toLowerCase();
52
+ if (ext === '.png')
53
+ mime = 'image/png';
54
+ else if (ext === '.jpg' || ext === '.jpeg')
55
+ mime = 'image/jpeg';
56
+ else if (ext === '.bmp')
57
+ mime = 'image/bmp';
27
58
  }
28
59
  catch { }
29
- return { success: true, imagePath: p, imageBase64: b64 };
60
+ return {
61
+ success: true,
62
+ imagePath: p.replace(/\\/g, '/'),
63
+ imageMimeType: mime,
64
+ imageSizeBytes: sizeBytes,
65
+ imageBase64: b64,
66
+ imageBase64Truncated: truncated
67
+ };
30
68
  }
31
69
  catch (err) {
32
70
  return { success: false, error: String(err?.message || err) };
33
71
  }
34
72
  }
35
- async findLatestScreenshot() {
73
+ async getEngineScreenshotDirectories() {
74
+ const python = `
75
+ import unreal
76
+ import json
77
+ import os
78
+
79
+ result = {
80
+ "success": True,
81
+ "message": "",
82
+ "error": "",
83
+ "directories": [],
84
+ "warnings": [],
85
+ "details": []
86
+ }
87
+
88
+ def finalize():
89
+ data = dict(result)
90
+ if data.get("success"):
91
+ if not data.get("message"):
92
+ data["message"] = "Collected screenshot directories"
93
+ data.pop("error", None)
94
+ else:
95
+ if not data.get("error"):
96
+ data["error"] = data.get("message") or "Failed to collect screenshot directories"
97
+ if not data.get("message"):
98
+ data["message"] = data["error"]
99
+ if not data.get("warnings"):
100
+ data.pop("warnings", None)
101
+ if not data.get("details"):
102
+ data.pop("details", None)
103
+ if not data.get("directories"):
104
+ data.pop("directories", None)
105
+ return data
106
+
107
+ def add_path(candidate, note=None):
108
+ if not candidate:
109
+ return
110
+ try:
111
+ abs_path = os.path.abspath(os.path.normpath(candidate))
112
+ except Exception as normalize_error:
113
+ result["warnings"].append(f"Failed to normalize path {candidate}: {normalize_error}")
114
+ return
115
+ if abs_path not in result["directories"]:
116
+ result["directories"].append(abs_path)
117
+ if note:
118
+ result["details"].append(f"{note}: {abs_path}")
119
+ else:
120
+ result["details"].append(f"Discovered screenshot directory: {abs_path}")
121
+
122
+ try:
123
+ automation_dir = unreal.AutomationLibrary.get_screenshot_directory()
124
+ add_path(automation_dir, "Automation screenshot directory")
125
+ except Exception as automation_error:
126
+ result["warnings"].append(f"Automation screenshot directory unavailable: {automation_error}")
127
+
128
+ try:
129
+ project_saved = unreal.Paths.project_saved_dir()
130
+ add_path(project_saved, "Project Saved directory")
131
+ if project_saved:
132
+ add_path(os.path.join(project_saved, 'Screenshots'), "Project Saved screenshots")
133
+ add_path(os.path.join(project_saved, 'Screenshots', 'Windows'), "Project Saved Windows screenshots")
134
+ add_path(os.path.join(project_saved, 'Screenshots', 'WindowsEditor'), "Project Saved WindowsEditor screenshots")
135
+ try:
136
+ platform_name = unreal.SystemLibrary.get_platform_user_name()
137
+ if platform_name:
138
+ add_path(os.path.join(project_saved, 'Screenshots', platform_name), f"Project Saved screenshots for {platform_name}")
139
+ add_path(os.path.join(project_saved, 'Screenshots', f"{platform_name}Editor"), f"Project Saved editor screenshots for {platform_name}")
140
+ except Exception as platform_error:
141
+ result["warnings"].append(f"Failed to resolve platform-specific screenshot directories: {platform_error}")
142
+ except Exception as saved_error:
143
+ result["warnings"].append(f"Project Saved directory unavailable: {saved_error}")
144
+
145
+ try:
146
+ project_file = unreal.Paths.get_project_file_path()
147
+ if project_file:
148
+ project_dir = os.path.dirname(project_file)
149
+ add_path(os.path.join(project_dir, 'Saved', 'Screenshots'), "Project directory screenshots")
150
+ add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'Windows'), "Project directory Windows screenshots")
151
+ add_path(os.path.join(project_dir, 'Saved', 'Screenshots', 'WindowsEditor'), "Project directory WindowsEditor screenshots")
152
+ except Exception as project_error:
153
+ result["warnings"].append(f"Project directory screenshots unavailable: {project_error}")
154
+
155
+ if not result["directories"]:
156
+ result["warnings"].append("No screenshot directories discovered")
157
+
158
+ print('RESULT:' + json.dumps(finalize()))
159
+ `.trim()
160
+ .replace(/\r?\n/g, '\n');
161
+ try {
162
+ const response = await this.bridge.executePython(python);
163
+ const interpreted = interpretStandardResult(response, {
164
+ successMessage: 'Collected screenshot directories',
165
+ failureMessage: 'Failed to collect screenshot directories'
166
+ });
167
+ if (interpreted.details) {
168
+ for (const entry of interpreted.details) {
169
+ this.log.debug(entry);
170
+ }
171
+ }
172
+ if (interpreted.warnings) {
173
+ for (const warning of interpreted.warnings) {
174
+ this.log.debug(`Screenshot directory warning: ${warning}`);
175
+ }
176
+ }
177
+ const directories = coerceStringArray(interpreted.payload.directories);
178
+ if (directories?.length) {
179
+ return directories;
180
+ }
181
+ if (!interpreted.success && interpreted.error) {
182
+ this.log.warn(`Screenshot path probe failed: ${interpreted.error}`);
183
+ }
184
+ if (interpreted.rawText) {
185
+ try {
186
+ const fallback = JSON.parse(interpreted.rawText);
187
+ const fallbackDirs = coerceStringArray(fallback);
188
+ if (fallbackDirs?.length) {
189
+ return fallbackDirs;
190
+ }
191
+ }
192
+ catch { }
193
+ }
194
+ }
195
+ catch (err) {
196
+ this.log.debug('Screenshot path probe failed', err);
197
+ }
198
+ return [];
199
+ }
200
+ async findLatestScreenshot(since) {
36
201
  // Try env override, otherwise look in common UE Saved/Screenshots folder under project
37
202
  const candidates = [];
203
+ const seen = new Set();
204
+ const addCandidate = (candidate) => {
205
+ if (!candidate)
206
+ return;
207
+ const normalized = path.isAbsolute(candidate)
208
+ ? path.normalize(candidate)
209
+ : path.resolve(candidate);
210
+ if (!seen.has(normalized)) {
211
+ seen.add(normalized);
212
+ candidates.push(normalized);
213
+ }
214
+ };
38
215
  if (this.env.UE_SCREENSHOT_DIR)
39
- candidates.push(this.env.UE_SCREENSHOT_DIR);
216
+ addCandidate(this.env.UE_SCREENSHOT_DIR);
40
217
  if (this.env.UE_PROJECT_PATH) {
41
218
  const projectDir = path.dirname(this.env.UE_PROJECT_PATH);
42
- candidates.push(path.join(projectDir, 'Saved', 'Screenshots'));
43
- candidates.push(path.join(projectDir, 'Saved', 'Screenshots', 'Windows'));
44
- candidates.push(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor'));
219
+ addCandidate(path.join(projectDir, 'Saved', 'Screenshots'));
220
+ addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'Windows'));
221
+ addCandidate(path.join(projectDir, 'Saved', 'Screenshots', 'WindowsEditor'));
45
222
  }
46
- // Fallback: common locations
47
- candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots'));
48
- candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows'));
49
- candidates.push(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor'));
50
- let latest = null;
51
- for (const c of candidates) {
223
+ const engineDirs = await this.getEngineScreenshotDirectories();
224
+ for (const dir of engineDirs) {
225
+ addCandidate(dir);
226
+ }
227
+ // Fallback: common locations relative to current working directory
228
+ addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots'));
229
+ addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'Windows'));
230
+ addCandidate(path.join(process.cwd(), 'Saved', 'Screenshots', 'WindowsEditor'));
231
+ const searchDirs = new Set();
232
+ const queue = [...candidates];
233
+ while (queue.length) {
234
+ const candidate = queue.pop();
235
+ if (!candidate || searchDirs.has(candidate))
236
+ continue;
237
+ searchDirs.add(candidate);
52
238
  try {
53
- const entries = fs.readdirSync(c).map(f => path.join(c, f));
54
- for (const fp of entries) {
55
- if (!/\.png$/i.test(fp))
56
- continue;
57
- const st = fs.statSync(fp);
58
- if (!latest || st.mtimeMs > latest.mtime)
59
- latest = { path: fp, mtime: st.mtimeMs };
239
+ const entries = await fs.readdir(candidate, { withFileTypes: true });
240
+ for (const entry of entries) {
241
+ if (entry.isDirectory()) {
242
+ queue.push(path.join(candidate, entry.name));
243
+ }
60
244
  }
61
245
  }
62
246
  catch { }
63
247
  }
64
- return latest?.path || null;
248
+ let latest = null;
249
+ let latestSince = null;
250
+ const cutoff = since ? since - 2000 : undefined; // allow slight clock drift
251
+ for (const dirPath of searchDirs) {
252
+ let entries;
253
+ try {
254
+ entries = await fs.readdir(dirPath);
255
+ }
256
+ catch {
257
+ continue;
258
+ }
259
+ for (const entry of entries) {
260
+ const fp = path.join(dirPath, entry);
261
+ if (!/\.(png|jpg|jpeg|bmp)$/i.test(fp))
262
+ continue;
263
+ try {
264
+ const st = await fs.stat(fp);
265
+ const info = { path: fp, mtime: st.mtimeMs };
266
+ if (!latest || info.mtime > latest.mtime) {
267
+ latest = info;
268
+ }
269
+ if (cutoff !== undefined && st.mtimeMs >= cutoff) {
270
+ if (!latestSince || info.mtime > latestSince.mtime) {
271
+ latestSince = info;
272
+ }
273
+ }
274
+ }
275
+ catch { }
276
+ }
277
+ }
278
+ const chosen = latestSince || latest;
279
+ return chosen?.path || null;
65
280
  }
66
281
  }
67
282
  //# sourceMappingURL=visual.js.map
@@ -265,10 +265,4 @@ export interface ToolResponseMap {
265
265
  export type ToolName = keyof ToolResponseMap;
266
266
  export type GetToolResponse<T extends ToolName> = ToolResponseMap[T];
267
267
  export type GetToolParams<T extends keyof ConsolidatedToolParams> = ConsolidatedToolParams[T];
268
- export declare function isSuccessResponse(response: BaseToolResponse): response is BaseToolResponse & {
269
- success: true;
270
- };
271
- export declare function isErrorResponse(response: BaseToolResponse): response is BaseToolResponse & {
272
- error: string;
273
- };
274
268
  //# sourceMappingURL=tool-types.d.ts.map
@@ -2,12 +2,5 @@
2
2
  * Auto-generated TypeScript types from tool schemas
3
3
  * This provides type safety and IntelliSense support
4
4
  */
5
- // Export a type guard to check if a response is successful
6
- export function isSuccessResponse(response) {
7
- return response.success === true;
8
- }
9
- // Export a type guard to check if a response has an error
10
- export function isErrorResponse(response) {
11
- return response.success === false && typeof response.error === 'string';
12
- }
5
+ export {};
13
6
  //# sourceMappingURL=tool-types.js.map