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.
Files changed (98) hide show
  1. package/CHANGELOG.md +195 -0
  2. package/README.md +9 -6
  3. package/dist/automation/bridge.d.ts +1 -0
  4. package/dist/automation/bridge.js +62 -4
  5. package/dist/automation/types.d.ts +1 -0
  6. package/dist/config/class-aliases.d.ts +5 -0
  7. package/dist/config/class-aliases.js +30 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/graphql/server.d.ts +0 -1
  11. package/dist/graphql/server.js +15 -16
  12. package/dist/index.js +1 -1
  13. package/dist/services/metrics-server.d.ts +2 -1
  14. package/dist/services/metrics-server.js +29 -4
  15. package/dist/tools/consolidated-tool-definitions.js +3 -3
  16. package/dist/tools/debug.d.ts +5 -0
  17. package/dist/tools/debug.js +7 -0
  18. package/dist/tools/handlers/actor-handlers.js +4 -27
  19. package/dist/tools/handlers/asset-handlers.js +13 -1
  20. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -1
  21. package/dist/tools/handlers/common-handlers.d.ts +11 -11
  22. package/dist/tools/handlers/common-handlers.js +6 -4
  23. package/dist/tools/handlers/editor-handlers.d.ts +2 -1
  24. package/dist/tools/handlers/editor-handlers.js +6 -6
  25. package/dist/tools/handlers/effect-handlers.js +3 -0
  26. package/dist/tools/handlers/graph-handlers.d.ts +2 -1
  27. package/dist/tools/handlers/graph-handlers.js +1 -1
  28. package/dist/tools/handlers/input-handlers.d.ts +5 -1
  29. package/dist/tools/handlers/level-handlers.d.ts +2 -1
  30. package/dist/tools/handlers/level-handlers.js +3 -3
  31. package/dist/tools/handlers/lighting-handlers.d.ts +2 -1
  32. package/dist/tools/handlers/lighting-handlers.js +3 -0
  33. package/dist/tools/handlers/pipeline-handlers.d.ts +2 -1
  34. package/dist/tools/handlers/pipeline-handlers.js +64 -10
  35. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  36. package/dist/tools/handlers/system-handlers.d.ts +1 -1
  37. package/dist/tools/input.d.ts +5 -1
  38. package/dist/tools/input.js +37 -1
  39. package/dist/tools/lighting.d.ts +1 -0
  40. package/dist/tools/lighting.js +7 -0
  41. package/dist/tools/physics.d.ts +1 -1
  42. package/dist/tools/sequence.d.ts +1 -0
  43. package/dist/tools/sequence.js +7 -0
  44. package/dist/types/handler-types.d.ts +343 -0
  45. package/dist/types/handler-types.js +2 -0
  46. package/dist/unreal-bridge.d.ts +1 -1
  47. package/dist/unreal-bridge.js +8 -6
  48. package/dist/utils/command-validator.d.ts +1 -0
  49. package/dist/utils/command-validator.js +11 -1
  50. package/dist/utils/error-handler.js +3 -1
  51. package/dist/utils/response-validator.js +2 -2
  52. package/dist/utils/safe-json.d.ts +1 -1
  53. package/dist/utils/safe-json.js +3 -6
  54. package/dist/utils/unreal-command-queue.js +1 -1
  55. package/dist/utils/validation.js +6 -2
  56. package/docs/handler-mapping.md +6 -1
  57. package/package.json +2 -2
  58. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +25 -1
  59. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +40 -58
  60. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +27 -46
  61. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +16 -1
  62. package/server.json +2 -2
  63. package/src/automation/bridge.ts +80 -10
  64. package/src/automation/types.ts +1 -0
  65. package/src/config/class-aliases.ts +65 -0
  66. package/src/constants.ts +10 -0
  67. package/src/graphql/server.ts +23 -23
  68. package/src/index.ts +1 -1
  69. package/src/services/metrics-server.ts +40 -6
  70. package/src/tools/consolidated-tool-definitions.ts +3 -3
  71. package/src/tools/debug.ts +8 -0
  72. package/src/tools/handlers/actor-handlers.ts +5 -31
  73. package/src/tools/handlers/asset-handlers.ts +19 -1
  74. package/src/tools/handlers/blueprint-handlers.ts +1 -1
  75. package/src/tools/handlers/common-handlers.ts +32 -11
  76. package/src/tools/handlers/editor-handlers.ts +8 -7
  77. package/src/tools/handlers/effect-handlers.ts +4 -0
  78. package/src/tools/handlers/graph-handlers.ts +7 -6
  79. package/src/tools/handlers/level-handlers.ts +5 -4
  80. package/src/tools/handlers/lighting-handlers.ts +5 -1
  81. package/src/tools/handlers/pipeline-handlers.ts +83 -16
  82. package/src/tools/input.ts +60 -1
  83. package/src/tools/lighting.ts +11 -0
  84. package/src/tools/physics.ts +1 -1
  85. package/src/tools/sequence.ts +11 -0
  86. package/src/types/handler-types.ts +442 -0
  87. package/src/unreal-bridge.ts +8 -6
  88. package/src/utils/command-validator.ts +23 -1
  89. package/src/utils/error-handler.ts +4 -1
  90. package/src/utils/response-validator.ts +7 -9
  91. package/src/utils/safe-json.ts +20 -15
  92. package/src/utils/unreal-command-queue.ts +3 -1
  93. package/src/utils/validation.test.ts +3 -3
  94. package/src/utils/validation.ts +36 -26
  95. package/tests/test-console-command.mjs +1 -1
  96. package/tests/test-runner.mjs +63 -3
  97. package/tests/run-unreal-tool-tests.mjs +0 -948
  98. package/tests/test-asset-errors.mjs +0 -35
@@ -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 > 64) {
67
- sanitized = sanitized.slice(0, 64);
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: "success"
46
+ expected: "blocked|command_blocked|blocked for safety"
47
47
  },
48
48
  {
49
49
  scenario: "Warning: Unknown command",
@@ -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,