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/.env.production CHANGED
@@ -10,11 +10,16 @@ UE_RC_WS_PORT=30020
10
10
  USE_CONSOLIDATED_TOOLS=true
11
11
 
12
12
  # Logging Level
13
+ # Available levels (from quietest to most verbose):
14
+ # - error : only errors
15
+ # - warn : warnings and errors
16
+ # - info : info, warnings, errors (default)
17
+ # - debug : full debug, very verbose
13
18
  LOG_LEVEL=info
14
19
 
15
20
  # Server Settings
16
21
  SERVER_NAME=unreal-engine-mcp
17
- SERVER_VERSION=0.3.0
22
+ SERVER_VERSION=0.3.1
18
23
 
19
24
  # Connection Settings
20
25
  MAX_RETRY_ATTEMPTS=3
package/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  # Multi-stage build for efficient image size
2
- FROM node:20-alpine AS builder
2
+ FROM node:22-alpine AS builder
3
3
 
4
4
  # Set working directory
5
5
  WORKDIR /app
@@ -18,37 +18,20 @@ COPY src ./src
18
18
  RUN npm run build
19
19
 
20
20
  # Production stage
21
- FROM node:20-alpine
21
+ FROM cgr.dev/chainguard/node:latest
22
22
 
23
- # Install dumb-init for proper signal handling
24
- RUN apk add --no-cache dumb-init
23
+ ENV NODE_ENV=production
25
24
 
26
- # Create non-root user
27
- RUN addgroup -g 1001 -S nodejs && \
28
- adduser -S nodejs -u 1001
29
-
30
- # Set working directory
25
+ # Chainguard node runs as nonroot by default (user: node)
31
26
  WORKDIR /app
32
27
 
33
- # Copy package files
34
- COPY package*.json ./
35
-
36
- # Install production dependencies only (skip prepare script)
37
- RUN npm ci --only=production --ignore-scripts && \
38
- npm cache clean --force
28
+ # Copy only package manifests and install production deps in a clean layer
29
+ COPY --chown=node:node package*.json ./
30
+ RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
39
31
 
40
32
  # Copy built application from builder stage
41
- COPY --from=builder /app/dist ./dist
42
-
43
- # Change ownership to nodejs user
44
- RUN chown -R nodejs:nodejs /app
45
-
46
- # Switch to non-root user
47
- USER nodejs
48
-
49
- # MCP servers use stdio, no port exposure needed
50
- # Use dumb-init to handle signals properly
51
- ENTRYPOINT ["dumb-init", "--"]
33
+ COPY --chown=node:node --from=builder /app/dist ./dist
52
34
 
53
- # Run the MCP server
54
- CMD ["node", "dist/cli.js"]
35
+ # No shell, no init needed; run as provided nonroot user
36
+ ENTRYPOINT ["/usr/bin/node"]
37
+ CMD ["dist/cli.js"]
package/dist/index.js CHANGED
@@ -51,6 +51,9 @@ const metrics = {
51
51
  uptime: Date.now(),
52
52
  recentErrors: []
53
53
  };
54
+ // Health check timer and last success tracking (stop pings after inactivity)
55
+ let healthCheckTimer;
56
+ let lastHealthSuccessAt = 0;
54
57
  // Configuration
55
58
  const CONFIG = {
56
59
  // Tool mode: true = consolidated (13 tools), false = individual (36+ tools)
@@ -60,7 +63,7 @@ const CONFIG = {
60
63
  RETRY_DELAY_MS: 2000,
61
64
  // Server info
62
65
  SERVER_NAME: 'unreal-engine-mcp',
63
- SERVER_VERSION: '0.3.0',
66
+ SERVER_VERSION: '0.3.1',
64
67
  // Monitoring
65
68
  HEALTH_CHECK_INTERVAL_MS: 30000 // 30 seconds
66
69
  };
@@ -84,11 +87,16 @@ function trackPerformance(startTime, success) {
84
87
  }
85
88
  // Health check function
86
89
  async function performHealthCheck(bridge) {
90
+ // If not connected, do not attempt any ping (stay quiet)
91
+ if (!bridge.isConnected) {
92
+ return false;
93
+ }
87
94
  try {
88
95
  // Use a safe echo command that doesn't affect any settings
89
96
  await bridge.executeConsoleCommand('echo MCP Server Health Check');
90
97
  metrics.connectionStatus = 'connected';
91
98
  metrics.lastHealthCheck = new Date();
99
+ lastHealthSuccessAt = Date.now();
92
100
  return true;
93
101
  }
94
102
  catch (err1) {
@@ -97,55 +105,80 @@ async function performHealthCheck(bridge) {
97
105
  await bridge.executePython("import sys; sys.stdout.write('OK')");
98
106
  metrics.connectionStatus = 'connected';
99
107
  metrics.lastHealthCheck = new Date();
108
+ lastHealthSuccessAt = Date.now();
100
109
  return true;
101
110
  }
102
111
  catch (err2) {
103
112
  metrics.connectionStatus = 'error';
104
113
  metrics.lastHealthCheck = new Date();
105
- log.warn('Health check failed (console and python):', err1, err2);
114
+ // Avoid noisy warnings when engine may be shutting down; log at debug
115
+ log.debug('Health check failed (console and python):', err1, err2);
106
116
  return false;
107
117
  }
108
118
  }
109
119
  }
110
120
  export async function createServer() {
111
121
  const bridge = new UnrealBridge();
122
+ // Disable auto-reconnect loops; connect only on-demand
123
+ bridge.setAutoReconnectEnabled(false);
112
124
  // Initialize response validation with schemas
113
- log.info('Initializing response validation...');
125
+ log.debug('Initializing response validation...');
114
126
  const toolDefs = CONFIG.USE_CONSOLIDATED_TOOLS ? consolidatedToolDefinitions : toolDefinitions;
115
127
  toolDefs.forEach((tool) => {
116
128
  if (tool.outputSchema) {
117
129
  responseValidator.registerSchema(tool.name, tool.outputSchema);
118
130
  }
119
131
  });
120
- log.info(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
121
- // Connect to UE5 Remote Control with retries and timeout
122
- const connected = await bridge.tryConnect(CONFIG.MAX_RETRY_ATTEMPTS, 5000, // 5 second timeout per attempt
123
- CONFIG.RETRY_DELAY_MS);
124
- if (connected) {
125
- metrics.connectionStatus = 'connected';
126
- log.info('Successfully connected to Unreal Engine');
127
- }
128
- else {
129
- log.warn('Could not connect to Unreal Engine after retries');
130
- log.info('Server will start anyway - connection will be retried periodically');
131
- log.info('Make sure Unreal Engine is running with Remote Control enabled');
132
- metrics.connectionStatus = 'disconnected';
133
- // Schedule automatic reconnection attempts
134
- setInterval(async () => {
132
+ // Summary at debug level to avoid repeated noisy blocks in some shells
133
+ log.debug(`Registered ${responseValidator.getStats().totalSchemas} output schemas for validation`);
134
+ // Do NOT connect to Unreal at startup; connect on demand
135
+ log.debug('Server starting without connecting to Unreal Engine');
136
+ metrics.connectionStatus = 'disconnected';
137
+ // Health checks manager (only active when connected)
138
+ const startHealthChecks = () => {
139
+ if (healthCheckTimer)
140
+ return;
141
+ lastHealthSuccessAt = Date.now();
142
+ healthCheckTimer = setInterval(async () => {
143
+ // Only attempt health pings while connected; stay silent otherwise
135
144
  if (!bridge.isConnected) {
136
- log.info('Attempting to reconnect to Unreal Engine...');
137
- const reconnected = await bridge.tryConnect(1, 5000, 0); // Single attempt
138
- if (reconnected) {
139
- log.info('Reconnected to Unreal Engine successfully');
140
- metrics.connectionStatus = 'connected';
145
+ // Optionally pause fully after 5 minutes of no success
146
+ const FIVE_MIN_MS = 5 * 60 * 1000;
147
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
148
+ if (healthCheckTimer) {
149
+ clearInterval(healthCheckTimer);
150
+ healthCheckTimer = undefined;
151
+ }
152
+ log.info('Health checks paused after 5 minutes without a successful response');
141
153
  }
154
+ return;
142
155
  }
143
- }, 10000); // Try every 10 seconds
144
- }
145
- // Start periodic health checks
146
- setInterval(() => {
147
- performHealthCheck(bridge);
148
- }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
156
+ await performHealthCheck(bridge);
157
+ // Stop sending echoes if we haven't had a successful response in > 5 minutes
158
+ const FIVE_MIN_MS = 5 * 60 * 1000;
159
+ if (!lastHealthSuccessAt || Date.now() - lastHealthSuccessAt > FIVE_MIN_MS) {
160
+ if (healthCheckTimer) {
161
+ clearInterval(healthCheckTimer);
162
+ healthCheckTimer = undefined;
163
+ log.info('Health checks paused after 5 minutes without a successful response');
164
+ }
165
+ }
166
+ }, CONFIG.HEALTH_CHECK_INTERVAL_MS);
167
+ };
168
+ // On-demand connection helper
169
+ const ensureConnectedOnDemand = async () => {
170
+ if (bridge.isConnected)
171
+ return true;
172
+ const ok = await bridge.tryConnect(3, 5000, 1000);
173
+ if (ok) {
174
+ metrics.connectionStatus = 'connected';
175
+ startHealthChecks();
176
+ }
177
+ else {
178
+ metrics.connectionStatus = 'disconnected';
179
+ }
180
+ return ok;
181
+ };
149
182
  // Resources
150
183
  const assetResources = new AssetResources(bridge);
151
184
  const actorResources = new ActorResources(bridge);
@@ -230,6 +263,10 @@ export async function createServer() {
230
263
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
231
264
  const uri = request.params.uri;
232
265
  if (uri === 'ue://assets') {
266
+ const ok = await ensureConnectedOnDemand();
267
+ if (!ok) {
268
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
269
+ }
233
270
  const list = await assetResources.list('/Game', true);
234
271
  return {
235
272
  contents: [{
@@ -240,6 +277,10 @@ export async function createServer() {
240
277
  };
241
278
  }
242
279
  if (uri === 'ue://actors') {
280
+ const ok = await ensureConnectedOnDemand();
281
+ if (!ok) {
282
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
283
+ }
243
284
  const list = await actorResources.listActors();
244
285
  return {
245
286
  contents: [{
@@ -250,6 +291,10 @@ export async function createServer() {
250
291
  };
251
292
  }
252
293
  if (uri === 'ue://level') {
294
+ const ok = await ensureConnectedOnDemand();
295
+ if (!ok) {
296
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
297
+ }
253
298
  const level = await levelResources.getCurrentLevel();
254
299
  return {
255
300
  contents: [{
@@ -260,6 +305,10 @@ export async function createServer() {
260
305
  };
261
306
  }
262
307
  if (uri === 'ue://exposed') {
308
+ const ok = await ensureConnectedOnDemand();
309
+ if (!ok) {
310
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
311
+ }
263
312
  try {
264
313
  const exposed = await bridge.getExposed();
265
314
  return {
@@ -282,17 +331,19 @@ export async function createServer() {
282
331
  }
283
332
  if (uri === 'ue://health') {
284
333
  const uptimeMs = Date.now() - metrics.uptime;
285
- // Query engine version and feature flags (best-effort)
334
+ // Query engine version and feature flags only when connected
286
335
  let versionInfo = {};
287
336
  let featureFlags = {};
288
- try {
289
- versionInfo = await bridge.getEngineVersion();
290
- }
291
- catch { }
292
- try {
293
- featureFlags = await bridge.getFeatureFlags();
337
+ if (bridge.isConnected) {
338
+ try {
339
+ versionInfo = await bridge.getEngineVersion();
340
+ }
341
+ catch { }
342
+ try {
343
+ featureFlags = await bridge.getFeatureFlags();
344
+ }
345
+ catch { }
294
346
  }
295
- catch { }
296
347
  const health = {
297
348
  status: metrics.connectionStatus,
298
349
  uptime: Math.floor(uptimeMs / 1000),
@@ -328,6 +379,10 @@ export async function createServer() {
328
379
  };
329
380
  }
330
381
  if (uri === 'ue://version') {
382
+ const ok = await ensureConnectedOnDemand();
383
+ if (!ok) {
384
+ return { contents: [{ uri, mimeType: 'text/plain', text: 'Unreal Engine not connected (after 3 attempts).' }] };
385
+ }
331
386
  const info = await bridge.getEngineVersion();
332
387
  return {
333
388
  contents: [{
@@ -350,6 +405,19 @@ export async function createServer() {
350
405
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
351
406
  const { name, arguments: args } = request.params;
352
407
  const startTime = Date.now();
408
+ // Ensure connection only when needed, with 3 attempts
409
+ const connected = await ensureConnectedOnDemand();
410
+ if (!connected) {
411
+ const notConnected = {
412
+ content: [{ type: 'text', text: 'Unreal Engine is not connected (after 3 attempts). Please open UE and try again.' }],
413
+ success: false,
414
+ error: 'UE_NOT_CONNECTED',
415
+ retriable: false,
416
+ scope: `tool-call/${name}`
417
+ };
418
+ trackPerformance(startTime, false);
419
+ return notConnected;
420
+ }
353
421
  // Create tools object for handler
354
422
  const tools = {
355
423
  actorTools,
@@ -25,25 +25,83 @@ export class ActorResources {
25
25
  // Use Python to get actors via EditorActorSubsystem
26
26
  try {
27
27
  const pythonCode = `
28
- import unreal
28
+ import unreal, json
29
29
  actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
30
- actors = actor_subsystem.get_all_level_actors()
30
+ actors = actor_subsystem.get_all_level_actors() if actor_subsystem else []
31
31
  actor_list = []
32
32
  for actor in actors:
33
- if actor:
34
- actor_list.append({
35
- 'name': actor.get_name(),
36
- 'class': actor.get_class().get_name(),
37
- 'path': actor.get_path_name()
38
- })
39
- print(f"Found {len(actor_list)} actors")
33
+ try:
34
+ if actor:
35
+ actor_list.append({
36
+ 'name': actor.get_name(),
37
+ 'class': actor.get_class().get_name(),
38
+ 'path': actor.get_path_name()
39
+ })
40
+ except Exception:
41
+ pass
42
+ print('RESULT:' + json.dumps({'success': True, 'count': len(actor_list), 'actors': actor_list}))
40
43
  `.trim();
41
- const result = await this.bridge.executePython(pythonCode);
42
- this.setCache('listActors', result);
43
- return result;
44
+ const resp = await this.bridge.executePythonWithResult(pythonCode);
45
+ if (resp && typeof resp === 'object' && resp.success === true && Array.isArray(resp.actors)) {
46
+ this.setCache('listActors', resp);
47
+ return resp;
48
+ }
49
+ // Fallback manual extraction with bracket matching
50
+ const raw = await this.bridge.executePython(pythonCode);
51
+ let output = '';
52
+ if (raw?.LogOutput && Array.isArray(raw.LogOutput))
53
+ output = raw.LogOutput.map((l) => l.Output || '').join('');
54
+ else if (typeof raw === 'string')
55
+ output = raw;
56
+ else
57
+ output = JSON.stringify(raw);
58
+ const marker = 'RESULT:';
59
+ const idx = output.lastIndexOf(marker);
60
+ if (idx !== -1) {
61
+ let i = idx + marker.length;
62
+ while (i < output.length && output[i] !== '{')
63
+ i++;
64
+ if (i < output.length) {
65
+ let depth = 0, inStr = false, esc = false, j = i;
66
+ for (; j < output.length; j++) {
67
+ const ch = output[j];
68
+ if (esc) {
69
+ esc = false;
70
+ continue;
71
+ }
72
+ if (ch === '\\') {
73
+ esc = true;
74
+ continue;
75
+ }
76
+ if (ch === '"') {
77
+ inStr = !inStr;
78
+ continue;
79
+ }
80
+ if (!inStr) {
81
+ if (ch === '{')
82
+ depth++;
83
+ else if (ch === '}') {
84
+ depth--;
85
+ if (depth === 0) {
86
+ j++;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ const jsonStr = output.slice(i, j);
93
+ try {
94
+ const parsed = JSON.parse(jsonStr);
95
+ this.setCache('listActors', parsed);
96
+ return parsed;
97
+ }
98
+ catch { }
99
+ }
100
+ }
101
+ return { success: false, error: 'Failed to parse actors list' };
44
102
  }
45
103
  catch (err) {
46
- return { error: `Failed to list actors: ${err}` };
104
+ return { success: false, error: `Failed to list actors: ${err}` };
47
105
  }
48
106
  }
49
107
  async getActorByName(actorName) {
@@ -72,18 +72,28 @@ Examples:
72
72
  description: `Spawn, delete, and apply physics to actors in the level.
73
73
 
74
74
  When to use this tool:
75
- - You need to place an actor or mesh in the level, remove an actor, or nudge an actor with a physics force.
75
+ - Place an actor/mesh, remove an actor, or nudge an actor with a physics force.
76
+
77
+ Supported actions:
78
+ - spawn
79
+ - delete
80
+ - apply_force
76
81
 
77
82
  Spawning:
78
83
  - classPath can be a class name (e.g., StaticMeshActor, CameraActor) OR an asset path (e.g., /Engine/BasicShapes/Cube, /Game/Meshes/SM_Rock).
79
- - If an asset path is provided, a StaticMeshActor is auto-spawned with the mesh assigned.
84
+ - Asset paths auto-spawn StaticMeshActor with the mesh assigned.
80
85
 
81
86
  Deleting:
82
- - Finds actors by label/name (case-insensitive). Deletes matching actors.
87
+ - Finds actors by label/name (case-insensitive) and deletes matches.
83
88
 
84
89
  Apply force:
85
90
  - Applies a world-space force vector to an actor with physics enabled.
86
91
 
92
+ Tips:
93
+ - classPath accepts classes or asset paths; simple names like Cube auto-resolve to engine assets.
94
+ - location/rotation are optional; defaults are used if omitted.
95
+ - For delete/apply_force, provide actorName.
96
+
87
97
  Examples:
88
98
  - {"action":"spawn","classPath":"/Engine/BasicShapes/Cube","location":{"x":0,"y":0,"z":100}}
89
99
  - {"action":"delete","actorName":"Cube_1"}
@@ -151,9 +161,15 @@ Examples:
151
161
  When to use this tool:
152
162
  - Start/stop a PIE session, move the viewport camera, or change viewmode (Lit/Unlit/Wireframe/etc.).
153
163
 
164
+ Supported actions:
165
+ - play
166
+ - stop
167
+ - set_camera
168
+ - set_view_mode
169
+
154
170
  Notes:
155
- - View modes are validated and unsafe modes are blocked.
156
- - Camera accepts location and/or rotation (both optional). Values are normalized.
171
+ - View modes are validated; unsafe modes are blocked.
172
+ - Camera accepts location/rotation (optional); values normalized.
157
173
 
158
174
  Examples:
159
175
  - {"action":"play"}
@@ -221,6 +237,18 @@ Examples:
221
237
  When to use this tool:
222
238
  - Switch to a level, save the current level, stream sublevels, add a light, or start a lighting build.
223
239
 
240
+ Supported actions:
241
+ - load
242
+ - save
243
+ - stream
244
+ - create_light
245
+ - build_lighting
246
+
247
+ Tips:
248
+ - Use /Game paths for levels (e.g., /Game/Maps/Level).
249
+ - For streaming, set shouldBeLoaded and shouldBeVisible accordingly.
250
+ - For lights, provide lightType and optional location/intensity.
251
+
224
252
  Examples:
225
253
  - {"action":"load","levelPath":"/Game/Maps/Lobby"}
226
254
  - {"action":"stream","levelName":"Sublevel_A","shouldBeLoaded":true,"shouldBeVisible":true}
@@ -285,6 +313,16 @@ Examples:
285
313
  When to use this tool:
286
314
  - Generate an Anim Blueprint for a skeleton, play a Montage/Animation on an actor, or enable ragdoll.
287
315
 
316
+ Supported actions:
317
+ - create_animation_bp
318
+ - play_montage
319
+ - setup_ragdoll
320
+
321
+ Tips:
322
+ - Ensure the montage/animation is compatible with the target actor/skeleton.
323
+ - setup_ragdoll requires a valid physicsAssetName on the skeleton.
324
+ - Use savePath when creating new assets.
325
+
288
326
  Examples:
289
327
  - {"action":"create_animation_bp","name":"ABP_Hero","skeletonPath":"/Game/Characters/Hero/SK_Hero_Skeleton","savePath":"/Game/Characters/Hero"}
290
328
  - {"action":"play_montage","actorName":"Hero","montagePath":"/Game/Anim/MT_Attack"}
@@ -332,6 +370,15 @@ Examples:
332
370
  When to use this tool:
333
371
  - Spawn a Niagara system at a location, create a particle effect by type tag, or draw debug geometry for planning.
334
372
 
373
+ Supported actions:
374
+ - particle
375
+ - niagara
376
+ - debug_shape
377
+
378
+ Tips:
379
+ - Set color as RGBA [r,g,b,a]; scale defaults to 1 if omitted.
380
+ - Use debug shapes for quick layout planning and measurements.
381
+
335
382
  Examples:
336
383
  - {"action":"niagara","systemPath":"/Game/FX/NS_Explosion","location":{"x":0,"y":0,"z":200},"scale":1.0}
337
384
  - {"action":"particle","effectType":"Smoke","name":"SMK1","location":{"x":100,"y":0,"z":50}}
@@ -400,6 +447,14 @@ Examples:
400
447
  When to use this tool:
401
448
  - Quickly scaffold a Blueprint asset or add a component to an existing Blueprint.
402
449
 
450
+ Supported actions:
451
+ - create
452
+ - add_component
453
+
454
+ Tips:
455
+ - blueprintType can be Actor, Pawn, Character, etc.
456
+ - Component names should be unique within the Blueprint.
457
+
403
458
  Examples:
404
459
  - {"action":"create","name":"BP_Switch","blueprintType":"Actor","savePath":"/Game/Blueprints"}
405
460
  - {"action":"add_component","name":"BP_Switch","componentType":"PointLightComponent","componentName":"KeyLight"}`,
@@ -441,10 +496,19 @@ Examples:
441
496
  When to use this tool:
442
497
  - Create a procedural terrain alternative, add/paint foliage, or attempt a landscape workflow.
443
498
 
499
+ Supported actions:
500
+ - create_landscape
501
+ - sculpt
502
+ - add_foliage
503
+ - paint_foliage
504
+
444
505
  Important:
445
506
  - Native Landscape creation via Python is limited and may return a helpful error suggesting Landscape Mode in the editor.
446
507
  - Foliage helpers create FoliageType assets and support simple placement.
447
508
 
509
+ Tips:
510
+ - Adjust brushSize and strength to tune sculpting results.
511
+
448
512
  Examples:
449
513
  - {"action":"create_landscape","name":"Landscape_Basic","sizeX":1024,"sizeY":1024}
450
514
  - {"action":"add_foliage","name":"FT_Grass","meshPath":"/Game/Foliage/SM_Grass","density":300}
@@ -503,6 +567,21 @@ Examples:
503
567
  When to use this tool:
504
568
  - Toggle profiling and FPS stats, adjust quality (sg.*), play a sound, create/show a basic widget, take a screenshot, or launch/quit the editor.
505
569
 
570
+ Supported actions:
571
+ - profile
572
+ - show_fps
573
+ - set_quality
574
+ - play_sound
575
+ - create_widget
576
+ - show_widget
577
+ - screenshot
578
+ - engine_start
579
+ - engine_quit
580
+
581
+ Tips:
582
+ - Screenshot resolution format: 1920x1080.
583
+ - engine_start can read UE project path from env; provide editorExe/projectPath if needed.
584
+
506
585
  Examples:
507
586
  - {"action":"show_fps","enabled":true}
508
587
  - {"action":"set_quality","category":"Shadows","level":2}
@@ -585,6 +664,9 @@ Safety:
585
664
  - Dangerous commands are blocked (quit/exit, GPU crash triggers, unsafe visualizebuffer modes, etc.).
586
665
  - Unknown commands will return a warning instead of crashing.
587
666
 
667
+ Tips:
668
+ - Prefer dedicated tools (system_control, control_editor) when available for structured control.
669
+
588
670
  Examples:
589
671
  - {"command":"stat fps"}
590
672
  - {"command":"viewmode wireframe"}
@@ -616,6 +698,18 @@ Examples:
616
698
  When to use this tool:
617
699
  - Automate Remote Control (RC) preset authoring and interaction from the assistant.
618
700
 
701
+ Supported actions:
702
+ - create_preset
703
+ - expose_actor
704
+ - expose_property
705
+ - list_fields
706
+ - set_property
707
+ - get_property
708
+
709
+ Tips:
710
+ - value must be JSON-serializable.
711
+ - Use objectPath/presetPath with full asset/object paths.
712
+
619
713
  Examples:
620
714
  - {"action":"create_preset","name":"LivePreset","path":"/Game/RCPresets"}
621
715
  - {"action":"expose_actor","presetPath":"/Game/RCPresets/LivePreset","actorName":"CameraActor"}
@@ -660,6 +754,26 @@ Examples:
660
754
  When to use this tool:
661
755
  - Build quick cinematics: create/open a sequence, add a camera or actors, tweak properties, and play.
662
756
 
757
+ Supported actions:
758
+ - create
759
+ - open
760
+ - add_camera
761
+ - add_actor
762
+ - add_actors
763
+ - remove_actors
764
+ - get_bindings
765
+ - add_spawnable_from_class
766
+ - play
767
+ - pause
768
+ - stop
769
+ - set_properties
770
+ - get_properties
771
+ - set_playback_speed
772
+
773
+ Tips:
774
+ - Set spawnable=true to auto-create a camera actor.
775
+ - Use frameRate/lengthInFrames to define timing; use playbackStart/End to trim.
776
+
663
777
  Examples:
664
778
  - {"action":"create","name":"Intro","path":"/Game/Cinematics"}
665
779
  - {"action":"add_camera","spawnable":true}
@@ -722,6 +836,14 @@ Examples:
722
836
  When to use this tool:
723
837
  - Inspect an object by path (class default object or actor/component) and optionally modify properties.
724
838
 
839
+ Supported actions:
840
+ - inspect_object
841
+ - set_property
842
+
843
+ Tips:
844
+ - propertyName is case-sensitive; ensure it exists on the target object.
845
+ - For class default objects (CDOs), use the /Script/...Default__Class path.
846
+
725
847
  Examples:
726
848
  - {"action":"inspect_object","objectPath":"/Script/Engine.Default__Engine"}
727
849
  - {"action":"set_property","objectPath":"/Game/MyActor","propertyName":"CustomBool","value":true}`,
@@ -1,9 +1,12 @@
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
+ const log = new Logger('ConsolidatedToolHandler');
4
6
  export async function handleConsolidatedToolCall(name, args, tools) {
5
7
  const startTime = Date.now();
6
- console.log(`[ConsolidatedToolHandler] Starting execution of ${name} at ${new Date().toISOString()}`);
8
+ // Use scoped logger (stderr) to avoid polluting stdout JSON
9
+ log.debug(`Starting execution of ${name} at ${new Date().toISOString()}`);
7
10
  try {
8
11
  // Validate args is not null/undefined
9
12
  if (args === null || args === undefined) {