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
package/src/utils/validation.ts
CHANGED
|
@@ -9,6 +9,11 @@ import { toRotTuple, toVec3Tuple } from './normalize.js';
|
|
|
9
9
|
*/
|
|
10
10
|
const MAX_PATH_LENGTH = 260;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Maximum asset name length
|
|
14
|
+
*/
|
|
15
|
+
const MAX_ASSET_NAME_LENGTH = 64;
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Invalid characters for Unreal Engine asset names
|
|
14
19
|
* Note: Dashes are allowed in Unreal asset names
|
|
@@ -34,39 +39,39 @@ export function sanitizeAssetName(name: string): string {
|
|
|
34
39
|
if (!name || typeof name !== 'string') {
|
|
35
40
|
return 'Asset';
|
|
36
41
|
}
|
|
37
|
-
|
|
42
|
+
|
|
38
43
|
// Remove leading/trailing whitespace
|
|
39
44
|
let sanitized = name.trim();
|
|
40
|
-
|
|
45
|
+
|
|
41
46
|
// Replace invalid characters with underscores
|
|
42
47
|
sanitized = sanitized.replace(INVALID_CHARS, '_');
|
|
43
|
-
|
|
48
|
+
|
|
44
49
|
// Remove consecutive underscores
|
|
45
50
|
sanitized = sanitized.replace(/_+/g, '_');
|
|
46
|
-
|
|
51
|
+
|
|
47
52
|
// Remove leading/trailing underscores
|
|
48
53
|
sanitized = sanitized.replace(/^_+|_+$/g, '');
|
|
49
|
-
|
|
54
|
+
|
|
50
55
|
// If name is empty after sanitization, use default
|
|
51
56
|
if (!sanitized) {
|
|
52
57
|
return 'Asset';
|
|
53
58
|
}
|
|
54
|
-
|
|
59
|
+
|
|
55
60
|
// If name is a reserved keyword, append underscore
|
|
56
61
|
if (RESERVED_KEYWORDS.includes(sanitized)) {
|
|
57
62
|
sanitized = `${sanitized}_Asset`;
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
|
|
60
65
|
// Ensure name starts with a letter
|
|
61
66
|
if (!/^[A-Za-z]/.test(sanitized)) {
|
|
62
67
|
sanitized = `Asset_${sanitized}`;
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
// Truncate overly long names to reduce risk of hitting path length limits
|
|
66
|
-
if (sanitized.length >
|
|
67
|
-
sanitized = sanitized.slice(0,
|
|
71
|
+
if (sanitized.length > MAX_ASSET_NAME_LENGTH) {
|
|
72
|
+
sanitized = sanitized.slice(0, MAX_ASSET_NAME_LENGTH);
|
|
68
73
|
}
|
|
69
|
-
|
|
74
|
+
|
|
70
75
|
return sanitized;
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -79,7 +84,7 @@ export function sanitizePath(path: string): string {
|
|
|
79
84
|
if (!path || typeof path !== 'string') {
|
|
80
85
|
return '/Game';
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
|
|
83
88
|
// Normalize slashes
|
|
84
89
|
path = path.replace(/\\/g, '/');
|
|
85
90
|
|
|
@@ -87,10 +92,15 @@ export function sanitizePath(path: string): string {
|
|
|
87
92
|
if (!path.startsWith('/')) {
|
|
88
93
|
path = `/${path}`;
|
|
89
94
|
}
|
|
90
|
-
|
|
95
|
+
|
|
91
96
|
// Split path into segments and sanitize each
|
|
92
97
|
let segments = path.split('/').filter(s => s.length > 0);
|
|
93
98
|
|
|
99
|
+
// Block path traversal attempts
|
|
100
|
+
if (segments.some(s => s === '..' || s === '.')) {
|
|
101
|
+
throw new Error('Path traversal (..) is not allowed');
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
if (segments.length === 0) {
|
|
95
105
|
return '/Game';
|
|
96
106
|
}
|
|
@@ -108,10 +118,10 @@ export function sanitizePath(path: string): string {
|
|
|
108
118
|
}
|
|
109
119
|
return sanitizeAssetName(segment);
|
|
110
120
|
});
|
|
111
|
-
|
|
121
|
+
|
|
112
122
|
// Reconstruct path
|
|
113
123
|
const sanitizedPath = '/' + sanitizedSegments.join('/');
|
|
114
|
-
|
|
124
|
+
|
|
115
125
|
return sanitizedPath;
|
|
116
126
|
}
|
|
117
127
|
|
|
@@ -146,20 +156,20 @@ export function validateAssetParams(params: {
|
|
|
146
156
|
} {
|
|
147
157
|
// Sanitize name
|
|
148
158
|
const sanitizedName = sanitizeAssetName(params.name);
|
|
149
|
-
|
|
159
|
+
|
|
150
160
|
// Sanitize path if provided
|
|
151
|
-
const sanitizedPath = params.savePath
|
|
161
|
+
const sanitizedPath = params.savePath
|
|
152
162
|
? sanitizePath(params.savePath)
|
|
153
163
|
: params.savePath;
|
|
154
|
-
|
|
164
|
+
|
|
155
165
|
// Construct full path for validation
|
|
156
166
|
const fullPath = sanitizedPath
|
|
157
167
|
? `${sanitizedPath}/${sanitizedName}`
|
|
158
168
|
: `/Game/${sanitizedName}`;
|
|
159
|
-
|
|
169
|
+
|
|
160
170
|
// Validate path length
|
|
161
171
|
const pathValidation = validatePathLength(fullPath);
|
|
162
|
-
|
|
172
|
+
|
|
163
173
|
if (!pathValidation.valid) {
|
|
164
174
|
return {
|
|
165
175
|
valid: false,
|
|
@@ -167,7 +177,7 @@ export function validateAssetParams(params: {
|
|
|
167
177
|
error: pathValidation.error
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
|
-
|
|
180
|
+
|
|
171
181
|
return {
|
|
172
182
|
valid: true,
|
|
173
183
|
sanitized: {
|
|
@@ -187,7 +197,7 @@ export function resolveSkeletalMeshPath(input: string): string | null {
|
|
|
187
197
|
if (!input || typeof input !== 'string') {
|
|
188
198
|
return null;
|
|
189
199
|
}
|
|
190
|
-
|
|
200
|
+
|
|
191
201
|
// Common skeleton to mesh mappings
|
|
192
202
|
const skeletonToMeshMap: { [key: string]: string } = {
|
|
193
203
|
'/Game/Mannequin/Character/Mesh/UE4_Mannequin_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
|
|
@@ -199,12 +209,12 @@ export function resolveSkeletalMeshPath(input: string): string | null {
|
|
|
199
209
|
'/Game/Characters/Mannequins/Skeletons/UE5_Manny_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Manny_Simple',
|
|
200
210
|
'/Game/Characters/Mannequins/Skeletons/UE5_Quinn_Skeleton': '/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple'
|
|
201
211
|
};
|
|
202
|
-
|
|
212
|
+
|
|
203
213
|
// Check if this is a known skeleton path
|
|
204
214
|
if (skeletonToMeshMap[input]) {
|
|
205
215
|
return skeletonToMeshMap[input];
|
|
206
216
|
}
|
|
207
|
-
|
|
217
|
+
|
|
208
218
|
// If it contains _Skeleton, try to convert to mesh name
|
|
209
219
|
if (input.includes('_Skeleton')) {
|
|
210
220
|
// Try common replacements
|
|
@@ -224,12 +234,12 @@ export function resolveSkeletalMeshPath(input: string): string | null {
|
|
|
224
234
|
);
|
|
225
235
|
return meshPath;
|
|
226
236
|
}
|
|
227
|
-
|
|
237
|
+
|
|
228
238
|
// If it starts with SK_ (skeleton prefix), try SKM_ (skeletal mesh prefix)
|
|
229
239
|
if (input.includes('/SK_')) {
|
|
230
240
|
return input.replace('/SK_', '/SKM_');
|
|
231
241
|
}
|
|
232
|
-
|
|
242
|
+
|
|
233
243
|
// Return as-is if no conversion needed
|
|
234
244
|
return input;
|
|
235
245
|
}
|
|
@@ -43,7 +43,7 @@ const testCases = [
|
|
|
43
43
|
scenario: "Edge: Very long safe command",
|
|
44
44
|
toolName: "system_control",
|
|
45
45
|
arguments: { action: "console_command", command: "stat fps; stat gpu; stat memory" },
|
|
46
|
-
expected: "
|
|
46
|
+
expected: "blocked|command_blocked|blocked for safety"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
scenario: "Warning: Unknown command",
|
package/tests/test-runner.mjs
CHANGED
|
@@ -20,6 +20,66 @@ let serverArgs = process.env.UNREAL_MCP_SERVER_ARGS ? process.env.UNREAL_MCP_SER
|
|
|
20
20
|
const serverCwd = process.env.UNREAL_MCP_SERVER_CWD ?? repoRoot;
|
|
21
21
|
const serverEnv = Object.assign({}, process.env);
|
|
22
22
|
|
|
23
|
+
const DEFAULT_RESPONSE_LOG_MAX_CHARS = 6000; // default max chars
|
|
24
|
+
const RESPONSE_LOGGING_ENABLED = process.env.UNREAL_MCP_TEST_LOG_RESPONSES !== '0';
|
|
25
|
+
|
|
26
|
+
function clampString(value, maxChars) {
|
|
27
|
+
if (typeof value !== 'string') return '';
|
|
28
|
+
if (value.length <= maxChars) return value;
|
|
29
|
+
return value.slice(0, maxChars) + `\n... (truncated, ${value.length - maxChars} chars omitted)`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function tryParseJson(text) {
|
|
33
|
+
if (typeof text !== 'string') return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeMcpResponse(response) {
|
|
42
|
+
const normalized = {
|
|
43
|
+
isError: Boolean(response?.isError),
|
|
44
|
+
structuredContent: response?.structuredContent ?? null,
|
|
45
|
+
contentText: '',
|
|
46
|
+
content: response?.content ?? undefined
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (normalized.structuredContent === null && Array.isArray(response?.content)) {
|
|
50
|
+
for (const entry of response.content) {
|
|
51
|
+
if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
|
|
52
|
+
const parsed = tryParseJson(entry.text);
|
|
53
|
+
if (parsed !== null) {
|
|
54
|
+
normalized.structuredContent = parsed;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(response?.content) && response.content.length > 0) {
|
|
61
|
+
normalized.contentText = response.content
|
|
62
|
+
.map((entry) => (entry && typeof entry.text === 'string' ? entry.text : ''))
|
|
63
|
+
.filter((text) => text.length > 0)
|
|
64
|
+
.join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function logMcpResponse(toolName, normalizedResponse) {
|
|
71
|
+
const maxChars = Number(process.env.UNREAL_MCP_TEST_RESPONSE_MAX_CHARS ?? DEFAULT_RESPONSE_LOG_MAX_CHARS);
|
|
72
|
+
const payload = {
|
|
73
|
+
isError: normalizedResponse.isError,
|
|
74
|
+
structuredContent: normalizedResponse.structuredContent,
|
|
75
|
+
contentText: normalizedResponse.contentText,
|
|
76
|
+
content: normalizedResponse.content
|
|
77
|
+
};
|
|
78
|
+
const json = JSON.stringify(payload, null, 2);
|
|
79
|
+
console.log(`[MCP RESPONSE] ${toolName}:`);
|
|
80
|
+
console.log(clampString(json, Number.isFinite(maxChars) && maxChars > 0 ? maxChars : DEFAULT_RESPONSE_LOG_MAX_CHARS));
|
|
81
|
+
}
|
|
82
|
+
|
|
23
83
|
function formatResultLine(testCase, status, detail, durationMs) {
|
|
24
84
|
const durationText = typeof durationMs === 'number' ? ` (${durationMs.toFixed(1)} ms)` : '';
|
|
25
85
|
return `[${status.toUpperCase()}] ${testCase.scenario}${durationText}${detail ? ` => ${detail}` : ''}`;
|
|
@@ -515,13 +575,13 @@ export async function runToolTests(toolName, testCases) {
|
|
|
515
575
|
}
|
|
516
576
|
}
|
|
517
577
|
const normalizedResponse = { ...response, structuredContent };
|
|
578
|
+
if (RESPONSE_LOGGING_ENABLED) {
|
|
579
|
+
logMcpResponse(testCase.toolName + " :: " + testCase.scenario, normalizeMcpResponse(normalizedResponse));
|
|
580
|
+
}
|
|
518
581
|
const { passed, reason } = evaluateExpectation(testCase, normalizedResponse);
|
|
519
582
|
|
|
520
583
|
if (!passed) {
|
|
521
584
|
console.log(`[FAILED] ${testCase.scenario} (${durationMs.toFixed(1)} ms) => ${reason}`);
|
|
522
|
-
if (normalizedResponse) {
|
|
523
|
-
console.log(`[DEBUG] Full response for ${testCase.scenario}:`, JSON.stringify(normalizedResponse, null, 2));
|
|
524
|
-
}
|
|
525
585
|
results.push({
|
|
526
586
|
scenario: testCase.scenario,
|
|
527
587
|
toolName: testCase.toolName,
|