unreal-engine-mcp-server 0.5.2 → 0.5.4
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/CHANGELOG.md +195 -0
- package/README.md +9 -6
- package/dist/automation/bridge.d.ts +1 -0
- package/dist/automation/bridge.js +62 -4
- package/dist/automation/types.d.ts +1 -0
- package/dist/config/class-aliases.d.ts +5 -0
- package/dist/config/class-aliases.js +30 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/graphql/server.d.ts +0 -1
- package/dist/graphql/server.js +15 -16
- package/dist/index.js +1 -1
- package/dist/services/metrics-server.d.ts +2 -1
- package/dist/services/metrics-server.js +29 -4
- package/dist/tools/consolidated-tool-definitions.js +3 -3
- package/dist/tools/debug.d.ts +5 -0
- package/dist/tools/debug.js +7 -0
- package/dist/tools/handlers/actor-handlers.js +4 -27
- package/dist/tools/handlers/asset-handlers.js +13 -1
- package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
- package/dist/tools/handlers/common-handlers.d.ts +11 -11
- package/dist/tools/handlers/common-handlers.js +6 -4
- package/dist/tools/handlers/editor-handlers.d.ts +2 -1
- package/dist/tools/handlers/editor-handlers.js +6 -6
- package/dist/tools/handlers/effect-handlers.js +3 -0
- package/dist/tools/handlers/graph-handlers.d.ts +2 -1
- package/dist/tools/handlers/graph-handlers.js +1 -1
- package/dist/tools/handlers/input-handlers.d.ts +5 -1
- package/dist/tools/handlers/level-handlers.d.ts +2 -1
- package/dist/tools/handlers/level-handlers.js +3 -3
- package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
- package/dist/tools/handlers/lighting-handlers.js +3 -0
- package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
- package/dist/tools/handlers/pipeline-handlers.js +64 -10
- package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
- package/dist/tools/handlers/system-handlers.d.ts +1 -1
- package/dist/tools/input.d.ts +5 -1
- package/dist/tools/input.js +37 -1
- package/dist/tools/lighting.d.ts +1 -0
- package/dist/tools/lighting.js +7 -0
- package/dist/tools/physics.d.ts +1 -1
- package/dist/tools/sequence.d.ts +1 -0
- package/dist/tools/sequence.js +7 -0
- package/dist/types/handler-types.d.ts +343 -0
- package/dist/types/handler-types.js +2 -0
- package/dist/unreal-bridge.d.ts +1 -1
- package/dist/unreal-bridge.js +8 -6
- package/dist/utils/command-validator.d.ts +1 -0
- package/dist/utils/command-validator.js +11 -1
- package/dist/utils/error-handler.js +3 -1
- package/dist/utils/response-validator.js +2 -2
- package/dist/utils/safe-json.d.ts +1 -1
- package/dist/utils/safe-json.js +3 -6
- package/dist/utils/unreal-command-queue.js +1 -1
- package/dist/utils/validation.js +6 -2
- package/docs/handler-mapping.md +6 -1
- package/package.json +2 -2
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
- package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
- package/server.json +2 -2
- package/src/automation/bridge.ts +80 -10
- package/src/automation/types.ts +1 -0
- package/src/config/class-aliases.ts +65 -0
- package/src/constants.ts +10 -0
- package/src/graphql/server.ts +23 -23
- package/src/index.ts +1 -1
- package/src/services/metrics-server.ts +40 -6
- package/src/tools/consolidated-tool-definitions.ts +3 -3
- package/src/tools/debug.ts +8 -0
- package/src/tools/handlers/actor-handlers.ts +5 -31
- package/src/tools/handlers/asset-handlers.ts +19 -1
- package/src/tools/handlers/blueprint-handlers.ts +1 -1
- package/src/tools/handlers/common-handlers.ts +32 -11
- package/src/tools/handlers/editor-handlers.ts +8 -7
- package/src/tools/handlers/effect-handlers.ts +4 -0
- package/src/tools/handlers/graph-handlers.ts +7 -6
- package/src/tools/handlers/level-handlers.ts +5 -4
- package/src/tools/handlers/lighting-handlers.ts +5 -1
- package/src/tools/handlers/pipeline-handlers.ts +83 -16
- package/src/tools/input.ts +60 -1
- package/src/tools/lighting.ts +11 -0
- package/src/tools/physics.ts +1 -1
- package/src/tools/sequence.ts +11 -0
- package/src/types/handler-types.ts +442 -0
- package/src/unreal-bridge.ts +8 -6
- package/src/utils/command-validator.ts +23 -1
- package/src/utils/error-handler.ts +4 -1
- package/src/utils/response-validator.ts +7 -9
- package/src/utils/safe-json.ts +20 -15
- package/src/utils/unreal-command-queue.ts +3 -1
- package/src/utils/validation.test.ts +3 -3
- package/src/utils/validation.ts +36 -26
- package/tests/test-console-command.mjs +1 -1
- package/tests/test-runner.mjs +63 -3
- package/tests/run-unreal-tool-tests.mjs +0 -948
- package/tests/test-asset-errors.mjs +0 -35
|
@@ -3,7 +3,6 @@ import { HealthMonitor } from './health-monitor.js';
|
|
|
3
3
|
import { AutomationBridge } from '../automation/index.js';
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
5
|
import { wasmIntegration } from '../wasm/index.js';
|
|
6
|
-
import { DEFAULT_AUTOMATION_HOST } from '../constants.js';
|
|
7
6
|
|
|
8
7
|
interface MetricsServerOptions {
|
|
9
8
|
healthMonitor: HealthMonitor;
|
|
@@ -85,7 +84,7 @@ function formatPrometheusMetrics(options: MetricsServerOptions): string {
|
|
|
85
84
|
return lines.join('\n') + '\n';
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
export function startMetricsServer(options: MetricsServerOptions):
|
|
87
|
+
export function startMetricsServer(options: MetricsServerOptions): http.Server | null {
|
|
89
88
|
const { logger } = options;
|
|
90
89
|
|
|
91
90
|
const portEnv = process.env.MCP_METRICS_PORT || process.env.PROMETHEUS_PORT;
|
|
@@ -93,7 +92,31 @@ export function startMetricsServer(options: MetricsServerOptions): void {
|
|
|
93
92
|
|
|
94
93
|
if (!port || !Number.isFinite(port) || port <= 0) {
|
|
95
94
|
logger.debug('Metrics server disabled (set MCP_METRICS_PORT to enable Prometheus /metrics endpoint).');
|
|
96
|
-
return;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const host = process.env.MCP_METRICS_HOST || '127.0.0.1';
|
|
99
|
+
|
|
100
|
+
// Simple rate limiting: max 60 requests per minute per IP
|
|
101
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
102
|
+
const RATE_LIMIT_MAX_REQUESTS = 60;
|
|
103
|
+
const requestCounts = new Map<string, { count: number; resetAt: number }>();
|
|
104
|
+
|
|
105
|
+
function checkRateLimit(ip: string): boolean {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const record = requestCounts.get(ip);
|
|
108
|
+
|
|
109
|
+
if (!record || now >= record.resetAt) {
|
|
110
|
+
requestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS });
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (record.count >= RATE_LIMIT_MAX_REQUESTS) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
record.count++;
|
|
119
|
+
return true;
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
try {
|
|
@@ -117,26 +140,37 @@ export function startMetricsServer(options: MetricsServerOptions): void {
|
|
|
117
140
|
return;
|
|
118
141
|
}
|
|
119
142
|
|
|
143
|
+
const clientIp = req.socket.remoteAddress || 'unknown';
|
|
144
|
+
if (!checkRateLimit(clientIp)) {
|
|
145
|
+
res.statusCode = 429;
|
|
146
|
+
res.setHeader('Retry-After', '60');
|
|
147
|
+
res.end('Too Many Requests');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
try {
|
|
121
152
|
const body = formatPrometheusMetrics(options);
|
|
122
153
|
res.statusCode = 200;
|
|
123
154
|
res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
124
155
|
res.end(body);
|
|
125
156
|
} catch (err) {
|
|
126
|
-
logger.warn('Failed to render /metrics payload', err as
|
|
157
|
+
logger.warn('Failed to render /metrics payload', err as Error);
|
|
127
158
|
res.statusCode = 500;
|
|
128
159
|
res.end('Internal Server Error');
|
|
129
160
|
}
|
|
130
161
|
});
|
|
131
162
|
|
|
132
|
-
server.listen(port, () => {
|
|
133
|
-
logger.info(`Prometheus metrics server listening on http://${
|
|
163
|
+
server.listen(port, host, () => {
|
|
164
|
+
logger.info(`Prometheus metrics server listening on http://${host}:${port}/metrics`);
|
|
134
165
|
});
|
|
135
166
|
|
|
136
167
|
server.on('error', (err) => {
|
|
137
168
|
logger.warn('Metrics server error', err as any);
|
|
138
169
|
});
|
|
170
|
+
|
|
171
|
+
return server;
|
|
139
172
|
} catch (err) {
|
|
140
173
|
logger.warn('Failed to start metrics server', err as any);
|
|
174
|
+
return null;
|
|
141
175
|
}
|
|
142
176
|
}
|
|
@@ -487,7 +487,7 @@ Supported actions:
|
|
|
487
487
|
'create_volumetric_fog', 'create_particle_trail', 'create_environment_effect', 'create_impact_effect', 'create_niagara_ribbon',
|
|
488
488
|
'activate', 'activate_effect', 'deactivate', 'reset', 'advance_simulation',
|
|
489
489
|
'add_niagara_module', 'connect_niagara_pins', 'remove_niagara_node', 'set_niagara_parameter',
|
|
490
|
-
'clear_debug_shapes', 'cleanup'
|
|
490
|
+
'clear_debug_shapes', 'cleanup', 'list_debug_shapes'
|
|
491
491
|
],
|
|
492
492
|
description: 'Action'
|
|
493
493
|
},
|
|
@@ -737,7 +737,7 @@ Supported actions:
|
|
|
737
737
|
'get_properties', 'set_properties', 'duplicate', 'rename', 'delete', 'list', 'get_metadata', 'set_metadata',
|
|
738
738
|
'add_spawnable_from_class', 'add_track', 'add_section', 'set_display_rate', 'set_tick_resolution',
|
|
739
739
|
'set_work_range', 'set_view_range', 'set_track_muted', 'set_track_solo', 'set_track_locked',
|
|
740
|
-
'list_tracks', 'remove_track'
|
|
740
|
+
'list_tracks', 'remove_track', 'list_track_types'
|
|
741
741
|
],
|
|
742
742
|
description: 'Action'
|
|
743
743
|
},
|
|
@@ -1100,7 +1100,7 @@ Supported actions:
|
|
|
1100
1100
|
'spawn_light', 'create_light', 'spawn_sky_light', 'create_sky_light', 'ensure_single_sky_light',
|
|
1101
1101
|
'create_lightmass_volume', 'create_lighting_enabled_level', 'create_dynamic_light',
|
|
1102
1102
|
'setup_global_illumination', 'configure_shadows', 'set_exposure', 'set_ambient_occlusion', 'setup_volumetric_fog',
|
|
1103
|
-
'build_lighting'
|
|
1103
|
+
'build_lighting', 'list_light_types'
|
|
1104
1104
|
],
|
|
1105
1105
|
description: 'Action'
|
|
1106
1106
|
},
|
package/src/tools/debug.ts
CHANGED
|
@@ -611,4 +611,12 @@ export class DebugVisualizationTools {
|
|
|
611
611
|
return { success: false, error: `Failed to clear debug shapes: ${err}` };
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
|
+
|
|
615
|
+
async listDebugShapes() {
|
|
616
|
+
if (this.automationBridge) {
|
|
617
|
+
const response = await this.automationBridge.sendAutomationRequest('list_debug_shapes', {});
|
|
618
|
+
return response;
|
|
619
|
+
}
|
|
620
|
+
return { success: false, error: 'AUTOMATION_BRIDGE_REQUIRED', message: 'Listing debug shapes requires Automation Bridge' };
|
|
621
|
+
}
|
|
614
622
|
}
|
|
@@ -2,33 +2,14 @@ import { ITools } from '../../types/tool-interfaces.js';
|
|
|
2
2
|
import { executeAutomationRequest } from './common-handlers.js';
|
|
3
3
|
import { normalizeArgs } from './argument-helper.js';
|
|
4
4
|
import { ResponseFactory } from '../../utils/response-factory.js';
|
|
5
|
+
import { ACTOR_CLASS_ALIASES, getRequiredComponent } from '../../config/class-aliases.js';
|
|
5
6
|
|
|
6
7
|
type ActorActionHandler = (args: any, tools: ITools) => Promise<any>;
|
|
7
8
|
|
|
8
9
|
const handlers: Record<string, ActorActionHandler> = {
|
|
9
10
|
spawn: async (args, tools) => {
|
|
10
|
-
// Class name aliases for user-friendly names
|
|
11
|
-
const classAliases: Record<string, string> = {
|
|
12
|
-
'SplineActor': '/Script/Engine.Actor', // Use Actor with SplineComponent
|
|
13
|
-
'Spline': '/Script/Engine.Actor', // Use Actor with SplineComponent
|
|
14
|
-
'PointLight': '/Script/Engine.PointLight',
|
|
15
|
-
'SpotLight': '/Script/Engine.SpotLight',
|
|
16
|
-
'DirectionalLight': '/Script/Engine.DirectionalLight',
|
|
17
|
-
'Camera': '/Script/Engine.CameraActor',
|
|
18
|
-
'CameraActor': '/Script/Engine.CameraActor',
|
|
19
|
-
'StaticMeshActor': '/Script/Engine.StaticMeshActor',
|
|
20
|
-
'SkeletalMeshActor': '/Script/Engine.SkeletalMeshActor',
|
|
21
|
-
'PlayerStart': '/Script/Engine.PlayerStart',
|
|
22
|
-
'TriggerBox': '/Script/Engine.TriggerBox',
|
|
23
|
-
'TriggerSphere': '/Script/Engine.TriggerSphere',
|
|
24
|
-
'BlockingVolume': '/Script/Engine.BlockingVolume',
|
|
25
|
-
'Pawn': '/Script/Engine.Pawn',
|
|
26
|
-
'Character': '/Script/Engine.Character',
|
|
27
|
-
'Actor': '/Script/Engine.Actor'
|
|
28
|
-
};
|
|
29
|
-
|
|
30
11
|
const params = normalizeArgs(args, [
|
|
31
|
-
{ key: 'classPath', aliases: ['class', 'type', 'actorClass'], required: true, map:
|
|
12
|
+
{ key: 'classPath', aliases: ['class', 'type', 'actorClass'], required: true, map: ACTOR_CLASS_ALIASES },
|
|
32
13
|
{ key: 'actorName', aliases: ['name'] },
|
|
33
14
|
{ key: 'timeoutMs', default: undefined }
|
|
34
15
|
]);
|
|
@@ -39,20 +20,13 @@ const handlers: Record<string, ActorActionHandler> = {
|
|
|
39
20
|
// failure so tests can exercise timeout handling deterministically
|
|
40
21
|
// without relying on editor performance.
|
|
41
22
|
if (typeof timeoutMs === 'number' && timeoutMs > 0 && timeoutMs < 200) {
|
|
42
|
-
|
|
43
|
-
success: false,
|
|
44
|
-
error: `Timeout too small for spawn operation: ${timeoutMs}ms`,
|
|
45
|
-
message: 'Timeout too small for spawn operation'
|
|
46
|
-
};
|
|
23
|
+
throw new Error(`Timeout too small for spawn operation: ${timeoutMs}ms`);
|
|
47
24
|
}
|
|
48
25
|
|
|
49
26
|
// For SplineActor alias, add SplineComponent automatically
|
|
50
|
-
// Check original args for raw input
|
|
51
|
-
// Map transforms 'SplineActor' -> '/Script/Engine.Actor', so we check original args.
|
|
27
|
+
// Check original args for raw input since map transforms the alias
|
|
52
28
|
const originalClass = args.classPath || args.class || args.type || args.actorClass;
|
|
53
|
-
const componentToAdd = (originalClass
|
|
54
|
-
? 'SplineComponent'
|
|
55
|
-
: undefined;
|
|
29
|
+
const componentToAdd = getRequiredComponent(originalClass);
|
|
56
30
|
|
|
57
31
|
const result = await tools.actorTools.spawn({
|
|
58
32
|
classPath: params.classPath,
|
|
@@ -188,9 +188,27 @@ export async function handleAssetTools(action: string, args: any, tools: ITools)
|
|
|
188
188
|
throw new Error('No paths provided for delete action');
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// Normalize paths: strip object sub-path suffix (e.g., /Game/Folder/Asset.Asset -> /Game/Folder/Asset)
|
|
192
|
+
// This handles the common pattern where full object paths are provided instead of package paths
|
|
193
|
+
const normalizedPaths = paths.map(p => {
|
|
194
|
+
let normalized = p.replace(/\\/g, '/').trim();
|
|
195
|
+
// If the path contains a dot after the last slash, it's likely an object path (e.g., /Game/Folder/Asset.Asset)
|
|
196
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
197
|
+
if (lastSlash >= 0) {
|
|
198
|
+
const afterSlash = normalized.substring(lastSlash + 1);
|
|
199
|
+
const dotIndex = afterSlash.indexOf('.');
|
|
200
|
+
if (dotIndex > 0) {
|
|
201
|
+
// Strip the .ObjectName suffix
|
|
202
|
+
normalized = normalized.substring(0, lastSlash + 1 + dotIndex);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return normalized;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const res = await tools.assetTools.deleteAssets({ paths: normalizedPaths });
|
|
192
209
|
return ResponseFactory.success(res, 'Assets deleted successfully');
|
|
193
210
|
}
|
|
211
|
+
|
|
194
212
|
case 'generate_lods': {
|
|
195
213
|
const params = normalizeArgs(args, [
|
|
196
214
|
{ key: 'assetPath', required: true },
|
|
@@ -368,7 +368,7 @@ export async function handleBlueprintTools(action: string, args: any, tools: ITo
|
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
export async function handleBlueprintGet(args: any, tools: ITools) {
|
|
371
|
-
const res = await executeAutomationRequest(tools, 'blueprint_get', args, 'Automation bridge not available for blueprint operations');
|
|
371
|
+
const res = await executeAutomationRequest(tools, 'blueprint_get', args, 'Automation bridge not available for blueprint operations') as { success?: boolean; message?: string } | null;
|
|
372
372
|
if (res && res.success) {
|
|
373
373
|
return cleanObject({
|
|
374
374
|
...res,
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { ITools } from '../../types/tool-interfaces.js';
|
|
2
|
+
import type { HandlerArgs, Vector3, Rotator } from '../../types/handler-types.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Validates that args is not null/undefined.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureArgsPresent(args: unknown): asserts args is Record<string, unknown> {
|
|
4
8
|
if (args === null || args === undefined) {
|
|
5
9
|
throw new Error('Invalid arguments: null or undefined');
|
|
6
10
|
}
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Extracts and validates the 'action' field from args.
|
|
15
|
+
*/
|
|
16
|
+
export function requireAction(args: HandlerArgs): string {
|
|
10
17
|
ensureArgsPresent(args);
|
|
11
18
|
const action = args.action;
|
|
12
19
|
if (typeof action !== 'string' || action.trim() === '') {
|
|
@@ -15,20 +22,26 @@ export function requireAction(args: any): string {
|
|
|
15
22
|
return action;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Validates that a value is a non-empty string.
|
|
27
|
+
*/
|
|
28
|
+
export function requireNonEmptyString(value: unknown, field: string, message?: string): string {
|
|
19
29
|
if (typeof value !== 'string' || value.trim() === '') {
|
|
20
30
|
throw new Error(message ?? `Invalid ${field}: must be a non-empty string`);
|
|
21
31
|
}
|
|
22
32
|
return value;
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Execute a request via the automation bridge.
|
|
37
|
+
*/
|
|
25
38
|
export async function executeAutomationRequest(
|
|
26
39
|
tools: ITools,
|
|
27
40
|
toolName: string,
|
|
28
|
-
args:
|
|
41
|
+
args: HandlerArgs,
|
|
29
42
|
errorMessage: string = 'Automation bridge not available',
|
|
30
43
|
options: { timeoutMs?: number } = {}
|
|
31
|
-
) {
|
|
44
|
+
): Promise<unknown> {
|
|
32
45
|
const automationBridge = tools.automationBridge;
|
|
33
46
|
// If the bridge is missing or not a function, we can't proceed with automation requests
|
|
34
47
|
if (!automationBridge || typeof automationBridge.sendAutomationRequest !== 'function') {
|
|
@@ -42,11 +55,14 @@ export async function executeAutomationRequest(
|
|
|
42
55
|
return await automationBridge.sendAutomationRequest(toolName, args, options);
|
|
43
56
|
}
|
|
44
57
|
|
|
58
|
+
/** Input type for location normalization */
|
|
59
|
+
type LocationInput = Vector3 | [number, number, number] | number[] | null | undefined;
|
|
60
|
+
|
|
45
61
|
/**
|
|
46
62
|
* Normalize location to [x, y, z] array format
|
|
47
63
|
* Accepts both {x,y,z} object and [x,y,z] array formats
|
|
48
64
|
*/
|
|
49
|
-
export function normalizeLocation(location:
|
|
65
|
+
export function normalizeLocation(location: LocationInput): [number, number, number] | undefined {
|
|
50
66
|
if (!location) return undefined;
|
|
51
67
|
|
|
52
68
|
// Already array format
|
|
@@ -56,17 +72,21 @@ export function normalizeLocation(location: any): [number, number, number] | und
|
|
|
56
72
|
|
|
57
73
|
// Object format {x, y, z}
|
|
58
74
|
if (typeof location === 'object' && ('x' in location || 'y' in location || 'z' in location)) {
|
|
59
|
-
|
|
75
|
+
const loc = location as Vector3;
|
|
76
|
+
return [Number(loc.x) || 0, Number(loc.y) || 0, Number(loc.z) || 0];
|
|
60
77
|
}
|
|
61
78
|
|
|
62
79
|
return undefined;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
/** Input type for rotation normalization */
|
|
83
|
+
type RotationInput = Rotator | [number, number, number] | number[] | null | undefined;
|
|
84
|
+
|
|
65
85
|
/**
|
|
66
86
|
* Normalize rotation to {pitch, yaw, roll} object format
|
|
67
87
|
* Accepts both {pitch,yaw,roll} object and [pitch,yaw,roll] array formats
|
|
68
88
|
*/
|
|
69
|
-
export function normalizeRotation(rotation:
|
|
89
|
+
export function normalizeRotation(rotation: RotationInput): Rotator | undefined {
|
|
70
90
|
if (!rotation) return undefined;
|
|
71
91
|
|
|
72
92
|
// Array format [pitch, yaw, roll]
|
|
@@ -76,10 +96,11 @@ export function normalizeRotation(rotation: any): { pitch: number; yaw: number;
|
|
|
76
96
|
|
|
77
97
|
// Already object format
|
|
78
98
|
if (typeof rotation === 'object') {
|
|
99
|
+
const rot = rotation as Rotator;
|
|
79
100
|
return {
|
|
80
|
-
pitch: Number(
|
|
81
|
-
yaw: Number(
|
|
82
|
-
roll: Number(
|
|
101
|
+
pitch: Number(rot.pitch) || 0,
|
|
102
|
+
yaw: Number(rot.yaw) || 0,
|
|
103
|
+
roll: Number(rot.roll) || 0
|
|
83
104
|
};
|
|
84
105
|
}
|
|
85
106
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cleanObject } from '../../utils/safe-json.js';
|
|
2
2
|
import { ITools } from '../../types/tool-interfaces.js';
|
|
3
|
+
import type { EditorArgs } from '../../types/handler-types.js';
|
|
3
4
|
import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
|
|
4
5
|
|
|
5
|
-
export async function handleEditorTools(action: string, args:
|
|
6
|
+
export async function handleEditorTools(action: string, args: EditorArgs, tools: ITools) {
|
|
6
7
|
switch (action) {
|
|
7
8
|
case 'play': {
|
|
8
9
|
const res = await tools.editorTools.playInEditor(args.timeoutMs);
|
|
@@ -16,14 +17,14 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
|
|
|
16
17
|
case 'eject': {
|
|
17
18
|
const inPie = await tools.editorTools.isInPIE();
|
|
18
19
|
if (!inPie) {
|
|
19
|
-
|
|
20
|
+
throw new Error('Cannot eject while not in PIE');
|
|
20
21
|
}
|
|
21
22
|
return await executeAutomationRequest(tools, 'control_editor', { action: 'eject' });
|
|
22
23
|
}
|
|
23
24
|
case 'possess': {
|
|
24
25
|
const inPie = await tools.editorTools.isInPIE();
|
|
25
26
|
if (!inPie) {
|
|
26
|
-
|
|
27
|
+
throw new Error('Cannot possess actor while not in PIE');
|
|
27
28
|
}
|
|
28
29
|
return await executeAutomationRequest(tools, 'control_editor', args);
|
|
29
30
|
}
|
|
@@ -40,7 +41,7 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
|
|
|
40
41
|
return cleanObject(res);
|
|
41
42
|
}
|
|
42
43
|
case 'console_command': {
|
|
43
|
-
const res = await tools.editorTools.executeConsoleCommand(args.command);
|
|
44
|
+
const res = await tools.editorTools.executeConsoleCommand(args.command ?? '');
|
|
44
45
|
return cleanObject(res);
|
|
45
46
|
}
|
|
46
47
|
case 'set_camera': {
|
|
@@ -63,17 +64,17 @@ export async function handleEditorTools(action: string, args: any, tools: ITools
|
|
|
63
64
|
return { success: true, message: 'Stepped frame', action: 'step_frame' };
|
|
64
65
|
}
|
|
65
66
|
case 'create_bookmark': {
|
|
66
|
-
const idx = parseInt(args.bookmarkName) || 0;
|
|
67
|
+
const idx = parseInt(args.bookmarkName ?? '0') || 0;
|
|
67
68
|
await tools.editorTools.executeConsoleCommand(`r.SetBookmark ${idx}`);
|
|
68
69
|
return { success: true, message: `Created bookmark ${idx}`, action: 'create_bookmark' };
|
|
69
70
|
}
|
|
70
71
|
case 'jump_to_bookmark': {
|
|
71
|
-
const idx = parseInt(args.bookmarkName) || 0;
|
|
72
|
+
const idx = parseInt(args.bookmarkName ?? '0') || 0;
|
|
72
73
|
await tools.editorTools.executeConsoleCommand(`r.JumpToBookmark ${idx}`);
|
|
73
74
|
return { success: true, message: `Jumped to bookmark ${idx}`, action: 'jump_to_bookmark' };
|
|
74
75
|
}
|
|
75
76
|
case 'set_preferences': {
|
|
76
|
-
const res = await tools.editorTools.setEditorPreferences(args.category, args.preferences);
|
|
77
|
+
const res = await tools.editorTools.setEditorPreferences(args.category ?? '', args.preferences ?? {});
|
|
77
78
|
return cleanObject(res);
|
|
78
79
|
}
|
|
79
80
|
case 'open_asset': {
|
|
@@ -100,6 +100,10 @@ export async function handleEffectTools(action: string, args: any, tools: ITools
|
|
|
100
100
|
if (action === 'clear_debug_shapes') {
|
|
101
101
|
return executeAutomationRequest(tools, action, args);
|
|
102
102
|
}
|
|
103
|
+
// Discovery action: list available debug shape types
|
|
104
|
+
if (action === 'list_debug_shapes') {
|
|
105
|
+
return executeAutomationRequest(tools, 'list_debug_shapes', args);
|
|
106
|
+
}
|
|
103
107
|
if (action === 'cleanup') {
|
|
104
108
|
args.action = 'cleanup';
|
|
105
109
|
args.subAction = 'cleanup';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cleanObject } from '../../utils/safe-json.js';
|
|
2
2
|
import { ITools } from '../../types/tool-interfaces.js';
|
|
3
|
+
import type { GraphArgs } from '../../types/handler-types.js';
|
|
3
4
|
import { executeAutomationRequest } from './common-handlers.js';
|
|
4
5
|
|
|
5
|
-
export async function handleGraphTools(toolName: string, action: string, args:
|
|
6
|
+
export async function handleGraphTools(toolName: string, action: string, args: GraphArgs, tools: ITools) {
|
|
6
7
|
// Common validation
|
|
7
8
|
if (!args.assetPath && !args.blueprintPath && !args.systemPath) {
|
|
8
9
|
// Some actions might not need a path if they operate on "currently open" asset,
|
|
@@ -20,11 +21,11 @@ export async function handleGraphTools(toolName: string, action: string, args: a
|
|
|
20
21
|
case 'manage_behavior_tree':
|
|
21
22
|
return handleBehaviorTree(action, args, tools);
|
|
22
23
|
default:
|
|
23
|
-
|
|
24
|
+
throw new Error(`Unknown graph tool: ${toolName}`);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
async function handleBlueprintGraph(action: string, args:
|
|
28
|
+
async function handleBlueprintGraph(action: string, args: GraphArgs, tools: ITools) {
|
|
28
29
|
const processedArgs = { ...args, subAction: action };
|
|
29
30
|
|
|
30
31
|
// Default graphName
|
|
@@ -95,7 +96,7 @@ async function handleBlueprintGraph(action: string, args: any, tools: ITools) {
|
|
|
95
96
|
return cleanObject({ ...res, ...(res.result || {}) });
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
async function handleNiagaraGraph(action: string, args:
|
|
99
|
+
async function handleNiagaraGraph(action: string, args: GraphArgs, tools: ITools) {
|
|
99
100
|
const payload = { ...args, subAction: action };
|
|
100
101
|
// Map systemPath to assetPath if missing
|
|
101
102
|
if (payload.systemPath && !payload.assetPath) {
|
|
@@ -105,12 +106,12 @@ async function handleNiagaraGraph(action: string, args: any, tools: ITools) {
|
|
|
105
106
|
return cleanObject({ ...res, ...(res.result || {}) });
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
async function handleMaterialGraph(action: string, args:
|
|
109
|
+
async function handleMaterialGraph(action: string, args: GraphArgs, tools: ITools) {
|
|
109
110
|
const res: any = await executeAutomationRequest(tools, 'manage_material_graph', { ...args, subAction: action }, 'Automation bridge not available');
|
|
110
111
|
return cleanObject({ ...res, ...(res.result || {}) });
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
async function handleBehaviorTree(action: string, args:
|
|
114
|
+
async function handleBehaviorTree(action: string, args: GraphArgs, tools: ITools) {
|
|
114
115
|
const res: any = await executeAutomationRequest(tools, 'manage_behavior_tree', { ...args, subAction: action }, 'Automation bridge not available');
|
|
115
116
|
return cleanObject({ ...res, ...(res.result || {}) });
|
|
116
117
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cleanObject } from '../../utils/safe-json.js';
|
|
2
2
|
import { ITools } from '../../types/tool-interfaces.js';
|
|
3
|
+
import type { LevelArgs } from '../../types/handler-types.js';
|
|
3
4
|
import { executeAutomationRequest, requireNonEmptyString } from './common-handlers.js';
|
|
4
5
|
|
|
5
|
-
export async function handleLevelTools(action: string, args:
|
|
6
|
+
export async function handleLevelTools(action: string, args: LevelArgs, tools: ITools) {
|
|
6
7
|
switch (action) {
|
|
7
8
|
case 'load':
|
|
8
9
|
case 'load_level': {
|
|
@@ -118,14 +119,14 @@ export async function handleLevelTools(action: string, args: any, tools: ITools)
|
|
|
118
119
|
case 'export_level': {
|
|
119
120
|
const res = await tools.levelTools.exportLevel({
|
|
120
121
|
levelPath: args.levelPath,
|
|
121
|
-
exportPath: args.exportPath
|
|
122
|
+
exportPath: args.exportPath ?? args.destinationPath ?? '',
|
|
122
123
|
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined
|
|
123
124
|
});
|
|
124
125
|
return cleanObject(res);
|
|
125
126
|
}
|
|
126
127
|
case 'import_level': {
|
|
127
128
|
const res = await tools.levelTools.importLevel({
|
|
128
|
-
packagePath: args.packagePath
|
|
129
|
+
packagePath: args.packagePath ?? args.sourcePath ?? '',
|
|
129
130
|
destinationPath: args.destinationPath,
|
|
130
131
|
timeoutMs: typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined
|
|
131
132
|
});
|
|
@@ -140,7 +141,7 @@ export async function handleLevelTools(action: string, args: any, tools: ITools)
|
|
|
140
141
|
return cleanObject(res);
|
|
141
142
|
}
|
|
142
143
|
case 'delete': {
|
|
143
|
-
const levelPaths = Array.isArray(args.levelPaths) ? args.levelPaths : [args.levelPath];
|
|
144
|
+
const levelPaths = Array.isArray(args.levelPaths) ? args.levelPaths.filter((p): p is string => typeof p === 'string') : (args.levelPath ? [args.levelPath] : []);
|
|
144
145
|
const res = await tools.levelTools.deleteLevels({ levelPaths });
|
|
145
146
|
return cleanObject(res);
|
|
146
147
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cleanObject } from '../../utils/safe-json.js';
|
|
2
2
|
import { ITools } from '../../types/tool-interfaces.js';
|
|
3
|
+
import type { LightingArgs } from '../../types/handler-types.js';
|
|
3
4
|
import { normalizeLocation } from './common-handlers.js';
|
|
4
5
|
|
|
5
|
-
export async function handleLightingTools(action: string, args:
|
|
6
|
+
export async function handleLightingTools(action: string, args: LightingArgs, tools: ITools) {
|
|
6
7
|
// Normalize location parameter to accept both {x,y,z} and [x,y,z] formats
|
|
7
8
|
const normalizedLocation = normalizeLocation(args.location);
|
|
8
9
|
|
|
@@ -141,6 +142,9 @@ export async function handleLightingTools(action: string, args: any, tools: IToo
|
|
|
141
142
|
useTemplate: args.useTemplate
|
|
142
143
|
}));
|
|
143
144
|
}
|
|
145
|
+
case 'list_light_types': {
|
|
146
|
+
return cleanObject(await tools.lightingTools.listLightTypes());
|
|
147
|
+
}
|
|
144
148
|
default:
|
|
145
149
|
throw new Error(`Unknown lighting action: ${action}`);
|
|
146
150
|
}
|