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
@@ -1,11 +1,73 @@
1
1
  import { cleanObject } from '../../utils/safe-json.js';
2
2
  import { ITools } from '../../types/tool-interfaces.js';
3
+ import type { PipelineArgs } from '../../types/handler-types.js';
3
4
  import { executeAutomationRequest } from './common-handlers.js';
4
5
  import { spawn } from 'child_process';
5
6
  import path from 'path';
6
7
  import fs from 'fs';
7
8
 
8
- export async function handlePipelineTools(action: string, args: any, tools: ITools) {
9
+ function validateUbtArgumentsString(extraArgs: string): void {
10
+ if (!extraArgs || typeof extraArgs !== 'string') {
11
+ return;
12
+ }
13
+
14
+ const forbiddenChars = ['\n', '\r', ';', '|', '`', '&&', '||', '>', '<'];
15
+ for (const char of forbiddenChars) {
16
+ if (extraArgs.includes(char)) {
17
+ throw new Error(
18
+ `UBT arguments contain forbidden character(s) and are blocked for safety. Blocked: ${JSON.stringify(char)}.`
19
+ );
20
+ }
21
+ }
22
+ }
23
+
24
+ function tokenizeArgs(extraArgs: string): string[] {
25
+ if (!extraArgs) {
26
+ return [];
27
+ }
28
+
29
+ const args: string[] = [];
30
+ let current = '';
31
+ let inQuotes = false;
32
+ let escapeNext = false;
33
+
34
+ for (let i = 0; i < extraArgs.length; i++) {
35
+ const ch = extraArgs[i];
36
+
37
+ if (escapeNext) {
38
+ current += ch;
39
+ escapeNext = false;
40
+ continue;
41
+ }
42
+
43
+ if (ch === '\\') {
44
+ escapeNext = true;
45
+ continue;
46
+ }
47
+
48
+ if (ch === '"') {
49
+ inQuotes = !inQuotes;
50
+ continue;
51
+ }
52
+
53
+ if (!inQuotes && /\s/.test(ch)) {
54
+ if (current.length > 0) {
55
+ args.push(current);
56
+ current = '';
57
+ }
58
+ } else {
59
+ current += ch;
60
+ }
61
+ }
62
+
63
+ if (current.length > 0) {
64
+ args.push(current);
65
+ }
66
+
67
+ return args;
68
+ }
69
+
70
+ export async function handlePipelineTools(action: string, args: PipelineArgs, tools: ITools) {
9
71
  switch (action) {
10
72
  case 'run_ubt': {
11
73
  const target = args.target;
@@ -14,11 +76,12 @@ export async function handlePipelineTools(action: string, args: any, tools: IToo
14
76
  const extraArgs = args.arguments || '';
15
77
 
16
78
  if (!target) {
17
- return { success: false, error: 'MISSING_TARGET', message: 'Target is required for run_ubt' };
79
+ throw new Error('Target is required for run_ubt');
18
80
  }
19
81
 
20
- // Try to find UnrealBuildTool
21
- let ubtPath = 'UnrealBuildTool'; // Assume in PATH by default
82
+ validateUbtArgumentsString(extraArgs);
83
+
84
+ let ubtPath = 'UnrealBuildTool';
22
85
  const enginePath = process.env.UE_ENGINE_PATH || process.env.UNREAL_ENGINE_PATH;
23
86
 
24
87
  if (enginePath) {
@@ -34,13 +97,11 @@ export async function handlePipelineTools(action: string, args: any, tools: IToo
34
97
  }
35
98
 
36
99
  if (!projectPath) {
37
- return { success: false, error: 'MISSING_PROJECT_PATH', message: 'UE_PROJECT_PATH environment variable is not set and no projectPath argument was provided.' };
100
+ throw new Error('UE_PROJECT_PATH environment variable is not set and no projectPath argument was provided.');
38
101
  }
39
102
 
40
- // If projectPath points to a .uproject file, use it. If it's a directory, look for a .uproject file.
41
103
  let uprojectFile = projectPath;
42
104
  if (!uprojectFile.endsWith('.uproject')) {
43
- // Find first .uproject in the directory
44
105
  try {
45
106
  const files = fs.readdirSync(projectPath);
46
107
  const found = files.find(f => f.endsWith('.uproject'));
@@ -48,20 +109,23 @@ export async function handlePipelineTools(action: string, args: any, tools: IToo
48
109
  uprojectFile = path.join(projectPath, found);
49
110
  }
50
111
  } catch (_e) {
51
- return { success: false, error: 'INVALID_PROJECT_PATH', message: `Could not read project directory: ${projectPath}` };
112
+ throw new Error(`Could not read project directory: ${projectPath}`);
52
113
  }
53
114
  }
54
115
 
116
+ const projectArg = `-Project="${uprojectFile}"`;
117
+ const extraTokens = tokenizeArgs(extraArgs);
118
+
55
119
  const cmdArgs = [
56
120
  target,
57
121
  platform,
58
122
  configuration,
59
- `-Project="${uprojectFile}"`,
60
- extraArgs
61
- ].filter(Boolean);
123
+ projectArg,
124
+ ...extraTokens
125
+ ];
62
126
 
63
127
  return new Promise((resolve) => {
64
- const child = spawn(ubtPath, cmdArgs, { shell: true });
128
+ const child = spawn(ubtPath, cmdArgs, { shell: false });
65
129
 
66
130
  const MAX_OUTPUT_SIZE = 20 * 1024; // 20KB cap
67
131
  let stdout = '';
@@ -90,12 +154,14 @@ export async function handlePipelineTools(action: string, args: any, tools: IToo
90
154
  ? '\n[Output truncated for response payload]'
91
155
  : '';
92
156
 
157
+ const quotedArgs = cmdArgs.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
158
+
93
159
  if (code === 0) {
94
160
  resolve({
95
161
  success: true,
96
162
  message: 'UnrealBuildTool finished successfully',
97
163
  output: stdout + truncatedNote,
98
- command: `${ubtPath} ${cmdArgs.join(' ')}`
164
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
99
165
  });
100
166
  } else {
101
167
  resolve({
@@ -104,23 +170,24 @@ export async function handlePipelineTools(action: string, args: any, tools: IToo
104
170
  message: `UnrealBuildTool failed with code ${code}`,
105
171
  output: stdout + truncatedNote,
106
172
  errorOutput: stderr + truncatedNote,
107
- command: `${ubtPath} ${cmdArgs.join(' ')}`
173
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
108
174
  });
109
175
  }
110
176
  });
111
177
 
112
178
  child.on('error', (err) => {
179
+ const quotedArgs = cmdArgs.map(arg => arg.includes(' ') ? `"${arg}"` : arg);
180
+
113
181
  resolve({
114
182
  success: false,
115
183
  error: 'SPAWN_FAILED',
116
184
  message: `Failed to spawn UnrealBuildTool: ${err.message}`,
117
- command: `${ubtPath} ${cmdArgs.join(' ')}`
185
+ command: `${ubtPath} ${quotedArgs.join(' ')}`
118
186
  });
119
187
  });
120
188
  });
121
189
  }
122
190
  default:
123
- // Fallback to automation bridge if we add more actions later that are bridge-supported
124
191
  const res = await executeAutomationRequest(tools, 'manage_pipeline', { ...args, subAction: action }, 'Automation bridge not available for manage_pipeline');
125
192
  return cleanObject(res);
126
193
  }
@@ -8,6 +8,42 @@ interface ToolDefinition {
8
8
  outputSchema: object;
9
9
  }
10
10
 
11
+ // Common valid key names for UE5 Enhanced Input (not exhaustive, but covers primary cases)
12
+ const VALID_KEY_NAMES = new Set([
13
+ // Keyboard - Letters
14
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
15
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
16
+ // Keyboard - Numbers
17
+ 'Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine',
18
+ // Keyboard - Function keys
19
+ 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
20
+ // Keyboard - Special
21
+ 'SpaceBar', 'Enter', 'Escape', 'Tab', 'BackSpace', 'CapsLock',
22
+ 'LeftShift', 'RightShift', 'LeftControl', 'RightControl', 'LeftAlt', 'RightAlt',
23
+ 'LeftCommand', 'RightCommand', 'Insert', 'Delete', 'Home', 'End', 'PageUp', 'PageDown',
24
+ 'Up', 'Down', 'Left', 'Right',
25
+ // Keyboard - Punctuation
26
+ 'Semicolon', 'Equals', 'Comma', 'Hyphen', 'Underscore', 'Period', 'Slash', 'Tilde',
27
+ 'LeftBracket', 'Backslash', 'RightBracket', 'Apostrophe', 'Quote',
28
+ // Mouse - Buttons
29
+ 'LeftMouseButton', 'RightMouseButton', 'MiddleMouseButton', 'ThumbMouseButton', 'ThumbMouseButton2',
30
+ // Mouse - Axes (must be mapped separately, not as composite 2D)
31
+ 'MouseX', 'MouseY', 'MouseWheelAxis', 'MouseScrollUp', 'MouseScrollDown',
32
+ // Gamepad - Face Buttons
33
+ 'Gamepad_FaceButton_Bottom', 'Gamepad_FaceButton_Right', 'Gamepad_FaceButton_Left', 'Gamepad_FaceButton_Top',
34
+ // Gamepad - Shoulder/Trigger
35
+ 'Gamepad_LeftShoulder', 'Gamepad_RightShoulder', 'Gamepad_LeftTrigger', 'Gamepad_RightTrigger',
36
+ 'Gamepad_LeftTriggerAxis', 'Gamepad_RightTriggerAxis',
37
+ // Gamepad - Sticks
38
+ 'Gamepad_LeftThumbstick', 'Gamepad_RightThumbstick',
39
+ 'Gamepad_LeftStick_Up', 'Gamepad_LeftStick_Down', 'Gamepad_LeftStick_Left', 'Gamepad_LeftStick_Right',
40
+ 'Gamepad_RightStick_Up', 'Gamepad_RightStick_Down', 'Gamepad_RightStick_Left', 'Gamepad_RightStick_Right',
41
+ // Gamepad - D-Pad
42
+ 'Gamepad_DPad_Up', 'Gamepad_DPad_Down', 'Gamepad_DPad_Left', 'Gamepad_DPad_Right',
43
+ // Gamepad - Special
44
+ 'Gamepad_Special_Left', 'Gamepad_Special_Right'
45
+ ]);
46
+
11
47
  export class InputTools {
12
48
  private automationBridge: AutomationBridge | null = null;
13
49
 
@@ -37,11 +73,33 @@ export class InputTools {
37
73
 
38
74
  async addMapping(contextPath: string, actionPath: string, key: string) {
39
75
  if (!this.automationBridge) throw new Error('Automation bridge not set');
76
+
77
+ // Validate key name
78
+ if (!key || typeof key !== 'string' || key.trim().length === 0) {
79
+ return { success: false, error: 'INVALID_ARGUMENT', message: 'Key name is required.' };
80
+ }
81
+
82
+ const trimmedKey = key.trim();
83
+
84
+ // Check for common mistakes (composite 2D axis names)
85
+ if (trimmedKey === 'MouseXY2D' || trimmedKey === 'Mouse2D' || trimmedKey === 'MouseXY') {
86
+ return {
87
+ success: false,
88
+ error: 'INVALID_ARGUMENT',
89
+ message: `Invalid key name '${trimmedKey}'. For mouse axis input, use separate mappings with 'MouseX' and 'MouseY' keys instead of composite 2D axis names.`
90
+ };
91
+ }
92
+
93
+ // Warn if key is not in our known list (but still attempt the mapping)
94
+ if (!VALID_KEY_NAMES.has(trimmedKey)) {
95
+ console.warn(`[InputTools] Key '${trimmedKey}' is not in the standard key list. Attempting mapping anyway.`);
96
+ }
97
+
40
98
  return this.automationBridge.sendAutomationRequest('manage_input', {
41
99
  action: 'add_mapping',
42
100
  contextPath,
43
101
  actionPath,
44
- key
102
+ key: trimmedKey
45
103
  });
46
104
  }
47
105
 
@@ -55,6 +113,7 @@ export class InputTools {
55
113
  }
56
114
  }
57
115
 
116
+
58
117
  export const inputTools: ToolDefinition = {
59
118
  name: 'manage_input',
60
119
  description: `Enhanced Input management.
@@ -29,6 +29,17 @@ export class LightingTools {
29
29
  return `Light_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
30
30
  }
31
31
 
32
+ /**
33
+ * List available light types (classes)
34
+ */
35
+ async listLightTypes() {
36
+ if (!this.automationBridge) {
37
+ throw new Error('Automation Bridge required to list light types');
38
+ }
39
+ const response = await this.automationBridge.sendAutomationRequest('list_light_types', {});
40
+ return response;
41
+ }
42
+
32
43
  /**
33
44
  * Spawn a light actor using the Automation Bridge.
34
45
  * @param lightClass The Unreal light class name (e.g. 'DirectionalLight', 'PointLight')
@@ -332,7 +332,7 @@ export class PhysicsTools {
332
332
  */
333
333
  async configureVehicle(params: {
334
334
  vehicleName: string;
335
- vehicleType: 'Car' | 'Bike' | 'Tank' | 'Aircraft';
335
+ vehicleType: string;
336
336
  wheels?: Array<{
337
337
  name: string;
338
338
  radius: number;
@@ -361,6 +361,17 @@ export class SequenceTools extends BaseTool implements ISequenceTools {
361
361
  return resp;
362
362
  }
363
363
 
364
+ /**
365
+ * List available track types
366
+ */
367
+ async listTrackTypes(): Promise<StandardActionResponse> {
368
+ const resp = await this.sendAction('list_track_types', {});
369
+ if (!resp.success && this.isUnknownActionResponse(resp)) {
370
+ return { success: false, error: 'UNKNOWN_PLUGIN_ACTION', message: 'Automation plugin does not implement list_track_types' } as const;
371
+ }
372
+ return resp;
373
+ }
374
+
364
375
  /**
365
376
  * Set playback work range
366
377
  */