unreal-engine-mcp-server 0.3.0 → 0.4.0

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 (43) hide show
  1. package/.env.production +6 -1
  2. package/Dockerfile +11 -28
  3. package/README.md +1 -2
  4. package/dist/index.js +120 -54
  5. package/dist/resources/actors.js +71 -13
  6. package/dist/resources/assets.d.ts +3 -2
  7. package/dist/resources/assets.js +96 -72
  8. package/dist/resources/levels.js +2 -2
  9. package/dist/tools/assets.js +6 -2
  10. package/dist/tools/build_environment_advanced.js +46 -42
  11. package/dist/tools/consolidated-tool-definitions.d.ts +232 -15
  12. package/dist/tools/consolidated-tool-definitions.js +173 -8
  13. package/dist/tools/consolidated-tool-handlers.js +331 -718
  14. package/dist/tools/debug.js +4 -6
  15. package/dist/tools/rc.js +2 -2
  16. package/dist/tools/sequence.js +21 -2
  17. package/dist/unreal-bridge.d.ts +4 -1
  18. package/dist/unreal-bridge.js +211 -53
  19. package/dist/utils/http.js +4 -2
  20. package/dist/utils/response-validator.d.ts +6 -1
  21. package/dist/utils/response-validator.js +43 -15
  22. package/package.json +5 -5
  23. package/server.json +2 -2
  24. package/src/index.ts +120 -56
  25. package/src/resources/actors.ts +51 -13
  26. package/src/resources/assets.ts +97 -73
  27. package/src/resources/levels.ts +2 -2
  28. package/src/tools/assets.ts +6 -2
  29. package/src/tools/build_environment_advanced.ts +46 -42
  30. package/src/tools/consolidated-tool-definitions.ts +173 -8
  31. package/src/tools/consolidated-tool-handlers.ts +318 -747
  32. package/src/tools/debug.ts +4 -6
  33. package/src/tools/rc.ts +2 -2
  34. package/src/tools/sequence.ts +21 -2
  35. package/src/unreal-bridge.ts +163 -60
  36. package/src/utils/http.ts +7 -4
  37. package/src/utils/response-validator.ts +48 -19
  38. package/dist/tools/tool-definitions.d.ts +0 -4919
  39. package/dist/tools/tool-definitions.js +0 -1007
  40. package/dist/tools/tool-handlers.d.ts +0 -47
  41. package/dist/tools/tool-handlers.js +0 -863
  42. package/src/tools/tool-definitions.ts +0 -1023
  43. package/src/tools/tool-handlers.ts +0 -973
@@ -27,7 +27,8 @@ export class ResponseValidator {
27
27
  try {
28
28
  const validator = this.ajv.compile(outputSchema);
29
29
  this.validators.set(toolName, validator);
30
- log.info(`Registered output schema for tool: ${toolName}`);
30
+ // Demote per-tool schema registration to debug to reduce log noise
31
+ log.debug(`Registered output schema for tool: ${toolName}`);
31
32
  }
32
33
  catch (_error) {
33
34
  log.error(`Failed to compile output schema for ${toolName}:`, _error);
@@ -75,14 +76,17 @@ export class ResponseValidator {
75
76
  };
76
77
  }
77
78
  /**
78
- * Wrap a tool response with validation
79
+ * Wrap a tool response with validation and MCP-compliant content shape.
80
+ *
81
+ * MCP tools/call responses must contain a `content` array. Many internal
82
+ * handlers return structured JSON objects (e.g., { success, message, ... }).
83
+ * This wrapper serializes such objects into a single text block while keeping
84
+ * existing `content` responses intact.
79
85
  */
80
86
  wrapResponse(toolName, response) {
81
87
  // Ensure response is safe to serialize first
82
88
  try {
83
- // The response should already be cleaned, but double-check
84
89
  if (response && typeof response === 'object') {
85
- // Make sure we can serialize it
86
90
  JSON.stringify(response);
87
91
  }
88
92
  }
@@ -90,21 +94,45 @@ export class ResponseValidator {
90
94
  log.error(`Response for ${toolName} contains circular references, cleaning...`);
91
95
  response = cleanObject(response);
92
96
  }
93
- const validation = this.validateResponse(toolName, response);
94
- // Add validation metadata
97
+ // If handler already returned MCP content, keep it as-is (still validate)
98
+ const alreadyMcpShaped = response && typeof response === 'object' && Array.isArray(response.content);
99
+ // Choose the payload to validate: if already MCP-shaped, validate the
100
+ // structured content extracted from text; otherwise validate the object directly.
101
+ const validationTarget = alreadyMcpShaped ? response : response;
102
+ const validation = this.validateResponse(toolName, validationTarget);
95
103
  if (!validation.valid) {
96
104
  log.warn(`Tool ${toolName} response validation failed:`, validation.errors);
97
- // Add warning to response but don't fail
98
- if (response && typeof response === 'object') {
99
- response._validation = {
100
- valid: false,
101
- errors: validation.errors
102
- };
105
+ }
106
+ // If it's already MCP-shaped, return as-is (optionally append validation meta)
107
+ if (alreadyMcpShaped) {
108
+ if (!validation.valid) {
109
+ try {
110
+ response._validation = { valid: false, errors: validation.errors };
111
+ }
112
+ catch { }
103
113
  }
114
+ return response;
115
+ }
116
+ // Otherwise, wrap structured result into MCP content
117
+ let text;
118
+ try {
119
+ // Pretty-print small objects for readability
120
+ text = typeof response === 'string'
121
+ ? response
122
+ : JSON.stringify(response ?? { success: true }, null, 2);
123
+ }
124
+ catch (_e) {
125
+ text = String(response);
126
+ }
127
+ const wrapped = {
128
+ content: [
129
+ { type: 'text', text }
130
+ ]
131
+ };
132
+ if (!validation.valid) {
133
+ wrapped._validation = { valid: false, errors: validation.errors };
104
134
  }
105
- // Don't add structuredContent to the response - it's for internal validation only
106
- // Adding it can cause circular references
107
- return response;
135
+ return wrapped;
108
136
  }
109
137
  /**
110
138
  * Get validation statistics
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "unreal-engine-mcp-server",
3
- "version": "0.3.0",
4
- "description": "Production-ready MCP server for Unreal Engine integration with consolidated and individual tool modes",
3
+ "version": "0.4.0",
4
+ "description": "Production-ready MCP server for Unreal Engine integration using consolidated tools only",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "bin": {
9
- "unreal-mcp-server": "dist/cli.js"
9
+ "unreal-engine-mcp-server": "dist/cli.js"
10
10
  },
11
11
  "scripts": {
12
12
  "build": "tsc -p tsconfig.json",
@@ -34,9 +34,9 @@
34
34
  "license": "MIT",
35
35
  "mcpName": "io.github.ChiR24/unreal-engine-mcp",
36
36
  "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.4.0",
37
+ "@modelcontextprotocol/sdk": "^1.18.1",
38
38
  "ajv": "^8.12.0",
39
- "axios": "^1.7.2",
39
+ "axios": "^1.12.2",
40
40
  "dotenv": "^16.4.5",
41
41
  "ws": "^8.18.0",
42
42
  "yargs": "^17.7.2",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3
3
  "name": "io.github.ChiR24/unreal-engine-mcp",
4
4
  "description": "Production-ready MCP server for Unreal Engine with comprehensive game development tools",
5
- "version": "0.3.0",
5
+ "version": "0.4.0",
6
6
  "packages": [
7
7
  {
8
8
  "registry_type": "npm",
9
9
  "registry_base_url": "https://registry.npmjs.org",
10
10
  "identifier": "unreal-engine-mcp-server",
11
- "version": "0.3.0",
11
+ "version": "0.4.0",
12
12
  "transport": {
13
13
  "type": "stdio"
14
14
  },
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import { BlueprintTools } from './tools/blueprint.js';
17
17
  import { LevelTools } from './tools/level.js';
18
18
  import { LightingTools } from './tools/lighting.js';
19
19
  import { LandscapeTools } from './tools/landscape.js';
20
+ import { BuildEnvironmentAdvanced } from './tools/build_environment_advanced.js';
20
21
  import { FoliageTools } from './tools/foliage.js';
21
22
  import { DebugVisualizationTools } from './tools/debug.js';
22
23
  import { PerformanceTools } from './tools/performance.js';
@@ -27,8 +28,6 @@ import { SequenceTools } from './tools/sequence.js';
27
28
  import { IntrospectionTools } from './tools/introspection.js';
28
29
  import { VisualTools } from './tools/visual.js';
29
30
  import { EngineTools } from './tools/engine.js';
30
- import { toolDefinitions } from './tools/tool-definitions.js';
31
- import { handleToolCall } from './tools/tool-handlers.js';
32
31
  import { consolidatedToolDefinitions } from './tools/consolidated-tool-definitions.js';
33
32
  import { handleConsolidatedToolCall } from './tools/consolidated-tool-handlers.js';
34
33
  import { prompts } from './prompts/index.js';
@@ -75,16 +74,19 @@ const metrics: PerformanceMetrics = {
75
74
  recentErrors: []
76
75
  };
77
76
 
77
+ // Health check timer and last success tracking (stop pings after inactivity)
78
+ let healthCheckTimer: NodeJS.Timeout | undefined;
79
+ let lastHealthSuccessAt = 0;
80
+
78
81
  // Configuration
79
82
  const CONFIG = {
80
- // Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
81
- USE_CONSOLIDATED_TOOLS: process.env.USE_CONSOLIDATED_TOOLS !== 'false',
83
+ // Tooling: use consolidated tools only (13 tools)
82
84
  // Connection retry settings
83
85
  MAX_RETRY_ATTEMPTS: 3,
84
86
  RETRY_DELAY_MS: 2000,
85
87
  // Server info
86
88
  SERVER_NAME: 'unreal-engine-mcp',
87
- SERVER_VERSION: '0.3.0',
89
+ SERVER_VERSION: '0.4.0',
88
90
  // Monitoring
89
91
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
90
92
  };
@@ -111,11 +113,16 @@ function trackPerformance(startTime: number, success: boolean) {
111
113
 
112
114
  // Health check function
113
115
  async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
116
+ // If not connected, do not attempt any ping (stay quiet)
117
+ if (!bridge.isConnected) {
118
+ return false;
119
+ }
114
120
  try {
115
121
  // Use a safe echo command that doesn't affect any settings
116
122
  await bridge.executeConsoleCommand('echo MCP Server Health Check');
117
123
  metrics.connectionStatus = 'connected';
118
124
  metrics.lastHealthCheck = new Date();
125
+ lastHealthSuccessAt = Date.now();
119
126
  return true;
120
127
  } catch (err1) {
121
128
  // Fallback: minimal Python ping (if Python plugin is enabled)
@@ -123,11 +130,13 @@ async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
123
130
  await bridge.executePython("import sys; sys.stdout.write('OK')");
124
131
  metrics.connectionStatus = 'connected';
125
132
  metrics.lastHealthCheck = new Date();
133
+ lastHealthSuccessAt = Date.now();
126
134
  return true;
127
135
  } catch (err2) {
128
136
  metrics.connectionStatus = 'error';
129
137
  metrics.lastHealthCheck = new Date();
130
- log.warn('Health check failed (console and python):', err1, err2);
138
+ // Avoid noisy warnings when engine may be shutting down; log at debug
139
+ log.debug('Health check failed (console and python):', err1, err2);
131
140
  return false;
132
141
  }
133
142
  }
@@ -135,50 +144,68 @@ async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
135
144
 
136
145
  export async function createServer() {
137
146
  const bridge = new UnrealBridge();
147
+ // Disable auto-reconnect loops; connect only on-demand
148
+ bridge.setAutoReconnectEnabled(false);
138
149
 
139
- // Initialize response validation with schemas
140
- log.info('Initializing response validation...');
141
- const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
150
+ // Initialize response validation with schemas
151
+ log.debug('Initializing response validation...');
152
+ const toolDefs = consolidatedToolDefinitions;
142
153
  toolDefs.forEach((tool: any) => {
143
154
  if (tool.outputSchema) {
144
155
  responseValidator.registerSchema(tool.name, tool.outputSchema);
145
156
  }
146
157
  });
147
- log.info(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
148
-
149
- // Connect to UE5 Remote Control with retries and timeout
150
- const connected = await bridge.tryConnect(
151
- CONFIG.MAX_RETRY_ATTEMPTS,
152
- 5000, // 5 second timeout per attempt
153
- CONFIG.RETRY_DELAY_MS
154
- );
155
-
156
- if (connected) {
157
- metrics.connectionStatus = 'connected';
158
- log.info('Successfully connected to Unreal Engine');
159
- } else {
160
- log.warn('Could not connect to Unreal Engine after retries');
161
- log.info('Server will start anyway - connection will be retried periodically');
162
- log.info('Make sure Unreal Engine is running with Remote Control enabled');
163
- metrics.connectionStatus = 'disconnected';
164
-
165
- // Schedule automatic reconnection attempts
166
- setInterval(async () => {
158
+ // Summary at debug level to avoid repeated noisy blocks in some shells
159
+ log.debug(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
160
+
161
+ // Do NOT connect to Unreal at startup; connect on demand
162
+ log.debug('Server starting without connecting to Unreal Engine');
163
+ metrics.connectionStatus = 'disconnected';
164
+
165
+ // Health checks manager (only active when connected)
166
+ const startHealthChecks = () => {
167
+ if (healthCheckTimer) return;
168
+ lastHealthSuccessAt = Date.now();
169
+ healthCheckTimer = setInterval(async () => {
170
+ // Only attempt health pings while connected; stay silent otherwise
167
171
  if (!bridge.isConnected) {
168
- log.info('Attempting to reconnect to Unreal Engine...');
169
- const reconnected = await bridge.tryConnect(1, 5000, 0); // Single attempt
170
- if (reconnected) {
171
- log.info('Reconnected to Unreal Engine successfully');
172
- metrics.connectionStatus = 'connected';
172
+ // Optionally pause fully after 5 minutes of no success
173
+ const FIVE_MIN_MS = 5 * 60 * 1000;
174
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
175
+ if (healthCheckTimer) {
176
+ clearInterval(healthCheckTimer);
177
+ healthCheckTimer = undefined;
178
+ }
179
+ log.info('Health checks paused after 5 minutes without a successful response');
173
180
  }
181
+ return;
174
182
  }
175
- }, 10000); // Try every 10 seconds
176
- }
177
-
178
- // Start periodic health checks
179
- setInterval(() => {
180
- performHealthCheck(bridge);
181
- }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
183
+
184
+ await performHealthCheck(bridge);
185
+ // Stop sending echoes if we haven't had a successful response in > 5 minutes
186
+ const FIVE_MIN_MS = 5 * 60 * 1000;
187
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
188
+ if (healthCheckTimer) {
189
+ clearInterval(healthCheckTimer);
190
+ healthCheckTimer = undefined;
191
+ log.info('Health checks paused after 5 minutes without a successful response');
192
+ }
193
+ }
194
+ }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
195
+ };
196
+
197
+ // On-demand connection helper
198
+ const ensureConnectedOnDemand = async (): Promise<boolean> => {
199
+ if (bridge.isConnected) return true;
200
+ const ok = await bridge.tryConnect(3, 5000, 1000);
201
+ if (ok) {
202
+ metrics.connectionStatus = 'connected';
203
+ startHealthChecks();
204
+ } else {
205
+ metrics.connectionStatus = 'disconnected';
206
+ }
207
+ return ok;
208
+ };
182
209
 
183
210
  // Resources
184
211
  const assetResources = new AssetResources(bridge);
@@ -198,6 +225,7 @@ export async function createServer() {
198
225
  const lightingTools = new LightingTools(bridge);
199
226
  const landscapeTools = new LandscapeTools(bridge);
200
227
  const foliageTools = new FoliageTools(bridge);
228
+ const buildEnvAdvanced = new BuildEnvironmentAdvanced(bridge);
201
229
  const debugTools = new DebugVisualizationTools(bridge);
202
230
  const performanceTools = new PerformanceTools(bridge);
203
231
  const audioTools = new AudioTools(bridge);
@@ -272,6 +300,10 @@ export async function createServer() {
272
300
  const uri = request.params.uri;
273
301
 
274
302
  if (uri === 'ue://assets') {
303
+ const ok = await ensureConnectedOnDemand();
304
+ if (!ok) {
305
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
306
+ }
275
307
  const list = await assetResources.list('/Game', true);
276
308
  return {
277
309
  contents: [{
@@ -283,6 +315,10 @@ export async function createServer() {
283
315
  }
284
316
 
285
317
  if (uri === 'ue://actors') {
318
+ const ok = await ensureConnectedOnDemand();
319
+ if (!ok) {
320
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
321
+ }
286
322
  const list = await actorResources.listActors();
287
323
  return {
288
324
  contents: [{
@@ -294,6 +330,10 @@ export async function createServer() {
294
330
  }
295
331
 
296
332
  if (uri === 'ue://level') {
333
+ const ok = await ensureConnectedOnDemand();
334
+ if (!ok) {
335
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
336
+ }
297
337
  const level = await levelResources.getCurrentLevel();
298
338
  return {
299
339
  contents: [{
@@ -305,6 +345,10 @@ export async function createServer() {
305
345
  }
306
346
 
307
347
  if (uri === 'ue://exposed') {
348
+ const ok = await ensureConnectedOnDemand();
349
+ if (!ok) {
350
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
351
+ }
308
352
  try {
309
353
  const exposed = await bridge.getExposed();
310
354
  return {
@@ -327,11 +371,13 @@ export async function createServer() {
327
371
 
328
372
  if (uri === 'ue://health') {
329
373
  const uptimeMs = Date.now() - metrics.uptime;
330
- // Query engine version and feature flags (best-effort)
374
+ // Query engine version and feature flags only when connected
331
375
  let versionInfo: any = {};
332
376
  let featureFlags: any = {};
333
- try { versionInfo = await bridge.getEngineVersion(); } catch {}
334
- try { featureFlags = await bridge.getFeatureFlags(); } catch {}
377
+ if (bridge.isConnected) {
378
+ try { versionInfo = await bridge.getEngineVersion(); } catch {}
379
+ try { featureFlags = await bridge.getFeatureFlags(); } catch {}
380
+ }
335
381
  const health = {
336
382
  status: metrics.connectionStatus,
337
383
  uptime: Math.floor(uptimeMs / 1000),
@@ -369,6 +415,10 @@ export async function createServer() {
369
415
  }
370
416
 
371
417
  if (uri === 'ue://version') {
418
+ const ok = await ensureConnectedOnDemand();
419
+ if (!ok) {
420
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
421
+ }
372
422
  const info = await bridge.getEngineVersion();
373
423
  return {
374
424
  contents: [{
@@ -382,20 +432,34 @@ export async function createServer() {
382
432
  throw new Error(`Unknown resource: ${uri}`);
383
433
  });
384
434
 
385
- // Handle tool listing - switch between consolidated (13) or individual (36) tools
435
+ // Handle tool listing - consolidated tools only
386
436
  server.setRequestHandler(ListToolsRequestSchema, async () => {
387
- log.info(`Serving ${CONFIG.USE_CONSOLIDATED_TOOLS ? 'consolidated' : 'individual'} tools`);
437
+ log.info('Serving consolidated tools');
388
438
  return {
389
- tools: CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions
439
+ tools: consolidatedToolDefinitions
390
440
  };
391
441
  });
392
442
 
393
- // Handle tool calls - switch between consolidated (13) or individual (36) tools
443
+ // Handle tool calls - consolidated tools only (13)
394
444
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
395
445
  const { name, arguments: args } = request.params;
396
446
  const startTime = Date.now();
447
+
448
+ // Ensure connection only when needed, with 3 attempts
449
+ const connected = await ensureConnectedOnDemand();
450
+ if (!connected) {
451
+ const notConnected = {
452
+ content: [{ type: 'text', text: 'Unreal Engine is not connected (after 3 attempts). Please open UE and try again.' }],
453
+ success: false,
454
+ error: 'UE_NOT_CONNECTED',
455
+ retriable: false,
456
+ scope: `tool-call/${name}`
457
+ } as any;
458
+ trackPerformance(startTime, false);
459
+ return notConnected;
460
+ }
397
461
 
398
- // Create tools object for handler
462
+ // Create tools object for handler
399
463
  const tools = {
400
464
  actorTools,
401
465
  assetTools,
@@ -409,6 +473,7 @@ export async function createServer() {
409
473
  lightingTools,
410
474
  landscapeTools,
411
475
  foliageTools,
476
+ buildEnvAdvanced,
412
477
  debugTools,
413
478
  performanceTools,
414
479
  audioTools,
@@ -418,19 +483,18 @@ export async function createServer() {
418
483
  introspectionTools,
419
484
  visualTools,
420
485
  engineTools,
486
+ // Resources for listing and info
487
+ assetResources,
488
+ actorResources,
489
+ levelResources,
421
490
  bridge
422
491
  };
423
492
 
424
- // Use consolidated or individual handler based on configuration
493
+ // Execute consolidated tool handler
425
494
  try {
426
495
  log.debug(`Executing tool: ${name}`);
427
496
 
428
- let result;
429
- if (CONFIG.USE_CONSOLIDATED_TOOLS) {
430
- result = await handleConsolidatedToolCall(name, args, tools);
431
- } else {
432
- result = await handleToolCall(name, args, tools);
433
- }
497
+ let result = await handleConsolidatedToolCall(name, args, tools);
434
498
 
435
499
  log.debug(`Tool ${name} returned result`);
436
500
 
@@ -34,25 +34,63 @@ export class ActorResources {
34
34
  // Use Python to get actors via EditorActorSubsystem
35
35
  try {
36
36
  const pythonCode = `
37
- import unreal
37
+ import unreal, json
38
38
  actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
39
- actors = actor_subsystem.get_all_level_actors()
39
+ actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
40
40
  actor_list = []
41
41
  for actor in actors:
42
- if actor:
43
- actor_list.append({
44
- 'name': actor.get_name(),
45
- 'class': actor.get_class().get_name(),
46
- 'path': actor.get_path_name()
47
- })
48
- print(f"Found {len(actor_list)} actors")
42
+ try:
43
+ if actor:
44
+ actor_list.append({
45
+ 'name': actor.get_name(),
46
+ 'class': actor.get_class().get_name(),
47
+ 'path': actor.get_path_name()
48
+ })
49
+ except Exception:
50
+ pass
51
+ print('RESULT:' + json.dumps({'success': True, 'count': len(actor_list), 'actors': actor_list}))
49
52
  `.trim();
50
53
 
51
- const result = await this.bridge.executePython(pythonCode);
52
- this.setCache('listActors', result);
53
- return result;
54
+ const resp = await this.bridge.executePythonWithResult(pythonCode);
55
+ if (resp && typeof resp === 'object' && resp.success === true && Array.isArray((resp as any).actors)) {
56
+ this.setCache('listActors', resp);
57
+ return resp;
58
+ }
59
+
60
+ // Fallback manual extraction with bracket matching
61
+ const raw = await this.bridge.executePython(pythonCode);
62
+ let output = '';
63
+ if (raw?.LogOutput && Array.isArray(raw.LogOutput)) output = raw.LogOutput.map((l: any) => l.Output || '').join('');
64
+ else if (typeof raw === 'string') output = raw; else output = JSON.stringify(raw);
65
+ const marker = 'RESULT:';
66
+ const idx = output.lastIndexOf(marker);
67
+ if (idx !== -1) {
68
+ let i = idx + marker.length;
69
+ while (i < output.length && output[i] !== '{') i++;
70
+ if (i < output.length) {
71
+ let depth = 0, inStr = false, esc = false, j = i;
72
+ for (; j < output.length; j++) {
73
+ const ch = output[j];
74
+ if (esc) { esc = false; continue; }
75
+ if (ch === '\\') { esc = true; continue; }
76
+ if (ch === '"') { inStr = !inStr; continue; }
77
+ if (!inStr) {
78
+ if (ch === '{') depth++;
79
+ else if (ch === '}') { depth--; if (depth === 0) { j++; break; } }
80
+ }
81
+ }
82
+ const jsonStr = output.slice(i, j);
83
+ try {
84
+ const parsed = JSON.parse(jsonStr);
85
+ this.setCache('listActors', parsed);
86
+ return parsed;
87
+ } catch {}
88
+ }
89
+ }
90
+
91
+ return { success: false, error: 'Failed to parse actors list' };
54
92
  } catch (err) {
55
- return { error: `Failed to list actors: ${err}` };
93
+ return { success: false, error: `Failed to list actors: ${err}` };
56
94
  }
57
95
  }
58
96