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/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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
375
|
+
// Query engine version and feature flags only when connected
|
|
331
376
|
let versionInfo: any = {};
|
|
332
377
|
let featureFlags: any = {};
|
|
333
|
-
|
|
334
|
-
|
|
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 = {
|
package/src/resources/actors.ts
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
86
|
+
- Asset paths auto-spawn StaticMeshActor with the mesh assigned.
|
|
82
87
|
|
|
83
88
|
Deleting:
|
|
84
|
-
- Finds actors by label/name (case-insensitive)
|
|
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
|
|
159
|
-
- Camera accepts location
|
|
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
|
-
|
|
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
|