unreal-engine-mcp-server 0.3.0 → 0.3.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.
package/src/index.ts CHANGED
@@ -75,6 +75,10 @@ const metrics: PerformanceMetrics = {
75
75
  recentErrors: []
76
76
  };
77
77
 
78
+ // Health check timer and last success tracking (stop pings after inactivity)
79
+ let healthCheckTimer: NodeJS.Timeout | undefined;
80
+ let lastHealthSuccessAt = 0;
81
+
78
82
  // Configuration
79
83
  const CONFIG = {
80
84
  // Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
@@ -84,7 +88,7 @@ const CONFIG = {
84
88
  RETRY_DELAY_MS: 2000,
85
89
  // Server info
86
90
  SERVER_NAME: 'unreal-engine-mcp',
87
- SERVER_VERSION: '0.3.0',
91
+ SERVER_VERSION: '0.3.1',
88
92
  // Monitoring
89
93
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
90
94
  };
@@ -111,11 +115,16 @@ function trackPerformance(startTime: number, success: boolean) {
111
115
 
112
116
  // Health check function
113
117
  async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
118
+ // If not connected, do not attempt any ping (stay quiet)
119
+ if (!bridge.isConnected) {
120
+ return false;
121
+ }
114
122
  try {
115
123
  // Use a safe echo command that doesn't affect any settings
116
124
  await bridge.executeConsoleCommand('echo MCP Server Health Check');
117
125
  metrics.connectionStatus = 'connected';
118
126
  metrics.lastHealthCheck = new Date();
127
+ lastHealthSuccessAt = Date.now();
119
128
  return true;
120
129
  } catch (err1) {
121
130
  // Fallback: minimal Python ping (if Python plugin is enabled)
@@ -123,11 +132,13 @@ async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
123
132
  await bridge.executePython("import sys; sys.stdout.write('OK')");
124
133
  metrics.connectionStatus = 'connected';
125
134
  metrics.lastHealthCheck = new Date();
135
+ lastHealthSuccessAt = Date.now();
126
136
  return true;
127
137
  } catch (err2) {
128
138
  metrics.connectionStatus = 'error';
129
139
  metrics.lastHealthCheck = new Date();
130
- log.warn('Health check failed (console and python):', err1, err2);
140
+ // Avoid noisy warnings when engine may be shutting down; log at debug
141
+ log.debug('Health check failed (console and python):', err1, err2);
131
142
  return false;
132
143
  }
133
144
  }
@@ -135,50 +146,68 @@ async function performHealthCheck(bridge: UnrealBridge): Promise<boolean> {
135
146
 
136
147
  export async function createServer() {
137
148
  const bridge = new UnrealBridge();
149
+ // Disable auto-reconnect loops; connect only on-demand
150
+ bridge.setAutoReconnectEnabled(false);
138
151
 
139
152
  // Initialize response validation with schemas
140
- log.info('Initializing response validation...');
153
+ log.debug('Initializing response validation...');
141
154
  const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
142
155
  toolDefs.forEach((tool: any) => {
143
156
  if (tool.outputSchema) {
144
157
  responseValidator.registerSchema(tool.name, tool.outputSchema);
145
158
  }
146
159
  });
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 () => {
160
+ // Summary at debug level to avoid repeated noisy blocks in some shells
161
+ log.debug(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
162
+
163
+ // Do NOT connect to Unreal at startup; connect on demand
164
+ log.debug('Server starting without connecting to Unreal Engine');
165
+ metrics.connectionStatus = 'disconnected';
166
+
167
+ // Health checks manager (only active when connected)
168
+ const startHealthChecks = () => {
169
+ if (healthCheckTimer) return;
170
+ lastHealthSuccessAt = Date.now();
171
+ healthCheckTimer = setInterval(async () => {
172
+ // Only attempt health pings while connected; stay silent otherwise
167
173
  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';
174
+ // Optionally pause fully after 5 minutes of no success
175
+ const FIVE_MIN_MS = 5 * 60 * 1000;
176
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
177
+ if (healthCheckTimer) {
178
+ clearInterval(healthCheckTimer);
179
+ healthCheckTimer = undefined;
180
+ }
181
+ log.info('Health checks paused after 5 minutes without a successful response');
173
182
  }
183
+ return;
174
184
  }
175
- }, 10000); // Try every 10 seconds
176
- }
177
-
178
- // Start periodic health checks
179
- setInterval(() => {
180
- performHealthCheck(bridge);
181
- }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
185
+
186
+ await performHealthCheck(bridge);
187
+ // Stop sending echoes if we haven't had a successful response in > 5 minutes
188
+ const FIVE_MIN_MS = 5 * 60 * 1000;
189
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
190
+ if (healthCheckTimer) {
191
+ clearInterval(healthCheckTimer);
192
+ healthCheckTimer = undefined;
193
+ log.info('Health checks paused after 5 minutes without a successful response');
194
+ }
195
+ }
196
+ }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
197
+ };
198
+
199
+ // On-demand connection helper
200
+ const ensureConnectedOnDemand = async (): Promise<boolean> => {
201
+ if (bridge.isConnected) return true;
202
+ const ok = await bridge.tryConnect(3, 5000, 1000);
203
+ if (ok) {
204
+ metrics.connectionStatus = 'connected';
205
+ startHealthChecks();
206
+ } else {
207
+ metrics.connectionStatus = 'disconnected';
208
+ }
209
+ return ok;
210
+ };
182
211
 
183
212
  // Resources
184
213
  const assetResources = new AssetResources(bridge);
@@ -272,6 +301,10 @@ export async function createServer() {
272
301
  const uri = request.params.uri;
273
302
 
274
303
  if (uri === 'ue://assets') {
304
+ const ok = await ensureConnectedOnDemand();
305
+ if (!ok) {
306
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
307
+ }
275
308
  const list = await assetResources.list('/Game', true);
276
309
  return {
277
310
  contents: [{
@@ -283,6 +316,10 @@ export async function createServer() {
283
316
  }
284
317
 
285
318
  if (uri === 'ue://actors') {
319
+ const ok = await ensureConnectedOnDemand();
320
+ if (!ok) {
321
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
322
+ }
286
323
  const list = await actorResources.listActors();
287
324
  return {
288
325
  contents: [{
@@ -294,6 +331,10 @@ export async function createServer() {
294
331
  }
295
332
 
296
333
  if (uri === 'ue://level') {
334
+ const ok = await ensureConnectedOnDemand();
335
+ if (!ok) {
336
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
337
+ }
297
338
  const level = await levelResources.getCurrentLevel();
298
339
  return {
299
340
  contents: [{
@@ -305,6 +346,10 @@ export async function createServer() {
305
346
  }
306
347
 
307
348
  if (uri === 'ue://exposed') {
349
+ const ok = await ensureConnectedOnDemand();
350
+ if (!ok) {
351
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
352
+ }
308
353
  try {
309
354
  const exposed = await bridge.getExposed();
310
355
  return {
@@ -327,11 +372,13 @@ export async function createServer() {
327
372
 
328
373
  if (uri === 'ue://health') {
329
374
  const uptimeMs = Date.now() - metrics.uptime;
330
- // Query engine version and feature flags (best-effort)
375
+ // Query engine version and feature flags only when connected
331
376
  let versionInfo: any = {};
332
377
  let featureFlags: any = {};
333
- try { versionInfo = await bridge.getEngineVersion(); } catch {}
334
- try { featureFlags = await bridge.getFeatureFlags(); } catch {}
378
+ if (bridge.isConnected) {
379
+ try { versionInfo = await bridge.getEngineVersion(); } catch {}
380
+ try { featureFlags = await bridge.getFeatureFlags(); } catch {}
381
+ }
335
382
  const health = {
336
383
  status: metrics.connectionStatus,
337
384
  uptime: Math.floor(uptimeMs / 1000),
@@ -369,6 +416,10 @@ export async function createServer() {
369
416
  }
370
417
 
371
418
  if (uri === 'ue://version') {
419
+ const ok = await ensureConnectedOnDemand();
420
+ if (!ok) {
421
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
422
+ }
372
423
  const info = await bridge.getEngineVersion();
373
424
  return {
374
425
  contents: [{
@@ -394,6 +445,20 @@ export async function createServer() {
394
445
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
395
446
  const { name, arguments: args } = request.params;
396
447
  const startTime = Date.now();
448
+
449
+ // Ensure connection only when needed, with 3 attempts
450
+ const connected = await ensureConnectedOnDemand();
451
+ if (!connected) {
452
+ const notConnected = {
453
+ content: [{ type: 'text', text: 'Unreal Engine is not connected (after 3 attempts). Please open UE and try again.' }],
454
+ success: false,
455
+ error: 'UE_NOT_CONNECTED',
456
+ retriable: false,
457
+ scope: `tool-call/${name}`
458
+ } as any;
459
+ trackPerformance(startTime, false);
460
+ return notConnected;
461
+ }
397
462
 
398
463
  // Create tools object for handler
399
464
  const tools = {
@@ -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
 
@@ -74,18 +74,28 @@ Examples:
74
74
  description: `Spawn, delete, and apply physics to actors in the level.
75
75
 
76
76
  When to use this tool:
77
- - You need to place an actor or mesh in the level, remove an actor, or nudge an actor with a physics force.
77
+ - Place an actor/mesh, remove an actor, or nudge an actor with a physics force.
78
+
79
+ Supported actions:
80
+ - spawn
81
+ - delete
82
+ - apply_force
78
83
 
79
84
  Spawning:
80
85
  - classPath can be a class name (e.g., StaticMeshActor, CameraActor) OR an asset path (e.g., /Engine/BasicShapes/Cube, /Game/Meshes/SM_Rock).
81
- - If an asset path is provided, a StaticMeshActor is auto-spawned with the mesh assigned.
86
+ - Asset paths auto-spawn StaticMeshActor with the mesh assigned.
82
87
 
83
88
  Deleting:
84
- - Finds actors by label/name (case-insensitive). Deletes matching actors.
89
+ - Finds actors by label/name (case-insensitive) and deletes matches.
85
90
 
86
91
  Apply force:
87
92
  - Applies a world-space force vector to an actor with physics enabled.
88
93
 
94
+ Tips:
95
+ - classPath accepts classes or asset paths; simple names like Cube auto-resolve to engine assets.
96
+ - location/rotation are optional; defaults are used if omitted.
97
+ - For delete/apply_force, provide actorName.
98
+
89
99
  Examples:
90
100
  - {"action":"spawn","classPath":"/Engine/BasicShapes/Cube","location":{"x":0,"y":0,"z":100}}
91
101
  - {"action":"delete","actorName":"Cube_1"}
@@ -154,9 +164,15 @@ Examples:
154
164
  When to use this tool:
155
165
  - Start/stop a PIE session, move the viewport camera, or change viewmode (Lit/Unlit/Wireframe/etc.).
156
166
 
167
+ Supported actions:
168
+ - play
169
+ - stop
170
+ - set_camera
171
+ - set_view_mode
172
+
157
173
  Notes:
158
- - View modes are validated and unsafe modes are blocked.
159
- - Camera accepts location and/or rotation (both optional). Values are normalized.
174
+ - View modes are validated; unsafe modes are blocked.
175
+ - Camera accepts location/rotation (optional); values normalized.
160
176
 
161
177
  Examples:
162
178
  - {"action":"play"}
@@ -225,6 +241,18 @@ Examples:
225
241
  When to use this tool:
226
242
  - Switch to a level, save the current level, stream sublevels, add a light, or start a lighting build.
227
243
 
244
+ Supported actions:
245
+ - load
246
+ - save
247
+ - stream
248
+ - create_light
249
+ - build_lighting
250
+
251
+ Tips:
252
+ - Use /Game paths for levels (e.g., /Game/Maps/Level).
253
+ - For streaming, set shouldBeLoaded and shouldBeVisible accordingly.
254
+ - For lights, provide lightType and optional location/intensity.
255
+
228
256
  Examples:
229
257
  - {"action":"load","levelPath":"/Game/Maps/Lobby"}
230
258
  - {"action":"stream","levelName":"Sublevel_A","shouldBeLoaded":true,"shouldBeVisible":true}
@@ -290,6 +318,16 @@ Examples:
290
318
  When to use this tool:
291
319
  - Generate an Anim Blueprint for a skeleton, play a Montage/Animation on an actor, or enable ragdoll.
292
320
 
321
+ Supported actions:
322
+ - create_animation_bp
323
+ - play_montage
324
+ - setup_ragdoll
325
+
326
+ Tips:
327
+ - Ensure the montage/animation is compatible with the target actor/skeleton.
328
+ - setup_ragdoll requires a valid physicsAssetName on the skeleton.
329
+ - Use savePath when creating new assets.
330
+
293
331
  Examples:
294
332
  - {"action":"create_animation_bp","name":"ABP_Hero","skeletonPath":"/Game/Characters/Hero/SK_Hero_Skeleton","savePath":"/Game/Characters/Hero"}
295
333
  - {"action":"play_montage","actorName":"Hero","montagePath":"/Game/Anim/MT_Attack"}
@@ -338,6 +376,15 @@ Examples:
338
376
  When to use this tool:
339
377
  - Spawn a Niagara system at a location, create a particle effect by type tag, or draw debug geometry for planning.
340
378
 
379
+ Supported actions:
380
+ - particle
381
+ - niagara
382
+ - debug_shape
383
+
384
+ Tips:
385
+ - Set color as RGBA [r,g,b,a]; scale defaults to 1 if omitted.
386
+ - Use debug shapes for quick layout planning and measurements.
387
+
341
388
  Examples:
342
389
  - {"action":"niagara","systemPath":"/Game/FX/NS_Explosion","location":{"x":0,"y":0,"z":200},"scale":1.0}
343
390
  - {"action":"particle","effectType":"Smoke","name":"SMK1","location":{"x":100,"y":0,"z":50}}
@@ -407,6 +454,14 @@ Examples:
407
454
  When to use this tool:
408
455
  - Quickly scaffold a Blueprint asset or add a component to an existing Blueprint.
409
456
 
457
+ Supported actions:
458
+ - create
459
+ - add_component
460
+
461
+ Tips:
462
+ - blueprintType can be Actor, Pawn, Character, etc.
463
+ - Component names should be unique within the Blueprint.
464
+
410
465
  Examples:
411
466
  - {"action":"create","name":"BP_Switch","blueprintType":"Actor","savePath":"/Game/Blueprints"}
412
467
  - {"action":"add_component","name":"BP_Switch","componentType":"PointLightComponent","componentName":"KeyLight"}`,
@@ -449,10 +504,19 @@ Examples:
449
504
  When to use this tool:
450
505
  - Create a procedural terrain alternative, add/paint foliage, or attempt a landscape workflow.
451
506
 
507
+ Supported actions:
508
+ - create_landscape
509
+ - sculpt
510
+ - add_foliage
511
+ - paint_foliage
512
+
452
513
  Important:
453
514
  - Native Landscape creation via Python is limited and may return a helpful error suggesting Landscape Mode in the editor.
454
515
  - Foliage helpers create FoliageType assets and support simple placement.
455
516
 
517
+ Tips:
518
+ - Adjust brushSize and strength to tune sculpting results.
519
+
456
520
  Examples:
457
521
  - {"action":"create_landscape","name":"Landscape_Basic","sizeX":1024,"sizeY":1024}
458
522
  - {"action":"add_foliage","name":"FT_Grass","meshPath":"/Game/Foliage/SM_Grass","density":300}
@@ -512,6 +576,21 @@ Examples:
512
576
  When to use this tool:
513
577
  - Toggle profiling and FPS stats, adjust quality (sg.*), play a sound, create/show a basic widget, take a screenshot, or launch/quit the editor.
514
578
 
579
+ Supported actions:
580
+ - profile
581
+ - show_fps
582
+ - set_quality
583
+ - play_sound
584
+ - create_widget
585
+ - show_widget
586
+ - screenshot
587
+ - engine_start
588
+ - engine_quit
589
+
590
+ Tips:
591
+ - Screenshot resolution format: 1920x1080.
592
+ - engine_start can read UE project path from env; provide editorExe/projectPath if needed.
593
+
515
594
  Examples:
516
595
  - {"action":"show_fps","enabled":true}
517
596
  - {"action":"set_quality","category":"Shadows","level":2}
@@ -595,6 +674,9 @@ Safety:
595
674
  - Dangerous commands are blocked (quit/exit, GPU crash triggers, unsafe visualizebuffer modes, etc.).
596
675
  - Unknown commands will return a warning instead of crashing.
597
676
 
677
+ Tips:
678
+ - Prefer dedicated tools (system_control, control_editor) when available for structured control.
679
+
598
680
  Examples:
599
681
  - {"command":"stat fps"}
600
682
  - {"command":"viewmode wireframe"}
@@ -627,6 +709,18 @@ Examples:
627
709
  When to use this tool:
628
710
  - Automate Remote Control (RC) preset authoring and interaction from the assistant.
629
711
 
712
+ Supported actions:
713
+ - create_preset
714
+ - expose_actor
715
+ - expose_property
716
+ - list_fields
717
+ - set_property
718
+ - get_property
719
+
720
+ Tips:
721
+ - value must be JSON-serializable.
722
+ - Use objectPath/presetPath with full asset/object paths.
723
+
630
724
  Examples:
631
725
  - {"action":"create_preset","name":"LivePreset","path":"/Game/RCPresets"}
632
726
  - {"action":"expose_actor","presetPath":"/Game/RCPresets/LivePreset","actorName":"CameraActor"}
@@ -672,6 +766,26 @@ Examples:
672
766
  When to use this tool:
673
767
  - Build quick cinematics: create/open a sequence, add a camera or actors, tweak properties, and play.
674
768
 
769
+ Supported actions:
770
+ - create
771
+ - open
772
+ - add_camera
773
+ - add_actor
774
+ - add_actors
775
+ - remove_actors
776
+ - get_bindings
777
+ - add_spawnable_from_class
778
+ - play
779
+ - pause
780
+ - stop
781
+ - set_properties
782
+ - get_properties
783
+ - set_playback_speed
784
+
785
+ Tips:
786
+ - Set spawnable=true to auto-create a camera actor.
787
+ - Use frameRate/lengthInFrames to define timing; use playbackStart/End to trim.
788
+
675
789
  Examples:
676
790
  - {"action":"create","name":"Intro","path":"/Game/Cinematics"}
677
791
  - {"action":"add_camera","spawnable":true}
@@ -735,6 +849,14 @@ Examples:
735
849
  When to use this tool:
736
850
  - Inspect an object by path (class default object or actor/component) and optionally modify properties.
737
851
 
852
+ Supported actions:
853
+ - inspect_object
854
+ - set_property
855
+
856
+ Tips:
857
+ - propertyName is case-sensitive; ensure it exists on the target object.
858
+ - For class default objects (CDOs), use the /Script/...Default__Class path.
859
+
738
860
  Examples:
739
861
  - {"action":"inspect_object","objectPath":"/Script/Engine.Default__Engine"}
740
862
  - {"action":"set_property","objectPath":"/Game/MyActor","propertyName":"CustomBool","value":true}`,
@@ -1,6 +1,9 @@
1
1
  // Consolidated tool handlers - maps 13 tools to all 36 operations
2
2
  import { handleToolCall } from './tool-handlers.js';
3
3
  import { cleanObject } from '../utils/safe-json.js';
4
+ import { Logger } from '../utils/logger.js';
5
+
6
+ const log = new Logger('ConsolidatedToolHandler');
4
7
 
5
8
  export async function handleConsolidatedToolCall(
6
9
  name: string,
@@ -8,7 +11,8 @@ export async function handleConsolidatedToolCall(
8
11
  tools: any
9
12
  ) {
10
13
  const startTime = Date.now();
11
- console.log(`[ConsolidatedToolHandler] Starting execution of ${name} at ${new Date().toISOString()}`);
14
+ // Use scoped logger (stderr) to avoid polluting stdout JSON
15
+ log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
12
16
 
13
17
  try {
14
18
  // Validate args is not null/undefined