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,437 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+ import { Logger } from '../utils/logger.js';
3
+
4
+ export interface RCPreset {
5
+ id: string;
6
+ name: string;
7
+ path: string;
8
+ description?: string;
9
+ exposedEntities?: RCExposedEntity[];
10
+ }
11
+
12
+ export interface RCExposedEntity {
13
+ id: string;
14
+ label: string;
15
+ type: 'property' | 'function' | 'actor';
16
+ objectPath?: string;
17
+ propertyName?: string;
18
+ functionName?: string;
19
+ metadata?: Record<string, any>;
20
+ }
21
+
22
+ export class RcTools {
23
+ private log = new Logger('RcTools');
24
+ private presetCache = new Map<string, RCPreset>();
25
+ private retryAttempts = 3;
26
+ private retryDelay = 1000;
27
+
28
+ constructor(private bridge: UnrealBridge) {}
29
+
30
+ /**
31
+ * Execute with retry logic for transient failures
32
+ */
33
+ private async executeWithRetry<T>(
34
+ operation: () => Promise<T>,
35
+ operationName: string
36
+ ): Promise<T> {
37
+ let lastError: any;
38
+
39
+ for (let attempt = 1; attempt <= this.retryAttempts; attempt++) {
40
+ try {
41
+ return await operation();
42
+ } catch (error: any) {
43
+ lastError = error;
44
+ this.log.warn(`${operationName} attempt ${attempt} failed: ${error.message || error}`);
45
+
46
+ if (attempt < this.retryAttempts) {
47
+ await new Promise(resolve =>
48
+ setTimeout(resolve, this.retryDelay * attempt)
49
+ );
50
+ }
51
+ }
52
+ }
53
+
54
+ throw lastError;
55
+ }
56
+
57
+ /**
58
+ * Parse Python execution result with better error handling
59
+ */
60
+ private parsePythonResult(resp: any, operationName: string): any {
61
+ let out = '';
62
+ if (resp?.LogOutput && Array.isArray((resp as any).LogOutput)) {
63
+ out = (resp as any).LogOutput.map((l: any) => l.Output || '').join('');
64
+ } else if (typeof resp === 'string') {
65
+ out = resp;
66
+ } else {
67
+ out = JSON.stringify(resp);
68
+ }
69
+
70
+ const m = out.match(/RESULT:({.*})/);
71
+ if (m) {
72
+ try {
73
+ return JSON.parse(m[1]);
74
+ } catch (e) {
75
+ this.log.error(`Failed to parse ${operationName} result: ${e}`);
76
+ }
77
+ }
78
+
79
+ // Check for common error patterns
80
+ if (out.includes('ModuleNotFoundError')) {
81
+ return { success: false, error: 'Remote Control module not available. Ensure Remote Control plugin is enabled.' };
82
+ }
83
+ if (out.includes('AttributeError')) {
84
+ return { success: false, error: 'Remote Control API method not found. Check Unreal Engine version compatibility.' };
85
+ }
86
+
87
+ return { success: false, error: `${operationName} did not return a valid result: ${out.substring(0, 200)}` };
88
+ }
89
+
90
+ // Create a Remote Control Preset asset
91
+ async createPreset(params: { name: string; path?: string }) {
92
+ const name = params.name?.trim();
93
+ const path = (params.path || '/Game/RCPresets').replace(/\/$/, '');
94
+ if (!name) return { success: false, error: 'Preset name is required' };
95
+ const python = `
96
+ import unreal, json
97
+ import time
98
+ name = r"${name}"
99
+ base_path = r"${path}"
100
+ full_path = f"{base_path}/{name}"
101
+ try:
102
+ # Check if asset already exists
103
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
104
+ # If it exists, add a timestamp suffix to create a unique name
105
+ timestamp = str(int(time.time() * 1000))
106
+ unique_name = f"{name}_{timestamp}"
107
+ full_path = f"{base_path}/{unique_name}"
108
+ # Check again to ensure uniqueness
109
+ if unreal.EditorAssetLibrary.does_asset_exist(full_path):
110
+ print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path, 'existing': True}))
111
+ else:
112
+ # Continue with creation using unique name
113
+ name = unique_name
114
+ # Now create the preset if it doesn't exist
115
+ if not unreal.EditorAssetLibrary.does_asset_exist(full_path):
116
+ # Ensure directory exists
117
+ if not unreal.EditorAssetLibrary.does_directory_exist(base_path):
118
+ unreal.EditorAssetLibrary.make_directory(base_path)
119
+
120
+ asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
121
+ factory = None
122
+ try:
123
+ factory = unreal.RemoteControlPresetFactory()
124
+ except Exception:
125
+ # Factory might not be available in older versions
126
+ factory = None
127
+
128
+ asset = None
129
+ try:
130
+ if factory is not None:
131
+ asset = asset_tools.create_asset(asset_name=name, package_path=base_path, asset_class=unreal.RemoteControlPreset, factory=factory)
132
+ else:
133
+ # Try alternative creation method
134
+ asset = asset_tools.create_asset(asset_name=name, package_path=base_path, asset_class=unreal.RemoteControlPreset, factory=None)
135
+ except Exception as e:
136
+ # If creation fails, try to provide helpful error
137
+ if "RemoteControlPreset" in str(e):
138
+ print('RESULT:' + json.dumps({'success': False, 'error': 'RemoteControlPreset class not available. Ensure Remote Control plugin is enabled.'}))
139
+ else:
140
+ print('RESULT:' + json.dumps({'success': False, 'error': f'Create asset failed: {str(e)}'}))
141
+ raise SystemExit(0)
142
+
143
+ if asset:
144
+ # Save with suppressed validation warnings
145
+ try:
146
+ unreal.EditorAssetLibrary.save_asset(full_path, only_if_is_dirty=False)
147
+ print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path}))
148
+ except Exception as save_err:
149
+ # Asset was created but save had warnings - still consider success
150
+ print('RESULT:' + json.dumps({'success': True, 'presetPath': full_path, 'warning': 'Asset created with validation warnings'}))
151
+ else:
152
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Preset creation returned None'}))
153
+ except Exception as e:
154
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
155
+ `.trim();
156
+ const resp = await this.executeWithRetry(
157
+ () => this.bridge.executePython(python),
158
+ 'createPreset'
159
+ );
160
+
161
+ const result = this.parsePythonResult(resp, 'createPreset');
162
+
163
+ // Cache the preset if successful
164
+ if (result.success && result.presetPath) {
165
+ const preset: RCPreset = {
166
+ id: result.presetPath,
167
+ name: name,
168
+ path: result.presetPath,
169
+ description: `Created at ${new Date().toISOString()}`
170
+ };
171
+ this.presetCache.set(preset.id, preset);
172
+ }
173
+
174
+ return result;
175
+ }
176
+
177
+ // Expose an actor by label/name into a preset
178
+ async exposeActor(params: { presetPath: string; actorName: string }) {
179
+ const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nactor_name = r"${params.actorName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n if not preset:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))\n else:\n actor_sub = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)\n target = None\n for a in actor_sub.get_all_level_actors():\n if not a: continue\n try:\n if a.get_actor_label() == actor_name or a.get_name() == actor_name:\n target = a; break\n except Exception: pass\n if not target:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Actor not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_actor(preset, target, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
180
+ const resp = await this.executeWithRetry(
181
+ () => this.bridge.executePython(python),
182
+ 'exposeActor'
183
+ );
184
+
185
+ const result = this.parsePythonResult(resp, 'exposeActor');
186
+
187
+ // Clear cache for this preset to force refresh
188
+ if (result.success) {
189
+ this.presetCache.delete(params.presetPath);
190
+ }
191
+
192
+ return result;
193
+ }
194
+
195
+ // Expose a property on an object into a preset
196
+ async exposeProperty(params: { presetPath: string; objectPath: string; propertyName: string }) {
197
+ const python = `\nimport unreal, json\npreset_path = r"${params.presetPath}"\nobj_path = r"${params.objectPath}"\nprop_name = r"${params.propertyName}"\ntry:\n preset = unreal.EditorAssetLibrary.load_asset(preset_path)\n obj = unreal.load_object(None, obj_path)\n if not preset or not obj:\n print('RESULT:' + json.dumps({'success': False, 'error': 'Preset or object not found'}))\n else:\n try:\n unreal.RemoteControlFunctionLibrary.expose_property(preset, obj, prop_name, None)\n unreal.EditorAssetLibrary.save_asset(preset_path)\n print('RESULT:' + json.dumps({'success': True}))\n except Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\nexcept Exception as e:\n print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))\n`.trim();
198
+ const resp = await this.executeWithRetry(
199
+ () => this.bridge.executePython(python),
200
+ 'exposeProperty'
201
+ );
202
+
203
+ const result = this.parsePythonResult(resp, 'exposeProperty');
204
+
205
+ // Clear cache for this preset to force refresh
206
+ if (result.success) {
207
+ this.presetCache.delete(params.presetPath);
208
+ }
209
+
210
+ return result;
211
+ }
212
+
213
+ // List exposed fields (best-effort)
214
+ async listFields(params: { presetPath: string }) {
215
+ const python = `
216
+ import unreal, json
217
+ preset_path = r"${params.presetPath}"
218
+ try:
219
+ # First check if the asset exists
220
+ if not preset_path or not preset_path.startswith('/Game/'):
221
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Invalid preset path. Must start with /Game/'}))
222
+ elif not unreal.EditorAssetLibrary.does_asset_exist(preset_path):
223
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found at path: ' + preset_path}))
224
+ else:
225
+ preset = unreal.EditorAssetLibrary.load_asset(preset_path)
226
+ if not preset:
227
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to load preset'}))
228
+ else:
229
+ fields = []
230
+ try:
231
+ # Try to get exposed entities
232
+ if hasattr(preset, 'get_exposed_entities'):
233
+ for entity in preset.get_exposed_entities():
234
+ try:
235
+ fields.append({
236
+ 'id': str(entity.id) if hasattr(entity, 'id') else '',
237
+ 'label': str(entity.label) if hasattr(entity, 'label') else '',
238
+ 'path': str(getattr(entity, 'path', ''))
239
+ })
240
+ except Exception:
241
+ pass
242
+ except Exception as e:
243
+ # Method might not exist or be accessible
244
+ pass
245
+ print('RESULT:' + json.dumps({'success': True, 'fields': fields}))
246
+ except Exception as e:
247
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
248
+ `.trim();
249
+ const resp = await this.executeWithRetry(
250
+ () => this.bridge.executePython(python),
251
+ 'listFields'
252
+ );
253
+
254
+ return this.parsePythonResult(resp, 'listFields');
255
+ }
256
+
257
+ // Set a property value via Remote Control property endpoint
258
+ async setProperty(params: { objectPath: string; propertyName: string; value: any }) {
259
+ return this.executeWithRetry(async () => {
260
+ try {
261
+ // Validate value type and convert if needed
262
+ let processedValue = params.value;
263
+
264
+ // Handle special types
265
+ if (typeof params.value === 'object' && params.value !== null) {
266
+ // Check if it's a vector/rotator/transform
267
+ if ('x' in params.value || 'X' in params.value) {
268
+ processedValue = {
269
+ X: params.value.x || params.value.X || 0,
270
+ Y: params.value.y || params.value.Y || 0,
271
+ Z: params.value.z || params.value.Z || 0
272
+ };
273
+ }
274
+ }
275
+
276
+ const res = await this.bridge.httpCall('/remote/object/property', 'PUT', {
277
+ objectPath: params.objectPath,
278
+ propertyName: params.propertyName,
279
+ propertyValue: processedValue
280
+ });
281
+ return { success: true, result: res };
282
+ } catch (err: any) {
283
+ // Check for specific error types
284
+ const errorMsg = err?.message || String(err);
285
+ if (errorMsg.includes('404')) {
286
+ return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
287
+ }
288
+ if (errorMsg.includes('400')) {
289
+ return { success: false, error: `Invalid value type for property '${params.propertyName}'` };
290
+ }
291
+ return { success: false, error: errorMsg };
292
+ }
293
+ }, 'setProperty');
294
+ }
295
+
296
+ // Get a property value via Remote Control property endpoint
297
+ async getProperty(params: { objectPath: string; propertyName: string }) {
298
+ return this.executeWithRetry(async () => {
299
+ try {
300
+ const res = await this.bridge.httpCall('/remote/object/property', 'GET', {
301
+ objectPath: params.objectPath,
302
+ propertyName: params.propertyName
303
+ });
304
+ return { success: true, value: res };
305
+ } catch (err: any) {
306
+ const errorMsg = err?.message || String(err);
307
+ if (errorMsg.includes('404')) {
308
+ return { success: false, error: `Property '${params.propertyName}' not found on object '${params.objectPath}'` };
309
+ }
310
+ return { success: false, error: errorMsg };
311
+ }
312
+ }, 'getProperty');
313
+ }
314
+
315
+ /**
316
+ * List all available Remote Control presets
317
+ */
318
+ async listPresets(): Promise<{ success: boolean; presets?: RCPreset[]; error?: string }> {
319
+ const python = `
320
+ import unreal, json
321
+ try:
322
+ presets = []
323
+ # Try to list assets in common RC preset locations
324
+ for path in ["/Game/RCPresets", "/Game/RemoteControl", "/Game"]:
325
+ try:
326
+ assets = unreal.EditorAssetLibrary.list_assets(path, recursive=True)
327
+ for asset in assets:
328
+ if "RemoteControlPreset" in asset:
329
+ try:
330
+ preset = unreal.EditorAssetLibrary.load_asset(asset)
331
+ if preset:
332
+ presets.append({
333
+ "id": asset,
334
+ "name": preset.get_name(),
335
+ "path": asset,
336
+ "description": getattr(preset, 'description', '')
337
+ })
338
+ except Exception:
339
+ pass
340
+ except Exception:
341
+ pass
342
+ print('RESULT:' + json.dumps({'success': True, 'presets': presets}))
343
+ except Exception as e:
344
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
345
+ `.trim();
346
+
347
+ const resp = await this.executeWithRetry(
348
+ () => this.bridge.executePython(python),
349
+ 'listPresets'
350
+ );
351
+
352
+ const result = this.parsePythonResult(resp, 'listPresets');
353
+
354
+ // Update cache
355
+ if (result.success && result.presets) {
356
+ result.presets.forEach((p: RCPreset) => {
357
+ this.presetCache.set(p.id, p);
358
+ });
359
+ }
360
+
361
+ return result;
362
+ }
363
+
364
+ /**
365
+ * Delete a Remote Control preset
366
+ */
367
+ async deletePreset(presetId: string): Promise<{ success: boolean; error?: string }> {
368
+ const python = `
369
+ import unreal, json
370
+ preset_id = r"${presetId}"
371
+ try:
372
+ if unreal.EditorAssetLibrary.does_asset_exist(preset_id):
373
+ success = unreal.EditorAssetLibrary.delete_asset(preset_id)
374
+ if success:
375
+ print('RESULT:' + json.dumps({'success': True}))
376
+ else:
377
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Failed to delete preset'}))
378
+ else:
379
+ print('RESULT:' + json.dumps({'success': False, 'error': 'Preset not found'}))
380
+ except Exception as e:
381
+ print('RESULT:' + json.dumps({'success': False, 'error': str(e)}))
382
+ `.trim();
383
+
384
+ const resp = await this.executeWithRetry(
385
+ () => this.bridge.executePython(python),
386
+ 'deletePreset'
387
+ );
388
+
389
+ const result = this.parsePythonResult(resp, 'deletePreset');
390
+
391
+ // Remove from cache if successful
392
+ if (result.success) {
393
+ this.presetCache.delete(presetId);
394
+ }
395
+
396
+ return result;
397
+ }
398
+
399
+ /**
400
+ * Call an exposed function through Remote Control
401
+ */
402
+ async callFunction(params: {
403
+ presetPath: string;
404
+ functionName: string;
405
+ parameters?: Record<string, any>
406
+ }): Promise<{ success: boolean; result?: any; error?: string }> {
407
+ try {
408
+ const res = await this.bridge.httpCall('/remote/object/call', 'PUT', {
409
+ objectPath: params.presetPath,
410
+ functionName: params.functionName,
411
+ parameters: params.parameters || {}
412
+ });
413
+ return { success: true, result: res };
414
+ } catch (err: any) {
415
+ return { success: false, error: String(err?.message || err) };
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Validate connection to Remote Control
421
+ */
422
+ async validateConnection(): Promise<boolean> {
423
+ try {
424
+ await this.bridge.httpCall('/remote/info', 'GET', {});
425
+ return true;
426
+ } catch {
427
+ return false;
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Clear preset cache
433
+ */
434
+ clearCache(): void {
435
+ this.presetCache.clear();
436
+ }
437
+ }