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 +6 -1
- package/Dockerfile +11 -28
- package/dist/index.js +105 -37
- package/dist/resources/actors.js +71 -13
- package/dist/tools/consolidated-tool-definitions.js +127 -5
- package/dist/tools/consolidated-tool-handlers.js +4 -1
- package/dist/tools/tool-definitions.js +77 -19
- package/dist/unreal-bridge.d.ts +4 -1
- package/dist/unreal-bridge.js +211 -53
- package/dist/utils/http.js +4 -2
- package/dist/utils/response-validator.js +2 -1
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/index.ts +103 -38
- package/src/resources/actors.ts +51 -13
- package/src/tools/consolidated-tool-definitions.ts +127 -5
- package/src/tools/consolidated-tool-handlers.ts +5 -1
- package/src/tools/tool-definitions.ts +77 -19
- package/src/unreal-bridge.ts +163 -60
- package/src/utils/http.ts +7 -4
- package/src/utils/response-validator.ts +2 -1
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.
|
|
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:
|
|
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:
|
|
21
|
+
FROM cgr.dev/chainguard/node:latest
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
RUN apk add --no-cache dumb-init
|
|
23
|
+
ENV NODE_ENV=production
|
|
25
24
|
|
|
26
|
-
#
|
|
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
|
|
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
|
-
#
|
|
54
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
334
|
+
// Query engine version and feature flags only when connected
|
|
286
335
|
let versionInfo = {};
|
|
287
336
|
let featureFlags = {};
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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,
|
package/dist/resources/actors.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
84
|
+
- Asset paths auto-spawn StaticMeshActor with the mesh assigned.
|
|
80
85
|
|
|
81
86
|
Deleting:
|
|
82
|
-
- Finds actors by label/name (case-insensitive)
|
|
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
|
|
156
|
-
- Camera accepts location
|
|
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
|
-
|
|
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) {
|