unreal-engine-mcp-server 0.5.0 → 0.5.2

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 (188) hide show
  1. package/.env.example +1 -1
  2. package/.github/release-drafter-config.yml +51 -0
  3. package/.github/workflows/greetings.yml +5 -1
  4. package/.github/workflows/labeler.yml +2 -1
  5. package/.github/workflows/publish-mcp.yml +2 -4
  6. package/.github/workflows/release-drafter.yml +3 -2
  7. package/.github/workflows/release.yml +3 -3
  8. package/CHANGELOG.md +109 -0
  9. package/CONTRIBUTING.md +1 -1
  10. package/GEMINI.md +115 -0
  11. package/Public/Plugin_setup_guide.mp4 +0 -0
  12. package/README.md +166 -200
  13. package/dist/automation/bridge.d.ts +1 -2
  14. package/dist/automation/bridge.js +24 -23
  15. package/dist/automation/connection-manager.d.ts +1 -0
  16. package/dist/automation/connection-manager.js +10 -0
  17. package/dist/automation/message-handler.js +5 -4
  18. package/dist/automation/request-tracker.d.ts +4 -0
  19. package/dist/automation/request-tracker.js +11 -3
  20. package/dist/config.d.ts +0 -1
  21. package/dist/config.js +0 -1
  22. package/dist/constants.d.ts +4 -0
  23. package/dist/constants.js +4 -0
  24. package/dist/graphql/loaders.d.ts +64 -0
  25. package/dist/graphql/loaders.js +117 -0
  26. package/dist/graphql/resolvers.d.ts +3 -3
  27. package/dist/graphql/resolvers.js +33 -30
  28. package/dist/graphql/server.js +3 -1
  29. package/dist/graphql/types.d.ts +2 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +13 -2
  32. package/dist/server-setup.d.ts +0 -1
  33. package/dist/server-setup.js +0 -40
  34. package/dist/tools/actors.d.ts +58 -24
  35. package/dist/tools/actors.js +22 -6
  36. package/dist/tools/assets.d.ts +19 -71
  37. package/dist/tools/assets.js +28 -22
  38. package/dist/tools/base-tool.d.ts +4 -4
  39. package/dist/tools/base-tool.js +1 -1
  40. package/dist/tools/blueprint.d.ts +45 -61
  41. package/dist/tools/blueprint.js +43 -14
  42. package/dist/tools/consolidated-tool-definitions.js +2 -1
  43. package/dist/tools/consolidated-tool-handlers.js +96 -110
  44. package/dist/tools/dynamic-handler-registry.d.ts +11 -9
  45. package/dist/tools/dynamic-handler-registry.js +17 -95
  46. package/dist/tools/editor.d.ts +19 -193
  47. package/dist/tools/editor.js +11 -2
  48. package/dist/tools/environment.d.ts +8 -14
  49. package/dist/tools/foliage.d.ts +18 -143
  50. package/dist/tools/foliage.js +4 -2
  51. package/dist/tools/handlers/actor-handlers.d.ts +1 -1
  52. package/dist/tools/handlers/actor-handlers.js +14 -13
  53. package/dist/tools/handlers/asset-handlers.js +454 -454
  54. package/dist/tools/handlers/sequence-handlers.d.ts +1 -1
  55. package/dist/tools/handlers/sequence-handlers.js +24 -13
  56. package/dist/tools/introspection.d.ts +1 -1
  57. package/dist/tools/introspection.js +1 -1
  58. package/dist/tools/landscape.d.ts +16 -116
  59. package/dist/tools/landscape.js +7 -3
  60. package/dist/tools/level.d.ts +22 -103
  61. package/dist/tools/level.js +26 -18
  62. package/dist/tools/lighting.d.ts +54 -7
  63. package/dist/tools/lighting.js +9 -5
  64. package/dist/tools/materials.d.ts +1 -1
  65. package/dist/tools/materials.js +5 -1
  66. package/dist/tools/niagara.js +37 -2
  67. package/dist/tools/performance.d.ts +0 -1
  68. package/dist/tools/performance.js +0 -1
  69. package/dist/tools/physics.js +5 -1
  70. package/dist/tools/sequence.d.ts +24 -24
  71. package/dist/tools/sequence.js +13 -0
  72. package/dist/tools/ui.d.ts +0 -2
  73. package/dist/types/automation-responses.d.ts +115 -0
  74. package/dist/types/automation-responses.js +2 -0
  75. package/dist/types/responses.d.ts +249 -0
  76. package/dist/types/responses.js +2 -0
  77. package/dist/types/tool-interfaces.d.ts +135 -135
  78. package/dist/types/tool-types.d.ts +2 -0
  79. package/dist/unreal-bridge.js +4 -4
  80. package/dist/utils/command-validator.js +7 -5
  81. package/dist/utils/error-handler.d.ts +24 -2
  82. package/dist/utils/error-handler.js +58 -23
  83. package/dist/utils/normalize.d.ts +7 -4
  84. package/dist/utils/normalize.js +12 -10
  85. package/dist/utils/path-security.d.ts +2 -0
  86. package/dist/utils/path-security.js +24 -0
  87. package/dist/utils/response-factory.d.ts +4 -4
  88. package/dist/utils/response-factory.js +15 -21
  89. package/dist/utils/response-validator.js +88 -73
  90. package/dist/utils/unreal-command-queue.d.ts +2 -0
  91. package/dist/utils/unreal-command-queue.js +8 -1
  92. package/docs/Migration-Guide-v0.5.0.md +1 -9
  93. package/docs/handler-mapping.md +4 -2
  94. package/docs/testing-guide.md +2 -2
  95. package/package.json +12 -6
  96. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +298 -33
  97. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +7 -8
  98. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +229 -319
  99. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +98 -0
  100. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +24 -0
  101. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +96 -0
  102. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +52 -5
  103. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +5 -268
  104. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +57 -2
  105. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +0 -1
  106. package/scripts/run-all-tests.mjs +25 -20
  107. package/server.json +3 -2
  108. package/src/automation/bridge.ts +27 -25
  109. package/src/automation/connection-manager.ts +18 -0
  110. package/src/automation/message-handler.ts +33 -8
  111. package/src/automation/request-tracker.ts +39 -7
  112. package/src/config.ts +1 -1
  113. package/src/constants.ts +7 -0
  114. package/src/graphql/loaders.ts +244 -0
  115. package/src/graphql/resolvers.ts +47 -49
  116. package/src/graphql/server.ts +3 -1
  117. package/src/graphql/types.ts +3 -0
  118. package/src/index.ts +15 -2
  119. package/src/resources/assets.ts +5 -4
  120. package/src/server/tool-registry.ts +3 -3
  121. package/src/server-setup.ts +3 -37
  122. package/src/tools/actors.ts +77 -44
  123. package/src/tools/animation.ts +1 -0
  124. package/src/tools/assets.ts +76 -65
  125. package/src/tools/base-tool.ts +3 -3
  126. package/src/tools/blueprint.ts +170 -104
  127. package/src/tools/consolidated-tool-definitions.ts +2 -1
  128. package/src/tools/consolidated-tool-handlers.ts +129 -150
  129. package/src/tools/dynamic-handler-registry.ts +22 -140
  130. package/src/tools/editor.ts +43 -29
  131. package/src/tools/environment.ts +21 -27
  132. package/src/tools/foliage.ts +28 -25
  133. package/src/tools/handlers/actor-handlers.ts +16 -17
  134. package/src/tools/handlers/asset-handlers.ts +484 -484
  135. package/src/tools/handlers/sequence-handlers.ts +85 -62
  136. package/src/tools/introspection.ts +7 -7
  137. package/src/tools/landscape.ts +34 -28
  138. package/src/tools/level.ts +100 -80
  139. package/src/tools/lighting.ts +25 -20
  140. package/src/tools/materials.ts +9 -3
  141. package/src/tools/niagara.ts +44 -2
  142. package/src/tools/performance.ts +1 -2
  143. package/src/tools/physics.ts +7 -1
  144. package/src/tools/sequence.ts +42 -26
  145. package/src/tools/ui.ts +1 -3
  146. package/src/types/automation-responses.ts +119 -0
  147. package/src/types/responses.ts +355 -0
  148. package/src/types/tool-interfaces.ts +135 -135
  149. package/src/types/tool-types.ts +4 -0
  150. package/src/unreal-bridge.ts +71 -26
  151. package/src/utils/command-validator.ts +47 -5
  152. package/src/utils/error-handler.ts +128 -45
  153. package/src/utils/normalize.test.ts +162 -0
  154. package/src/utils/normalize.ts +38 -16
  155. package/src/utils/path-security.ts +43 -0
  156. package/src/utils/response-factory.ts +29 -24
  157. package/src/utils/response-validator.ts +103 -87
  158. package/src/utils/safe-json.test.ts +90 -0
  159. package/src/utils/unreal-command-queue.ts +13 -1
  160. package/src/utils/validation.test.ts +184 -0
  161. package/tests/test-animation.mjs +358 -33
  162. package/tests/test-asset-graph.mjs +311 -0
  163. package/tests/test-audio.mjs +314 -116
  164. package/tests/test-behavior-tree.mjs +327 -144
  165. package/tests/test-blueprint-graph.mjs +343 -12
  166. package/tests/test-control-editor.mjs +85 -53
  167. package/tests/test-graphql.mjs +58 -8
  168. package/tests/test-input.mjs +349 -0
  169. package/tests/test-inspect.mjs +291 -61
  170. package/tests/test-landscape.mjs +304 -48
  171. package/tests/test-lighting.mjs +428 -0
  172. package/tests/test-manage-level.mjs +70 -51
  173. package/tests/test-performance.mjs +539 -0
  174. package/tests/test-sequence.mjs +82 -46
  175. package/tests/test-system.mjs +72 -33
  176. package/tests/test-wasm.mjs +98 -8
  177. package/vitest.config.ts +35 -0
  178. package/.github/release-drafter.yml +0 -148
  179. package/dist/prompts/index.d.ts +0 -21
  180. package/dist/prompts/index.js +0 -217
  181. package/dist/tools/blueprint/helpers.d.ts +0 -29
  182. package/dist/tools/blueprint/helpers.js +0 -182
  183. package/src/prompts/index.ts +0 -249
  184. package/src/tools/blueprint/helpers.ts +0 -189
  185. package/tests/test-blueprint-events.mjs +0 -35
  186. package/tests/test-extra-tools.mjs +0 -38
  187. package/tests/test-render.mjs +0 -33
  188. package/tests/test-search-assets.mjs +0 -66
@@ -16,61 +16,134 @@ export enum ErrorType {
16
16
  UNKNOWN = 'UNKNOWN'
17
17
  }
18
18
 
19
+ /**
20
+ * Debug information attached to error responses in development mode
21
+ */
22
+ interface ErrorResponseDebug {
23
+ errorType: ErrorType;
24
+ originalError: string;
25
+ stack?: string;
26
+ context?: Record<string, unknown>;
27
+ retriable: boolean;
28
+ scope: string;
29
+ }
30
+
31
+ /**
32
+ * Extended error response with optional debug info
33
+ */
34
+ interface ErrorToolResponse extends BaseToolResponse {
35
+ _debug?: ErrorResponseDebug;
36
+ }
37
+
38
+ /**
39
+ * Represents any error object with common properties
40
+ */
41
+ interface ErrorLike {
42
+ message?: string;
43
+ code?: string;
44
+ type?: string;
45
+ errorType?: string;
46
+ stack?: string;
47
+ response?: { status?: number };
48
+ }
49
+
50
+ /**
51
+ * Normalize any error type to ErrorLike interface
52
+ */
53
+ function normalizeErrorToLike(error: unknown): ErrorLike {
54
+ if (error instanceof Error) {
55
+ return {
56
+ message: error.message,
57
+ stack: error.stack,
58
+ code: (error as NodeJS.ErrnoException).code
59
+ };
60
+ }
61
+ if (typeof error === 'object' && error !== null) {
62
+ const obj = error as Record<string, unknown>;
63
+ return {
64
+ message: typeof obj.message === 'string' ? obj.message : undefined,
65
+ code: typeof obj.code === 'string' ? obj.code : undefined,
66
+ type: typeof obj.type === 'string' ? obj.type : undefined,
67
+ errorType: typeof obj.errorType === 'string' ? obj.errorType : undefined,
68
+ stack: typeof obj.stack === 'string' ? obj.stack : undefined,
69
+ response: typeof obj.response === 'object' && obj.response !== null
70
+ ? {
71
+ status: typeof (obj.response as Record<string, unknown>).status === 'number'
72
+ ? (obj.response as Record<string, unknown>).status as number
73
+ : undefined
74
+ }
75
+ : undefined
76
+ };
77
+ }
78
+ return { message: String(error) };
79
+ }
80
+
19
81
  /**
20
82
  * Consistent error handling for all tools
21
83
  */
22
84
  export class ErrorHandler {
23
85
  /**
24
86
  * Create a standardized error response
87
+ * @param error - The error object (can be Error, string, object with message, or unknown)
88
+ * @param toolName - Name of the tool that failed
89
+ * @param context - Optional additional context for debugging
25
90
  */
26
91
  static createErrorResponse(
27
- error: any,
92
+ error: unknown,
28
93
  toolName: string,
29
- context?: any
30
- ): BaseToolResponse {
31
- const errorType = this.categorizeError(error);
32
- const userMessage = this.getUserFriendlyMessage(errorType, error);
33
- const retriable = this.isRetriable(error);
34
- const scope = context?.scope || `tool-call/${toolName}`;
35
-
94
+ context?: Record<string, unknown>
95
+ ): ErrorToolResponse {
96
+ const errorObj = normalizeErrorToLike(error);
97
+ const errorType = this.categorizeError(errorObj);
98
+ const userMessage = this.getUserFriendlyMessage(errorType, errorObj);
99
+ const retriable = this.isRetriable(errorObj);
100
+ const scope = (context?.scope as string) || `tool-call/${toolName}`;
101
+ const errorMessage = errorObj.message || String(error);
102
+ const errorStack = errorObj.stack;
103
+
36
104
  log.error(`Tool ${toolName} failed:`, {
37
105
  type: errorType,
38
- message: error.message || error,
106
+ message: errorMessage,
39
107
  retriable,
40
108
  scope,
41
109
  context
42
110
  });
43
111
 
44
- return {
112
+ const response: ErrorToolResponse = {
45
113
  success: false,
46
114
  error: userMessage,
47
115
  message: `Failed to execute ${toolName}: ${userMessage}`,
48
- retriable: retriable as any,
49
- scope: scope as any,
50
- // Add debug info in development
51
- ...(process.env.NODE_ENV === 'development' && {
52
- _debug: {
53
- errorType,
54
- originalError: error.message || String(error),
55
- stack: error.stack,
56
- context,
57
- retriable,
58
- scope
59
- }
60
- })
61
- } as any;
116
+ retriable,
117
+ scope
118
+ };
119
+
120
+ // Add debug info in development
121
+ if (process.env.NODE_ENV === 'development') {
122
+ response._debug = {
123
+ errorType,
124
+ originalError: errorMessage,
125
+ stack: errorStack,
126
+ context,
127
+ retriable,
128
+ scope
129
+ };
130
+ }
131
+
132
+ return response;
62
133
  }
63
134
 
64
135
  /**
65
136
  * Categorize error by type
137
+ * @param error - The error to categorize
66
138
  */
67
- private static categorizeError(error: any): ErrorType {
68
- const explicitType = (error?.type || error?.errorType || '').toString().toUpperCase();
139
+ private static categorizeError(error: ErrorLike | Error | string): ErrorType {
140
+ const errorObj = typeof error === 'object' ? error as ErrorLike : null;
141
+ const explicitType = (errorObj?.type || errorObj?.errorType || '').toString().toUpperCase();
69
142
  if (explicitType && Object.values(ErrorType).includes(explicitType as ErrorType)) {
70
143
  return explicitType as ErrorType;
71
144
  }
72
145
 
73
- const errorMessage = error?.message?.toLowerCase() || String(error).toLowerCase();
146
+ const errorMessage = (errorObj?.message || String(error)).toLowerCase();
74
147
 
75
148
  // Connection errors
76
149
  if (
@@ -122,49 +195,59 @@ export class ErrorHandler {
122
195
 
123
196
  /**
124
197
  * Get user-friendly error message
198
+ * @param type - The categorized error type
199
+ * @param error - The original error
125
200
  */
126
- private static getUserFriendlyMessage(type: ErrorType, error: any): string {
127
- const originalMessage = error.message || String(error);
201
+ private static getUserFriendlyMessage(type: ErrorType, error: ErrorLike | Error | string): string {
202
+ const originalMessage = (typeof error === 'object' && error !== null && 'message' in error)
203
+ ? (error as { message?: string }).message || String(error)
204
+ : String(error);
128
205
 
129
206
  switch (type) {
130
207
  case ErrorType.CONNECTION:
131
208
  return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
132
-
209
+
133
210
  case ErrorType.VALIDATION:
134
211
  return `Invalid input: ${originalMessage}`;
135
-
212
+
136
213
  case ErrorType.UNREAL_ENGINE:
137
214
  return `Unreal Engine error: ${originalMessage}`;
138
-
215
+
139
216
  case ErrorType.PARAMETER:
140
217
  return `Invalid parameters: ${originalMessage}`;
141
-
218
+
142
219
  case ErrorType.TIMEOUT:
143
220
  return 'Operation timed out. Unreal Engine may be busy or unresponsive.';
144
-
221
+
145
222
  case ErrorType.EXECUTION:
146
223
  return `Execution failed: ${originalMessage}`;
147
-
224
+
148
225
  default:
149
226
  return originalMessage;
150
227
  }
151
228
  }
152
229
 
153
- /** Determine if an error is likely retriable */
154
- private static isRetriable(error: any): boolean {
230
+ /**
231
+ * Determine if an error is likely retriable
232
+ * @param error - The error to check
233
+ */
234
+ private static isRetriable(error: ErrorLike | Error | string): boolean {
155
235
  try {
156
- const code = (error?.code || '').toString().toUpperCase();
157
- const msg = (error?.message || String(error) || '').toLowerCase();
158
- const status = Number((error?.response?.status));
159
- if (['ECONNRESET','ECONNREFUSED','ETIMEDOUT','EPIPE'].includes(code)) return true;
236
+ const errorObj = typeof error === 'object' ? error as ErrorLike : null;
237
+ const code = (errorObj?.code || '').toString().toUpperCase();
238
+ const msg = (errorObj?.message || String(error) || '').toLowerCase();
239
+ const status = Number(errorObj?.response?.status);
240
+ if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE'].includes(code)) return true;
160
241
  if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg)) return true;
161
242
  if (!isNaN(status) && (status === 429 || (status >= 500 && status < 600))) return true;
162
- } catch {}
243
+ } catch { }
163
244
  return false;
164
245
  }
165
246
 
166
247
  /**
167
248
  * Retry a function with exponential backoff
249
+ * @param fn - The async function to retry
250
+ * @param options - Retry configuration options
168
251
  */
169
252
  static async retryWithBackoff<T>(
170
253
  fn: () => Promise<T>,
@@ -173,14 +256,14 @@ export class ErrorHandler {
173
256
  initialDelay?: number;
174
257
  maxDelay?: number;
175
258
  backoffMultiplier?: number;
176
- shouldRetry?: (error: any) => boolean;
259
+ shouldRetry?: (error: ErrorLike | Error | unknown) => boolean;
177
260
  } = {}
178
261
  ): Promise<T> {
179
262
  const maxRetries = options.maxRetries ?? 3;
180
263
  const initialDelay = options.initialDelay ?? 1000;
181
264
  const maxDelay = options.maxDelay ?? 10000;
182
265
  const multiplier = options.backoffMultiplier ?? 2;
183
- const shouldRetry = options.shouldRetry ?? ((err) => this.isRetriable(err));
266
+ const shouldRetry = options.shouldRetry ?? ((err: unknown) => this.isRetriable(err as ErrorLike));
184
267
 
185
268
  let delay = initialDelay;
186
269
 
@@ -191,7 +274,7 @@ export class ErrorHandler {
191
274
  if (attempt === maxRetries || !shouldRetry(error)) {
192
275
  throw error;
193
276
  }
194
-
277
+
195
278
  await new Promise(resolve => setTimeout(resolve, delay));
196
279
  delay = Math.min(delay * multiplier, maxDelay);
197
280
  }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Unit tests for normalize utility functions
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import {
6
+ toVec3Object,
7
+ toRotObject,
8
+ toVec3Tuple,
9
+ toRotTuple,
10
+ toFiniteNumber,
11
+ normalizePartialVector,
12
+ normalizeTransformInput
13
+ } from './normalize.js';
14
+
15
+ describe('toVec3Object', () => {
16
+ it('converts object input to Vector3', () => {
17
+ const result = toVec3Object({ x: 1, y: 2, z: 3 });
18
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
19
+ });
20
+
21
+ it('converts array input to Vector3', () => {
22
+ const result = toVec3Object([1, 2, 3]);
23
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
24
+ });
25
+
26
+ it('returns null for invalid input', () => {
27
+ expect(toVec3Object('invalid')).toBeNull();
28
+ expect(toVec3Object(null)).toBeNull();
29
+ expect(toVec3Object(undefined)).toBeNull();
30
+ });
31
+
32
+ it('returns null for incomplete object', () => {
33
+ expect(toVec3Object({ x: 1, y: 2 })).toBeNull();
34
+ });
35
+
36
+ it('handles zero values', () => {
37
+ const result = toVec3Object({ x: 0, y: 0, z: 0 });
38
+ expect(result).toEqual({ x: 0, y: 0, z: 0 });
39
+ });
40
+ });
41
+
42
+ describe('toRotObject', () => {
43
+ it('converts object input to Rotator', () => {
44
+ const result = toRotObject({ pitch: 10, yaw: 20, roll: 30 });
45
+ expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
46
+ });
47
+
48
+ it('converts array input to Rotator', () => {
49
+ const result = toRotObject([10, 20, 30]);
50
+ expect(result).toEqual({ pitch: 10, yaw: 20, roll: 30 });
51
+ });
52
+
53
+ it('returns null for invalid input', () => {
54
+ expect(toRotObject('invalid')).toBeNull();
55
+ expect(toRotObject(null)).toBeNull();
56
+ });
57
+ });
58
+
59
+ describe('toVec3Tuple', () => {
60
+ it('converts object to tuple', () => {
61
+ const result = toVec3Tuple({ x: 1, y: 2, z: 3 });
62
+ expect(result).toEqual([1, 2, 3]);
63
+ });
64
+
65
+ it('passes through valid array', () => {
66
+ const result = toVec3Tuple([1, 2, 3]);
67
+ expect(result).toEqual([1, 2, 3]);
68
+ });
69
+
70
+ it('returns null for invalid input', () => {
71
+ expect(toVec3Tuple([1, 2])).toBeNull();
72
+ expect(toVec3Tuple(null)).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe('toRotTuple', () => {
77
+ it('converts object to tuple', () => {
78
+ const result = toRotTuple({ pitch: 10, yaw: 20, roll: 30 });
79
+ expect(result).toEqual([10, 20, 30]);
80
+ });
81
+
82
+ it('passes through valid array', () => {
83
+ const result = toRotTuple([10, 20, 30]);
84
+ expect(result).toEqual([10, 20, 30]);
85
+ });
86
+ });
87
+
88
+ describe('toFiniteNumber', () => {
89
+ it('accepts valid numbers', () => {
90
+ expect(toFiniteNumber(42)).toBe(42);
91
+ expect(toFiniteNumber(0)).toBe(0);
92
+ expect(toFiniteNumber(-5)).toBe(-5);
93
+ expect(toFiniteNumber(3.14)).toBe(3.14);
94
+ });
95
+
96
+ it('parses string numbers', () => {
97
+ expect(toFiniteNumber('42')).toBe(42);
98
+ expect(toFiniteNumber('3.14')).toBe(3.14);
99
+ });
100
+
101
+ it('returns undefined for invalid input', () => {
102
+ expect(toFiniteNumber('not a number')).toBeUndefined();
103
+ expect(toFiniteNumber(NaN)).toBeUndefined();
104
+ expect(toFiniteNumber(Infinity)).toBeUndefined();
105
+ expect(toFiniteNumber(null)).toBeUndefined();
106
+ expect(toFiniteNumber(undefined)).toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe('normalizePartialVector', () => {
111
+ it('normalizes complete vectors', () => {
112
+ const result = normalizePartialVector({ x: 1, y: 2, z: 3 });
113
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
114
+ });
115
+
116
+ it('handles partial vectors', () => {
117
+ const result = normalizePartialVector({ x: 1 });
118
+ expect(result).toBeDefined();
119
+ expect(result?.x).toBe(1);
120
+ });
121
+
122
+ it('uses alternate keys for reading input', () => {
123
+ const result = normalizePartialVector(
124
+ { pitch: 10, yaw: 20, roll: 30 },
125
+ ['pitch', 'yaw', 'roll']
126
+ );
127
+ // alternateKeys are used to READ from input, but output is still x/y/z
128
+ expect(result).toEqual({ x: 10, y: 20, z: 30 });
129
+ });
130
+
131
+ it('returns undefined for invalid input', () => {
132
+ expect(normalizePartialVector(null)).toBeUndefined();
133
+ expect(normalizePartialVector('string')).toBeUndefined();
134
+ });
135
+ });
136
+
137
+ describe('normalizeTransformInput', () => {
138
+ it('normalizes complete transform', () => {
139
+ const result = normalizeTransformInput({
140
+ location: { x: 0, y: 0, z: 100 },
141
+ rotation: { pitch: 0, yaw: 90, roll: 0 },
142
+ scale: { x: 1, y: 1, z: 1 }
143
+ });
144
+ expect(result).toBeDefined();
145
+ expect(result?.location).toBeDefined();
146
+ expect(result?.rotation).toBeDefined();
147
+ expect(result?.scale).toBeDefined();
148
+ });
149
+
150
+ it('handles partial transforms', () => {
151
+ const result = normalizeTransformInput({
152
+ location: { x: 100, y: 200, z: 300 }
153
+ });
154
+ expect(result).toBeDefined();
155
+ expect(result?.location).toBeDefined();
156
+ });
157
+
158
+ it('returns undefined for invalid input', () => {
159
+ expect(normalizeTransformInput(null)).toBeUndefined();
160
+ expect(normalizeTransformInput('invalid')).toBeUndefined();
161
+ });
162
+ });
@@ -4,47 +4,66 @@ export interface Rot3Obj { pitch: number; yaw: number; roll: number; }
4
4
  export type Vec3Tuple = [number, number, number];
5
5
  export type Rot3Tuple = [number, number, number];
6
6
 
7
- export function toVec3Object(input: any): Vec3Obj | null {
7
+ /** Input that may represent a 3D vector */
8
+ type VectorInput = Vec3Obj | Vec3Tuple | Record<string, unknown> | unknown[];
9
+
10
+ /** Input that may represent a 3D rotation */
11
+ type RotationInput = Rot3Obj | Rot3Tuple | Record<string, unknown> | unknown[];
12
+
13
+ /**
14
+ * Convert various input formats to a Vec3 object
15
+ * @param input - Array, object with x/y/z or X/Y/Z properties
16
+ */
17
+ export function toVec3Object(input: VectorInput | unknown): Vec3Obj | null {
8
18
  try {
9
19
  if (Array.isArray(input) && input.length === 3) {
10
20
  const [x, y, z] = input;
11
21
  if ([x, y, z].every(v => typeof v === 'number' && isFinite(v))) {
12
- return { x, y, z };
22
+ return { x: x as number, y: y as number, z: z as number };
13
23
  }
14
24
  }
15
- if (input && typeof input === 'object') {
16
- const x = Number((input as any).x ?? (input as any).X);
17
- const y = Number((input as any).y ?? (input as any).Y);
18
- const z = Number((input as any).z ?? (input as any).Z);
25
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
26
+ const obj = input as Record<string, unknown>;
27
+ const x = Number(obj.x ?? obj.X);
28
+ const y = Number(obj.y ?? obj.Y);
29
+ const z = Number(obj.z ?? obj.Z);
19
30
  if ([x, y, z].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
20
31
  return { x, y, z };
21
32
  }
22
33
  }
23
- } catch {}
34
+ } catch { }
24
35
  return null;
25
36
  }
26
37
 
27
- export function toRotObject(input: any): Rot3Obj | null {
38
+ /**
39
+ * Convert various input formats to a Rotation object
40
+ * @param input - Array, object with pitch/yaw/roll or Pitch/Yaw/Roll properties
41
+ */
42
+ export function toRotObject(input: RotationInput | unknown): Rot3Obj | null {
28
43
  try {
29
44
  if (Array.isArray(input) && input.length === 3) {
30
45
  const [pitch, yaw, roll] = input;
31
46
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && isFinite(v))) {
32
- return { pitch, yaw, roll };
47
+ return { pitch: pitch as number, yaw: yaw as number, roll: roll as number };
33
48
  }
34
49
  }
35
- if (input && typeof input === 'object') {
36
- const pitch = Number((input as any).pitch ?? (input as any).Pitch);
37
- const yaw = Number((input as any).yaw ?? (input as any).Yaw);
38
- const roll = Number((input as any).roll ?? (input as any).Roll);
50
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
51
+ const obj = input as Record<string, unknown>;
52
+ const pitch = Number(obj.pitch ?? obj.Pitch);
53
+ const yaw = Number(obj.yaw ?? obj.Yaw);
54
+ const roll = Number(obj.roll ?? obj.Roll);
39
55
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
40
56
  return { pitch, yaw, roll };
41
57
  }
42
58
  }
43
- } catch {}
59
+ } catch { }
44
60
  return null;
45
61
  }
46
62
 
47
- export function toVec3Tuple(input: any): Vec3Tuple | null {
63
+ /**
64
+ * Convert vector input to a tuple format [x, y, z]
65
+ */
66
+ export function toVec3Tuple(input: VectorInput | unknown): Vec3Tuple | null {
48
67
  const vec = toVec3Object(input);
49
68
  if (!vec) {
50
69
  return null;
@@ -53,7 +72,10 @@ export function toVec3Tuple(input: any): Vec3Tuple | null {
53
72
  return [x, y, z];
54
73
  }
55
74
 
56
- export function toRotTuple(input: any): Rot3Tuple | null {
75
+ /**
76
+ * Convert rotation input to a tuple format [pitch, yaw, roll]
77
+ */
78
+ export function toRotTuple(input: RotationInput | unknown): Rot3Tuple | null {
57
79
  const rot = toRotObject(input);
58
80
  if (!rot) {
59
81
  return null;
@@ -0,0 +1,43 @@
1
+ export function sanitizePath(path: string, allowedRoots: string[] = ['/Game', '/Engine']): string {
2
+ if (!path || typeof path !== 'string') {
3
+ throw new Error('Invalid path: must be a non-empty string');
4
+ }
5
+
6
+ const trimmed = path.trim();
7
+ if (trimmed.length === 0) {
8
+ throw new Error('Invalid path: cannot be empty');
9
+ }
10
+
11
+ // Normalize separators
12
+ const normalized = trimmed.replace(/\\/g, '/');
13
+
14
+ // Prevent directory traversal
15
+ if (normalized.includes('..')) {
16
+ throw new Error('Invalid path: directory traversal (..) is not allowed');
17
+ }
18
+
19
+ // Ensure path starts with a valid root
20
+ // We check case-insensitive for the root prefix to be user-friendly,
21
+ // but Unreal paths are typically case-insensitive anyway.
22
+ const isAllowed = allowedRoots.some(root =>
23
+ normalized.toLowerCase() === root.toLowerCase() ||
24
+ normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`)
25
+ );
26
+
27
+ if (!isAllowed) {
28
+ throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
29
+ }
30
+
31
+ // Basic character validation (Unreal strictness)
32
+ // Blocks: < > : " | ? * (Windows reserved) and control characters
33
+ // allowing spaces, dots, underscores, dashes, slashes
34
+ // Note: Unreal allows spaces in some contexts but it's often safer to restrict them if strict mode is desired.
35
+ // For now, we block the definitely invalid ones.
36
+ // eslint-disable-next-line no-control-regex
37
+ const invalidChars = /[<>:"|?*\x00-\x1f]/;
38
+ if (invalidChars.test(normalized)) {
39
+ throw new Error('Invalid path: contains illegal characters');
40
+ }
41
+
42
+ return normalized;
43
+ }
@@ -1,39 +1,44 @@
1
- import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import { StandardActionResponse } from '../types/tool-interfaces.js';
2
+ import { cleanObject } from './safe-json.js';
2
3
 
3
4
  export class ResponseFactory {
4
- static success(message: string, data?: unknown): CallToolResult {
5
- const content: any[] = [{ type: 'text', text: message }];
6
-
7
- if (data) {
8
- content.push({
9
- type: 'text',
10
- text: JSON.stringify(data, null, 2)
11
- });
12
- }
13
-
5
+ /**
6
+ * Create a standard success response
7
+ */
8
+ static success(data: any, message: string = 'Operation successful'): StandardActionResponse {
14
9
  return {
15
- content,
16
- isError: false
10
+ success: true,
11
+ message,
12
+ data: cleanObject(data)
17
13
  };
18
14
  }
19
15
 
20
- static error(message: string, error?: unknown): CallToolResult {
21
- const errorText = error instanceof Error ? error.message : String(error);
22
- const fullMessage = error ? `${message}: ${errorText}` : message;
16
+ /**
17
+ * Create a standard error response
18
+ * @param error The error object or message
19
+ * @param defaultMessage Fallback message if error is not an Error object
20
+ */
21
+ static error(error: any, defaultMessage: string = 'Operation failed'): StandardActionResponse {
22
+ const errorMessage = error instanceof Error ? error.message : String(error || defaultMessage);
23
+
24
+ // Log the full error for debugging (internal logs) but return a clean message to the client
25
+ console.error('[ResponseFactory] Error:', error);
23
26
 
24
27
  return {
25
- content: [{ type: 'text', text: fullMessage }],
26
- isError: true
28
+ success: false,
29
+ message: errorMessage,
30
+ data: null
27
31
  };
28
32
  }
29
33
 
30
- static json(data: unknown): CallToolResult {
34
+ /**
35
+ * Create a validation error response
36
+ */
37
+ static validationError(message: string): StandardActionResponse {
31
38
  return {
32
- content: [{
33
- type: 'text',
34
- text: JSON.stringify(data, null, 2)
35
- }],
36
- isError: false
39
+ success: false,
40
+ message: `Validation Error: ${message}`,
41
+ data: null
37
42
  };
38
43
  }
39
44
  }