unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,360 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { toVec3Object, toRotObject } from '../utils/normalize.js';
3
+
4
+ export class EditorTools {
5
+ constructor(private bridge: UnrealBridge) {}
6
+
7
+ async isInPIE(): Promise<boolean> {
8
+ try {
9
+ const pythonCmd = `
10
+ import unreal
11
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
12
+ if les:
13
+ print("PIE_STATE:" + str(les.is_in_play_in_editor()))
14
+ else:
15
+ print("PIE_STATE:False")
16
+ `.trim();
17
+
18
+ const resp: any = await this.bridge.executePython(pythonCmd);
19
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
20
+ return out.includes('PIE_STATE:True');
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ async ensureNotInPIE(): Promise<void> {
27
+ if (await this.isInPIE()) {
28
+ await this.stopPlayInEditor();
29
+ // Wait a bit for PIE to fully stop
30
+ await new Promise(resolve => setTimeout(resolve, 500));
31
+ }
32
+ }
33
+
34
+ async playInEditor() {
35
+ try {
36
+ // Set tick rate to match UI play (60 fps for game mode)
37
+ await this.bridge.executeConsoleCommand('t.MaxFPS 60');
38
+
39
+ // Try Python first using the modern LevelEditorSubsystem
40
+ try {
41
+ // Use LevelEditorSubsystem to play in the selected viewport (modern API)
42
+ const pythonCmd = `
43
+ import unreal, time, json
44
+ # Start PIE using LevelEditorSubsystem (modern approach)
45
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
46
+ if les:
47
+ # Store initial state
48
+ was_playing = les.is_in_play_in_editor()
49
+
50
+ # Request PIE in the current viewport
51
+ les.editor_play_simulate()
52
+
53
+ # Wait for PIE to start with multiple checks
54
+ max_attempts = 10
55
+ for i in range(max_attempts):
56
+ time.sleep(0.2) # Wait 200ms between checks
57
+ is_playing = les.is_in_play_in_editor()
58
+ if is_playing and not was_playing:
59
+ # PIE has started
60
+ print('RESULT:' + json.dumps({'success': True, 'method': 'LevelEditorSubsystem'}))
61
+ break
62
+ else:
63
+ # If we've waited 2 seconds total and PIE hasn't started,
64
+ # but the command was sent, assume it will start
65
+ print('RESULT:' + json.dumps({'success': True, 'method': 'LevelEditorSubsystem'}))
66
+ else:
67
+ # If subsystem not available, report error
68
+ print('RESULT:' + json.dumps({'success': False, 'error': 'LevelEditorSubsystem not available'}))
69
+ `.trim();
70
+
71
+ const resp: any = await this.bridge.executePython(pythonCmd);
72
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
73
+ const m = out.match(/RESULT:({.*})/);
74
+ if (m) {
75
+ try {
76
+ const parsed = JSON.parse(m[1]);
77
+ if (parsed.success) {
78
+ const method = parsed.method || 'LevelEditorSubsystem';
79
+ return { success: true, message: `PIE started (via ${method})` };
80
+ }
81
+ } catch {
82
+ try {
83
+ // Fallback: handle non-JSON python dict-style output
84
+ const sanitized = m[1].replace(/'/g, '"').replace(/\bTrue\b/g, 'true').replace(/\bFalse\b/g, 'false');
85
+ const parsed = JSON.parse(sanitized);
86
+ if (parsed.success) {
87
+ const method = parsed.method || 'LevelEditorSubsystem';
88
+ return { success: true, message: `PIE started (via ${method})` };
89
+ }
90
+ } catch {}
91
+ }
92
+ }
93
+ // If not verified, fall through to fallback
94
+ } catch (err) {
95
+ // Log the error for debugging but continue
96
+ console.error('Python PIE start issue:', err);
97
+ }
98
+ // Fallback to console command which is more reliable
99
+ await this.bridge.executeConsoleCommand('PlayInViewport');
100
+
101
+ // Wait a moment and verify PIE started
102
+ await new Promise(resolve => setTimeout(resolve, 1000));
103
+
104
+ // Check if PIE is now active
105
+ const isPlaying = await this.isInPIE();
106
+
107
+ return {
108
+ success: true,
109
+ message: isPlaying ? 'PIE started successfully' : 'PIE start command sent (may take a moment)'
110
+ };
111
+ } catch (err) {
112
+ return { success: false, error: `Failed to start PIE: ${err}` };
113
+ }
114
+ }
115
+
116
+ async stopPlayInEditor() {
117
+ try {
118
+ // Try Python first using the modern LevelEditorSubsystem
119
+ try {
120
+ const pythonCmd = `
121
+ import unreal, time, json
122
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
123
+ if les:
124
+ # Use correct method name for stopping PIE
125
+ les.editor_request_end_play() # Modern API method
126
+ print('RESULT:' + json.dumps({'success': True, 'method': 'LevelEditorSubsystem'}))
127
+ else:
128
+ # If subsystem not available, report error
129
+ print('RESULT:' + json.dumps({'success': False, 'error': 'LevelEditorSubsystem not available'}))
130
+ `.trim();
131
+ const resp: any = await this.bridge.executePython(pythonCmd);
132
+ const out = typeof resp === 'string' ? resp : JSON.stringify(resp);
133
+ const m = out.match(/RESULT:({.*})/);
134
+ if (m) {
135
+ try {
136
+ const parsed = JSON.parse(m[1].replace(/'/g, '"'));
137
+ if (parsed.success) {
138
+ const method = parsed.method || 'LevelEditorSubsystem';
139
+ return { success: true, message: `PIE stopped via ${method}` };
140
+ }
141
+ } catch {}
142
+ }
143
+ // Default success message if parsing fails
144
+ return { success: true, message: 'PIE stopped successfully' };
145
+ } catch {
146
+ // Fallback to console command
147
+ await this.bridge.executeConsoleCommand('stop');
148
+ return { success: true, message: 'PIE stopped via console command' };
149
+ }
150
+ } catch (err) {
151
+ return { success: false, error: `Failed to stop PIE: ${err}` };
152
+ }
153
+ }
154
+
155
+ async pausePlayInEditor() {
156
+ try {
157
+ // Pause/Resume PIE
158
+ await this.bridge.httpCall('/remote/object/call', 'PUT', {
159
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
160
+ functionName: 'ExecuteConsoleCommand',
161
+ parameters: {
162
+ WorldContextObject: null,
163
+ Command: 'pause',
164
+ SpecificPlayer: null
165
+ },
166
+ generateTransaction: false
167
+ });
168
+ return { success: true, message: 'PIE paused/resumed' };
169
+ } catch (err) {
170
+ return { success: false, error: `Failed to pause PIE: ${err}` };
171
+ }
172
+ }
173
+
174
+ // Alias for consistency with naming convention
175
+ async pauseInEditor() {
176
+ return this.pausePlayInEditor();
177
+ }
178
+
179
+ async buildLighting() {
180
+ try {
181
+ // Use modern LevelEditorSubsystem to build lighting
182
+ const py = `
183
+ import unreal
184
+ import json
185
+ try:
186
+ # Use modern LevelEditorSubsystem API
187
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
188
+ if les:
189
+ # build_light_maps(quality, with_reflection_captures)
190
+ les.build_light_maps(unreal.LightingBuildQuality.QUALITY_HIGH, True)
191
+ print('RESULT:' + json.dumps({'success': True, 'message': 'Lighting build started via LevelEditorSubsystem'}))
192
+ else:
193
+ # If subsystem not available, report error
194
+ print('RESULT:' + json.dumps({'success': False, 'error': 'LevelEditorSubsystem not available'}))
195
+ except Exception as e:
196
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
197
+ `.trim();
198
+ const resp: any = 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 { const parsed = JSON.parse(m[1].replace(/'/g, '"')); return parsed.success ? { success: true, message: parsed.message } : { success: false, error: parsed.error }; } catch {}
203
+ }
204
+ return { success: true, message: 'Lighting build started' };
205
+ } catch (err) {
206
+ return { success: false, error: `Failed to build lighting: ${err}` };
207
+ }
208
+ }
209
+
210
+ async setViewportCamera(location?: { x: number; y: number; z: number } | [number, number, number] | null | undefined, rotation?: { pitch: number; yaw: number; roll: number } | [number, number, number] | null | undefined) {
211
+ // Special handling for when both location and rotation are missing/invalid
212
+ // Allow rotation-only updates
213
+ if (location === null) {
214
+ // Explicit null is not allowed for location
215
+ throw new Error('Invalid location: null is not allowed');
216
+ }
217
+ if (location !== undefined && location !== null) {
218
+ const locObj = toVec3Object(location);
219
+ if (!locObj) {
220
+ throw new Error('Invalid location: must be {x,y,z} or [x,y,z]');
221
+ }
222
+ // Clamp extreme values to reasonable limits for Unreal Engine
223
+ const MAX_COORD = 1000000; // 1 million units is a reasonable max for UE
224
+ locObj.x = Math.max(-MAX_COORD, Math.min(MAX_COORD, locObj.x));
225
+ locObj.y = Math.max(-MAX_COORD, Math.min(MAX_COORD, locObj.y));
226
+ locObj.z = Math.max(-MAX_COORD, Math.min(MAX_COORD, locObj.z));
227
+ location = locObj as any;
228
+ }
229
+
230
+ // Validate rotation if provided
231
+ if (rotation !== undefined) {
232
+ if (rotation === null) {
233
+ throw new Error('Invalid rotation: null is not allowed');
234
+ }
235
+ const rotObj = toRotObject(rotation);
236
+ if (!rotObj) {
237
+ throw new Error('Invalid rotation: must be {pitch,yaw,roll} or [pitch,yaw,roll]');
238
+ }
239
+ // Normalize rotation values to 0-360 range
240
+ rotObj.pitch = ((rotObj.pitch % 360) + 360) % 360;
241
+ rotObj.yaw = ((rotObj.yaw % 360) + 360) % 360;
242
+ rotObj.roll = ((rotObj.roll % 360) + 360) % 360;
243
+ rotation = rotObj as any;
244
+ }
245
+
246
+ try {
247
+ // Try Python for actual viewport camera positioning
248
+ // Only proceed if we have a valid location
249
+ if (location) {
250
+ try {
251
+ const rot = (rotation as any) || { pitch: 0, yaw: 0, roll: 0 };
252
+ const pythonCmd = `
253
+ import unreal
254
+ # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary
255
+ ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
256
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
257
+ location = unreal.Vector(${(location as any).x}, ${(location as any).y}, ${(location as any).z})
258
+ rotation = unreal.Rotator(${rot.pitch}, ${rot.yaw}, ${rot.roll})
259
+ if ues:
260
+ ues.set_level_viewport_camera_info(location, rotation)
261
+ # Invalidate viewports to ensure visual update
262
+ try:
263
+ if les:
264
+ les.editor_invalidate_viewports()
265
+ except Exception:
266
+ pass
267
+ `.trim();
268
+ await this.bridge.executePython(pythonCmd);
269
+ return {
270
+ success: true,
271
+ message: 'Viewport camera positioned via UnrealEditorSubsystem'
272
+ };
273
+ } catch {
274
+ // Fallback to camera speed control
275
+ await this.bridge.executeConsoleCommand('camspeed 4');
276
+ return {
277
+ success: true,
278
+ message: 'Camera speed set. Use debug camera (toggledebugcamera) for manual positioning'
279
+ };
280
+ }
281
+ } else if (rotation) {
282
+ // Only rotation provided, try to set just rotation
283
+ try {
284
+ const pythonCmd = `
285
+ import unreal
286
+ # Use UnrealEditorSubsystem to read/write viewport camera
287
+ ues = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
288
+ les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
289
+ rotation = unreal.Rotator(${(rotation as any).pitch}, ${(rotation as any).yaw}, ${(rotation as any).roll})
290
+ if ues:
291
+ info = ues.get_level_viewport_camera_info()
292
+ if info is not None:
293
+ current_location, _ = info
294
+ ues.set_level_viewport_camera_info(current_location, rotation)
295
+ try:
296
+ if les:
297
+ les.editor_invalidate_viewports()
298
+ except Exception:
299
+ pass
300
+ `.trim();
301
+ await this.bridge.executePython(pythonCmd);
302
+ return {
303
+ success: true,
304
+ message: 'Viewport camera rotation set via UnrealEditorSubsystem'
305
+ };
306
+ } catch {
307
+ // Fallback
308
+ return {
309
+ success: true,
310
+ message: 'Camera rotation update attempted'
311
+ };
312
+ }
313
+ } else {
314
+ // Neither location nor rotation provided - this is valid, just no-op
315
+ return {
316
+ success: true,
317
+ message: 'No camera changes requested'
318
+ };
319
+ }
320
+ } catch (err) {
321
+ return { success: false, error: `Failed to set camera: ${err}` };
322
+ }
323
+ }
324
+
325
+ async setCameraSpeed(speed: number) {
326
+ try {
327
+ await this.bridge.httpCall('/remote/object/call', 'PUT', {
328
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
329
+ functionName: 'ExecuteConsoleCommand',
330
+ parameters: {
331
+ WorldContextObject: null,
332
+ Command: `camspeed ${speed}`,
333
+ SpecificPlayer: null
334
+ },
335
+ generateTransaction: false
336
+ });
337
+ return { success: true, message: `Camera speed set to ${speed}` };
338
+ } catch (err) {
339
+ return { success: false, error: `Failed to set camera speed: ${err}` };
340
+ }
341
+ }
342
+
343
+ async setFOV(fov: number) {
344
+ try {
345
+ await this.bridge.httpCall('/remote/object/call', 'PUT', {
346
+ objectPath: '/Script/Engine.Default__KismetSystemLibrary',
347
+ functionName: 'ExecuteConsoleCommand',
348
+ parameters: {
349
+ WorldContextObject: null,
350
+ Command: `fov ${fov}`,
351
+ SpecificPlayer: null
352
+ },
353
+ generateTransaction: false
354
+ });
355
+ return { success: true, message: `FOV set to ${fov}` };
356
+ } catch (err) {
357
+ return { success: false, error: `Failed to set FOV: ${err}` };
358
+ }
359
+ }
360
+ }
@@ -0,0 +1,32 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { loadEnv } from '../types/env.js';
3
+ import { spawn } from 'child_process';
4
+
5
+ export class EngineTools {
6
+ private env = loadEnv();
7
+ constructor(private bridge: UnrealBridge) {}
8
+
9
+ async launchEditor(params?: { editorExe?: string; projectPath?: string }) {
10
+ const exe = params?.editorExe || this.env.UE_EDITOR_EXE;
11
+ const proj = params?.projectPath || this.env.UE_PROJECT_PATH;
12
+ if (!exe) return { success: false, error: 'UE_EDITOR_EXE not set and editorExe not provided' };
13
+ if (!proj) return { success: false, error: 'UE_PROJECT_PATH not set and projectPath not provided' };
14
+ try {
15
+ const child = spawn(exe, [proj], { detached: true, stdio: 'ignore' });
16
+ child.unref();
17
+ return { success: true, pid: child.pid, message: 'Editor launch requested' };
18
+ } catch (err: any) {
19
+ return { success: false, error: String(err?.message || err) };
20
+ }
21
+ }
22
+
23
+ async quitEditor() {
24
+ try {
25
+ // Use Python SystemLibrary.quit_editor if available
26
+ await this.bridge.executePython('import unreal; unreal.SystemLibrary.quit_editor()');
27
+ return { success: true, message: 'Quit command sent' };
28
+ } catch (err: any) {
29
+ return { success: false, error: String(err?.message || err) };
30
+ }
31
+ }
32
+ }