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
@@ -3,6 +3,8 @@ export interface BaseToolResponse {
3
3
  message?: string;
4
4
  error?: string;
5
5
  warning?: string;
6
+ retriable?: boolean;
7
+ scope?: string;
6
8
  }
7
9
  export interface AssetInfo {
8
10
  Name: string;
@@ -206,7 +206,7 @@ export class UnrealBridge {
206
206
  const success = response.success !== false;
207
207
  const rawResult = response.result && typeof response.result === 'object'
208
208
  ? { ...response.result }
209
- : response.result;
209
+ : undefined;
210
210
  const value = rawResult?.value ??
211
211
  rawResult?.propertyValue ??
212
212
  (success ? rawResult : undefined);
@@ -295,7 +295,7 @@ export class UnrealBridge {
295
295
  const success = response.success !== false;
296
296
  const rawResult = response.result && typeof response.result === 'object'
297
297
  ? { ...response.result }
298
- : response.result;
298
+ : undefined;
299
299
  if (success) {
300
300
  return {
301
301
  success: true,
@@ -423,7 +423,7 @@ export class UnrealBridge {
423
423
  const resp = await bridge.sendAutomationRequest('system_control', { action: 'get_engine_version' }, { timeoutMs: 15000 });
424
424
  const raw = resp && typeof resp.result === 'object'
425
425
  ? resp.result
426
- : (resp?.result ?? resp ?? {});
426
+ : resp?.result ?? resp ?? {};
427
427
  const version = typeof raw.version === 'string' ? raw.version : 'unknown';
428
428
  const major = typeof raw.major === 'number' ? raw.major : 0;
429
429
  const minor = typeof raw.minor === 'number' ? raw.minor : 0;
@@ -459,7 +459,7 @@ export class UnrealBridge {
459
459
  const resp = await bridge.sendAutomationRequest('system_control', { action: 'get_feature_flags' }, { timeoutMs: 15000 });
460
460
  const raw = resp && typeof resp.result === 'object'
461
461
  ? resp.result
462
- : (resp?.result ?? resp ?? {});
462
+ : resp?.result ?? resp ?? {};
463
463
  const subs = raw && typeof raw.subsystems === 'object'
464
464
  ? raw.subsystems
465
465
  : {};
@@ -1,18 +1,20 @@
1
1
  export class CommandValidator {
2
2
  static DANGEROUS_COMMANDS = [
3
- 'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
3
+ 'quit', 'exit', 'kill', 'crash',
4
+ 'r.gpucrash', 'r.crash', 'debug crash', 'forcecrash', 'debug break',
5
+ 'assert false', 'check(false)',
4
6
  'viewmode visualizebuffer basecolor',
5
7
  'viewmode visualizebuffer worldnormal',
6
- 'r.gpucrash',
7
- 'buildpaths',
8
- 'rebuildnavigation'
8
+ 'buildpaths', 'rebuildnavigation',
9
+ 'obj garbage', 'obj list', 'memreport',
10
+ 'delete', 'destroy'
9
11
  ];
10
12
  static FORBIDDEN_TOKENS = [
11
13
  'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
12
14
  'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
13
15
  'import os', 'import subprocess', 'subprocess.', 'os.system',
14
16
  'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
15
- 'with open', 'open('
17
+ 'with open', 'open(', 'write(', 'read('
16
18
  ];
17
19
  static INVALID_PATTERNS = [
18
20
  /^\d+$/,
@@ -8,8 +8,29 @@ export declare enum ErrorType {
8
8
  TIMEOUT = "TIMEOUT",
9
9
  UNKNOWN = "UNKNOWN"
10
10
  }
11
+ interface ErrorResponseDebug {
12
+ errorType: ErrorType;
13
+ originalError: string;
14
+ stack?: string;
15
+ context?: Record<string, unknown>;
16
+ retriable: boolean;
17
+ scope: string;
18
+ }
19
+ interface ErrorToolResponse extends BaseToolResponse {
20
+ _debug?: ErrorResponseDebug;
21
+ }
22
+ interface ErrorLike {
23
+ message?: string;
24
+ code?: string;
25
+ type?: string;
26
+ errorType?: string;
27
+ stack?: string;
28
+ response?: {
29
+ status?: number;
30
+ };
31
+ }
11
32
  export declare class ErrorHandler {
12
- static createErrorResponse(error: any, toolName: string, context?: any): BaseToolResponse;
33
+ static createErrorResponse(error: unknown, toolName: string, context?: Record<string, unknown>): ErrorToolResponse;
13
34
  private static categorizeError;
14
35
  private static getUserFriendlyMessage;
15
36
  private static isRetriable;
@@ -18,7 +39,8 @@ export declare class ErrorHandler {
18
39
  initialDelay?: number;
19
40
  maxDelay?: number;
20
41
  backoffMultiplier?: number;
21
- shouldRetry?: (error: any) => boolean;
42
+ shouldRetry?: (error: ErrorLike | Error | unknown) => boolean;
22
43
  }): Promise<T>;
23
44
  }
45
+ export {};
24
46
  //# sourceMappingURL=error-handler.d.ts.map
@@ -10,43 +10,75 @@ export var ErrorType;
10
10
  ErrorType["TIMEOUT"] = "TIMEOUT";
11
11
  ErrorType["UNKNOWN"] = "UNKNOWN";
12
12
  })(ErrorType || (ErrorType = {}));
13
+ function normalizeErrorToLike(error) {
14
+ if (error instanceof Error) {
15
+ return {
16
+ message: error.message,
17
+ stack: error.stack,
18
+ code: error.code
19
+ };
20
+ }
21
+ if (typeof error === 'object' && error !== null) {
22
+ const obj = error;
23
+ return {
24
+ message: typeof obj.message === 'string' ? obj.message : undefined,
25
+ code: typeof obj.code === 'string' ? obj.code : undefined,
26
+ type: typeof obj.type === 'string' ? obj.type : undefined,
27
+ errorType: typeof obj.errorType === 'string' ? obj.errorType : undefined,
28
+ stack: typeof obj.stack === 'string' ? obj.stack : undefined,
29
+ response: typeof obj.response === 'object' && obj.response !== null
30
+ ? {
31
+ status: typeof obj.response.status === 'number'
32
+ ? obj.response.status
33
+ : undefined
34
+ }
35
+ : undefined
36
+ };
37
+ }
38
+ return { message: String(error) };
39
+ }
13
40
  export class ErrorHandler {
14
41
  static createErrorResponse(error, toolName, context) {
15
- const errorType = this.categorizeError(error);
16
- const userMessage = this.getUserFriendlyMessage(errorType, error);
17
- const retriable = this.isRetriable(error);
42
+ const errorObj = normalizeErrorToLike(error);
43
+ const errorType = this.categorizeError(errorObj);
44
+ const userMessage = this.getUserFriendlyMessage(errorType, errorObj);
45
+ const retriable = this.isRetriable(errorObj);
18
46
  const scope = context?.scope || `tool-call/${toolName}`;
47
+ const errorMessage = errorObj.message || String(error);
48
+ const errorStack = errorObj.stack;
19
49
  log.error(`Tool ${toolName} failed:`, {
20
50
  type: errorType,
21
- message: error.message || error,
51
+ message: errorMessage,
22
52
  retriable,
23
53
  scope,
24
54
  context
25
55
  });
26
- return {
56
+ const response = {
27
57
  success: false,
28
58
  error: userMessage,
29
59
  message: `Failed to execute ${toolName}: ${userMessage}`,
30
- retriable: retriable,
31
- scope: scope,
32
- ...(process.env.NODE_ENV === 'development' && {
33
- _debug: {
34
- errorType,
35
- originalError: error.message || String(error),
36
- stack: error.stack,
37
- context,
38
- retriable,
39
- scope
40
- }
41
- })
60
+ retriable,
61
+ scope
42
62
  };
63
+ if (process.env.NODE_ENV === 'development') {
64
+ response._debug = {
65
+ errorType,
66
+ originalError: errorMessage,
67
+ stack: errorStack,
68
+ context,
69
+ retriable,
70
+ scope
71
+ };
72
+ }
73
+ return response;
43
74
  }
44
75
  static categorizeError(error) {
45
- const explicitType = (error?.type || error?.errorType || '').toString().toUpperCase();
76
+ const errorObj = typeof error === 'object' ? error : null;
77
+ const explicitType = (errorObj?.type || errorObj?.errorType || '').toString().toUpperCase();
46
78
  if (explicitType && Object.values(ErrorType).includes(explicitType)) {
47
79
  return explicitType;
48
80
  }
49
- const errorMessage = error?.message?.toLowerCase() || String(error).toLowerCase();
81
+ const errorMessage = (errorObj?.message || String(error)).toLowerCase();
50
82
  if (errorMessage.includes('econnrefused') ||
51
83
  errorMessage.includes('timeout') ||
52
84
  errorMessage.includes('connection') ||
@@ -77,7 +109,9 @@ export class ErrorHandler {
77
109
  return ErrorType.UNKNOWN;
78
110
  }
79
111
  static getUserFriendlyMessage(type, error) {
80
- const originalMessage = error.message || String(error);
112
+ const originalMessage = (typeof error === 'object' && error !== null && 'message' in error)
113
+ ? error.message || String(error)
114
+ : String(error);
81
115
  switch (type) {
82
116
  case ErrorType.CONNECTION:
83
117
  return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
@@ -97,9 +131,10 @@ export class ErrorHandler {
97
131
  }
98
132
  static isRetriable(error) {
99
133
  try {
100
- const code = (error?.code || '').toString().toUpperCase();
101
- const msg = (error?.message || String(error) || '').toLowerCase();
102
- const status = Number((error?.response?.status));
134
+ const errorObj = typeof error === 'object' ? error : null;
135
+ const code = (errorObj?.code || '').toString().toUpperCase();
136
+ const msg = (errorObj?.message || String(error) || '').toLowerCase();
137
+ const status = Number(errorObj?.response?.status);
103
138
  if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE'].includes(code))
104
139
  return true;
105
140
  if (/timeout|timed out|network|connection|closed|unavailable|busy|temporar/.test(msg))
@@ -10,11 +10,14 @@ export interface Rot3Obj {
10
10
  }
11
11
  export type Vec3Tuple = [number, number, number];
12
12
  export type Rot3Tuple = [number, number, number];
13
- export declare function toVec3Object(input: any): Vec3Obj | null;
14
- export declare function toRotObject(input: any): Rot3Obj | null;
15
- export declare function toVec3Tuple(input: any): Vec3Tuple | null;
16
- export declare function toRotTuple(input: any): Rot3Tuple | null;
13
+ type VectorInput = Vec3Obj | Vec3Tuple | Record<string, unknown> | unknown[];
14
+ type RotationInput = Rot3Obj | Rot3Tuple | Record<string, unknown> | unknown[];
15
+ export declare function toVec3Object(input: VectorInput | unknown): Vec3Obj | null;
16
+ export declare function toRotObject(input: RotationInput | unknown): Rot3Obj | null;
17
+ export declare function toVec3Tuple(input: VectorInput | unknown): Vec3Tuple | null;
18
+ export declare function toRotTuple(input: RotationInput | unknown): Rot3Tuple | null;
17
19
  export declare function toFiniteNumber(raw: unknown): number | undefined;
18
20
  export declare function normalizePartialVector(value: any, alternateKeys?: string[]): Record<string, number> | undefined;
19
21
  export declare function normalizeTransformInput(transform: any): Record<string, unknown> | undefined;
22
+ export {};
20
23
  //# sourceMappingURL=normalize.d.ts.map
@@ -3,13 +3,14 @@ export function toVec3Object(input) {
3
3
  if (Array.isArray(input) && input.length === 3) {
4
4
  const [x, y, z] = input;
5
5
  if ([x, y, z].every(v => typeof v === 'number' && isFinite(v))) {
6
- return { x, y, z };
6
+ return { x: x, y: y, z: z };
7
7
  }
8
8
  }
9
- if (input && typeof input === 'object') {
10
- const x = Number(input.x ?? input.X);
11
- const y = Number(input.y ?? input.Y);
12
- const z = Number(input.z ?? input.Z);
9
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
10
+ const obj = input;
11
+ const x = Number(obj.x ?? obj.X);
12
+ const y = Number(obj.y ?? obj.Y);
13
+ const z = Number(obj.z ?? obj.Z);
13
14
  if ([x, y, z].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
14
15
  return { x, y, z };
15
16
  }
@@ -23,13 +24,14 @@ export function toRotObject(input) {
23
24
  if (Array.isArray(input) && input.length === 3) {
24
25
  const [pitch, yaw, roll] = input;
25
26
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && isFinite(v))) {
26
- return { pitch, yaw, roll };
27
+ return { pitch: pitch, yaw: yaw, roll: roll };
27
28
  }
28
29
  }
29
- if (input && typeof input === 'object') {
30
- const pitch = Number(input.pitch ?? input.Pitch);
31
- const yaw = Number(input.yaw ?? input.Yaw);
32
- const roll = Number(input.roll ?? input.Roll);
30
+ if (input && typeof input === 'object' && !Array.isArray(input)) {
31
+ const obj = input;
32
+ const pitch = Number(obj.pitch ?? obj.Pitch);
33
+ const yaw = Number(obj.yaw ?? obj.Yaw);
34
+ const roll = Number(obj.roll ?? obj.Roll);
33
35
  if ([pitch, yaw, roll].every(v => typeof v === 'number' && !isNaN(v) && isFinite(v))) {
34
36
  return { pitch, yaw, roll };
35
37
  }
@@ -0,0 +1,2 @@
1
+ export declare function sanitizePath(path: string, allowedRoots?: string[]): string;
2
+ //# sourceMappingURL=path-security.d.ts.map
@@ -0,0 +1,24 @@
1
+ export function sanitizePath(path, allowedRoots = ['/Game', '/Engine']) {
2
+ if (!path || typeof path !== 'string') {
3
+ throw new Error('Invalid path: must be a non-empty string');
4
+ }
5
+ const trimmed = path.trim();
6
+ if (trimmed.length === 0) {
7
+ throw new Error('Invalid path: cannot be empty');
8
+ }
9
+ const normalized = trimmed.replace(/\\/g, '/');
10
+ if (normalized.includes('..')) {
11
+ throw new Error('Invalid path: directory traversal (..) is not allowed');
12
+ }
13
+ const isAllowed = allowedRoots.some(root => normalized.toLowerCase() === root.toLowerCase() ||
14
+ normalized.toLowerCase().startsWith(`${root.toLowerCase()}/`));
15
+ if (!isAllowed) {
16
+ throw new Error(`Invalid path: must start with one of [${allowedRoots.join(', ')}]`);
17
+ }
18
+ const invalidChars = /[<>:"|?*\x00-\x1f]/;
19
+ if (invalidChars.test(normalized)) {
20
+ throw new Error('Invalid path: contains illegal characters');
21
+ }
22
+ return normalized;
23
+ }
24
+ //# sourceMappingURL=path-security.js.map
@@ -1,7 +1,7 @@
1
- import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
1
+ import { StandardActionResponse } from '../types/tool-interfaces.js';
2
2
  export declare class ResponseFactory {
3
- static success(message: string, data?: unknown): CallToolResult;
4
- static error(message: string, error?: unknown): CallToolResult;
5
- static json(data: unknown): CallToolResult;
3
+ static success(data: any, message?: string): StandardActionResponse;
4
+ static error(error: any, defaultMessage?: string): StandardActionResponse;
5
+ static validationError(message: string): StandardActionResponse;
6
6
  }
7
7
  //# sourceMappingURL=response-factory.d.ts.map
@@ -1,32 +1,26 @@
1
+ import { cleanObject } from './safe-json.js';
1
2
  export class ResponseFactory {
2
- static success(message, data) {
3
- const content = [{ type: 'text', text: message }];
4
- if (data) {
5
- content.push({
6
- type: 'text',
7
- text: JSON.stringify(data, null, 2)
8
- });
9
- }
3
+ static success(data, message = 'Operation successful') {
10
4
  return {
11
- content,
12
- isError: false
5
+ success: true,
6
+ message,
7
+ data: cleanObject(data)
13
8
  };
14
9
  }
15
- static error(message, error) {
16
- const errorText = error instanceof Error ? error.message : String(error);
17
- const fullMessage = error ? `${message}: ${errorText}` : message;
10
+ static error(error, defaultMessage = 'Operation failed') {
11
+ const errorMessage = error instanceof Error ? error.message : String(error || defaultMessage);
12
+ console.error('[ResponseFactory] Error:', error);
18
13
  return {
19
- content: [{ type: 'text', text: fullMessage }],
20
- isError: true
14
+ success: false,
15
+ message: errorMessage,
16
+ data: null
21
17
  };
22
18
  }
23
- static json(data) {
19
+ static validationError(message) {
24
20
  return {
25
- content: [{
26
- type: 'text',
27
- text: JSON.stringify(data, null, 2)
28
- }],
29
- isError: false
21
+ success: false,
22
+ message: `Validation Error: ${message}`,
23
+ data: null
30
24
  };
31
25
  }
32
26
  }
@@ -21,91 +21,106 @@ function buildSummaryText(toolName, payload) {
21
21
  return `${toolName} responded`;
22
22
  }
23
23
  const effectivePayload = { ...payload };
24
- if (isRecord(effectivePayload.data)) {
25
- Object.assign(effectivePayload, effectivePayload.data);
26
- }
27
- if (isRecord(effectivePayload.result)) {
28
- Object.assign(effectivePayload, effectivePayload.result);
29
- }
30
- const parts = [];
31
- const listKeys = ['actors', 'levels', 'assets', 'folders', 'blueprints', 'components', 'pawnClasses', 'foliageTypes', 'nodes', 'tracks', 'bindings', 'keys'];
32
- for (const key of listKeys) {
33
- if (Array.isArray(effectivePayload[key])) {
34
- const arr = effectivePayload[key];
35
- const names = arr.map(i => isRecord(i) ? (i.name || i.path || i.id || i.assetName || i.objectPath || i.packageName || i.nodeName || '<?>') : String(i));
36
- const count = arr.length;
37
- const preview = names.slice(0, 100).join(', ');
38
- const suffix = count > 100 ? `, ... (+${count - 100} more)` : '';
39
- parts.push(`${key}: ${preview}${suffix} (Total: ${count})`);
24
+ const flattenWrappers = (obj, depth = 0) => {
25
+ if (depth > 5)
26
+ return;
27
+ if (isRecord(obj.data)) {
28
+ Object.assign(obj, obj.data);
29
+ delete obj.data;
30
+ flattenWrappers(obj, depth + 1);
40
31
  }
41
- }
42
- if (typeof effectivePayload.actor === 'string' || isRecord(effectivePayload.actor)) {
43
- const a = effectivePayload.actor;
44
- const name = isRecord(a) ? (a.name || a.path) : a;
45
- const loc = isRecord(effectivePayload.location) ? ` at [${effectivePayload.location.x},${effectivePayload.location.y},${effectivePayload.location.z}]` : '';
46
- parts.push(`Actor: ${name}${loc}`);
47
- }
48
- if (typeof effectivePayload.asset === 'string' || isRecord(effectivePayload.asset)) {
49
- const a = effectivePayload.asset;
50
- const path = isRecord(a) ? (a.path || a.name) : a;
51
- parts.push(`Asset: ${path}`);
52
- }
53
- if (typeof effectivePayload.blueprint === 'string' || isRecord(effectivePayload.blueprint)) {
54
- const bp = effectivePayload.blueprint;
55
- const name = isRecord(bp) ? (bp.name || bp.path || effectivePayload.blueprintPath) : bp;
56
- parts.push(`Blueprint: ${name}`);
57
- }
58
- if (typeof effectivePayload.sequence === 'string' || isRecord(effectivePayload.sequence)) {
59
- const seq = effectivePayload.sequence;
60
- const name = isRecord(seq) ? (seq.name || seq.path) : seq;
61
- parts.push(`Sequence: ${name}`);
62
- }
63
- const usefulKeys = [
64
- 'success', 'error', 'message', 'assets', 'folders', 'count', 'totalCount',
65
- 'saved', 'valid', 'issues', 'class', 'skeleton', 'parent',
66
- 'package', 'dependencies', 'graph', 'tags', 'metadata', 'properties'
67
- ];
68
- for (const key of usefulKeys) {
69
- if (effectivePayload[key] !== undefined && effectivePayload[key] !== null) {
70
- const val = effectivePayload[key];
71
- if (typeof val === 'object') {
72
- if (key === 'metadata' || key === 'properties' || key === 'tags') {
73
- const entries = Object.entries(val);
74
- const formatted = entries.map(([k, v]) => `${k}=${v}`);
75
- const limit = 50;
76
- parts.push(`${key}: { ${formatted.slice(0, limit).join(', ')}${formatted.length > limit ? '...' : ''} }`);
77
- continue;
32
+ if (isRecord(obj.result)) {
33
+ Object.assign(obj, obj.result);
34
+ delete obj.result;
35
+ flattenWrappers(obj, depth + 1);
36
+ }
37
+ };
38
+ flattenWrappers(effectivePayload);
39
+ const parts = [];
40
+ const addedKeys = new Set();
41
+ const skipKeys = new Set(['requestId', 'type', 'data', 'result', 'warnings']);
42
+ const formatValue = (val) => {
43
+ if (val === null || val === undefined)
44
+ return '';
45
+ if (typeof val === 'string')
46
+ return val.length > 150 ? val.slice(0, 150) + '...' : val;
47
+ if (typeof val === 'number' || typeof val === 'boolean')
48
+ return String(val);
49
+ if (Array.isArray(val)) {
50
+ if (val.length === 0)
51
+ return '[] (0)';
52
+ const items = val.slice(0, 30).map(v => {
53
+ if (isRecord(v)) {
54
+ return v.name || v.path || v.id || v.nodeId || v.nodeName || v.className ||
55
+ v.displayName || v.type || v.assetPath || v.objectPath ||
56
+ JSON.stringify(v).slice(0, 50);
78
57
  }
79
- continue;
58
+ return String(v);
59
+ });
60
+ const suffix = val.length > 30 ? `, ... (+${val.length - 30} more)` : '';
61
+ return `[${items.join(', ')}${suffix}] (${val.length})`;
62
+ }
63
+ if (isRecord(val)) {
64
+ const keys = Object.keys(val);
65
+ if (keys.some(k => ['x', 'y', 'z', 'pitch', 'yaw', 'roll'].includes(k))) {
66
+ const x = val.x ?? val.pitch ?? 0;
67
+ const y = val.y ?? val.yaw ?? 0;
68
+ const z = val.z ?? val.roll ?? 0;
69
+ return `[${x}, ${y}, ${z}]`;
80
70
  }
81
- const strVal = String(val);
82
- if (strVal.length > 100)
83
- continue;
84
- if (!parts.some(p => p.includes(strVal))) {
85
- parts.push(`${key}: ${strVal}`);
71
+ const entries = Object.entries(val).slice(0, 8);
72
+ const formatted = entries.map(([k, v]) => {
73
+ const vStr = typeof v === 'object' ? JSON.stringify(v).slice(0, 40) : String(v);
74
+ return `${k}=${vStr}`;
75
+ });
76
+ return `{ ${formatted.join(', ')}${keys.length > 8 ? ' ...' : ''} }`;
77
+ }
78
+ return String(val);
79
+ };
80
+ for (const key of ['success', 'error']) {
81
+ if (effectivePayload[key] !== undefined && !addedKeys.has(key)) {
82
+ const formatted = formatValue(effectivePayload[key]);
83
+ if (formatted) {
84
+ parts.push(`${key}: ${formatted}`);
85
+ addedKeys.add(key);
86
86
  }
87
87
  }
88
88
  }
89
- const success = typeof payload.success === 'boolean' ? payload.success : undefined;
90
- const message = typeof payload.message === 'string' ? normalizeText(payload.message) : '';
91
- const error = typeof payload.error === 'string' ? normalizeText(payload.error) : '';
92
- if (parts.length > 0) {
93
- if (message && message.toLowerCase() !== 'success') {
94
- parts.push(message);
89
+ let hasArrays = false;
90
+ for (const [key, val] of Object.entries(effectivePayload)) {
91
+ if (addedKeys.has(key))
92
+ continue;
93
+ if (skipKeys.has(key))
94
+ continue;
95
+ if (val === undefined || val === null)
96
+ continue;
97
+ if (typeof val === 'string' && val.trim() === '')
98
+ continue;
99
+ if (key === 'message')
100
+ continue;
101
+ if (Array.isArray(val) && val.length > 0)
102
+ hasArrays = true;
103
+ if ((key === 'count' || key === 'totalCount') && hasArrays)
104
+ continue;
105
+ const formatted = formatValue(val);
106
+ if (formatted) {
107
+ parts.push(`${key}: ${formatted}`);
108
+ addedKeys.add(key);
95
109
  }
96
110
  }
97
- else {
98
- if (message)
111
+ const message = typeof effectivePayload.message === 'string' ? normalizeText(effectivePayload.message) : '';
112
+ if (message && message.toLowerCase() !== 'success') {
113
+ const isDuplicateInfo = /^(found|listed|retrieved|got|loaded|created|deleted|saved|spawned)\s+\d+/i.test(message) ||
114
+ /Folders:\s*\[/.test(message) ||
115
+ /\d+\s+(assets?|folders?|items?|actors?|components?)\s+(and|in|at)/i.test(message);
116
+ const messageInParts = parts.some(p => p.toLowerCase().includes(message.toLowerCase().slice(0, 30)));
117
+ if (!isDuplicateInfo && !messageInParts) {
99
118
  parts.push(message);
100
- if (error)
101
- parts.push(`Error: ${error}`);
102
- if (parts.length === 0 && success !== undefined) {
103
- parts.push(success ? 'Success' : 'Failed');
104
119
  }
105
120
  }
106
- const warnings = Array.isArray(payload.warnings) ? payload.warnings : [];
121
+ const warnings = Array.isArray(effectivePayload.warnings) ? effectivePayload.warnings : [];
107
122
  if (warnings.length > 0) {
108
- parts.push(`Warnings: ${warnings.length}`);
123
+ parts.push(`Warnings: ${warnings.map((w) => typeof w === 'string' ? w : JSON.stringify(w)).join('; ')}`);
109
124
  }
110
125
  return parts.length > 0 ? parts.join(' | ') : `${toolName} responded`;
111
126
  }
@@ -11,6 +11,7 @@ export declare class UnrealCommandQueue {
11
11
  private isProcessing;
12
12
  private lastCommandTime;
13
13
  private lastStatCommandTime;
14
+ private processorInterval?;
14
15
  private readonly MIN_COMMAND_DELAY;
15
16
  private readonly MAX_COMMAND_DELAY;
16
17
  private readonly STAT_COMMAND_DELAY;
@@ -19,6 +20,7 @@ export declare class UnrealCommandQueue {
19
20
  private processQueue;
20
21
  private calculateDelay;
21
22
  private startProcessor;
23
+ stopProcessor(): void;
22
24
  private delay;
23
25
  }
24
26
  //# sourceMappingURL=unreal-command-queue.d.ts.map
@@ -5,6 +5,7 @@ export class UnrealCommandQueue {
5
5
  isProcessing = false;
6
6
  lastCommandTime = 0;
7
7
  lastStatCommandTime = 0;
8
+ processorInterval;
8
9
  MIN_COMMAND_DELAY = 100;
9
10
  MAX_COMMAND_DELAY = 500;
10
11
  STAT_COMMAND_DELAY = 300;
@@ -107,12 +108,18 @@ export class UnrealCommandQueue {
107
108
  }
108
109
  }
109
110
  startProcessor() {
110
- setInterval(() => {
111
+ this.processorInterval = setInterval(() => {
111
112
  if (!this.isProcessing && this.queue.length > 0) {
112
113
  this.processQueue();
113
114
  }
114
115
  }, 1000);
115
116
  }
117
+ stopProcessor() {
118
+ if (this.processorInterval) {
119
+ clearInterval(this.processorInterval);
120
+ this.processorInterval = undefined;
121
+ }
122
+ }
116
123
  delay(ms) {
117
124
  return new Promise(resolve => setTimeout(resolve, ms));
118
125
  }
@@ -652,18 +652,10 @@ echo $WASM_ENABLED # Should be "true"
652
652
  ### Documentation
653
653
  - [GraphQL API Documentation](GraphQL-API.md)
654
654
  - [WebAssembly Integration Guide](WebAssembly-Integration.md)
655
- - Added **GraphQL API** (optional) for complex queries.
656
- - Disabled by default (`GRAPHQL_ENABLED=false`).
657
- - Uses Apollo Server.
658
- - Integrated with **Apollo Sandbox** (local IDE) or [Apollo Studio](https://studio.apollographql.com). - GraphQL platform
659
- - [Postman](https://www.postman.com/) - API testing
660
-
661
- ### Learning
662
- - [GraphQL.org](https://graphql.org/) - Official GraphQL documentation
663
- - [WebAssembly.org](https://webassembly.org/) - WebAssembly specification
664
655
 
665
656
  ### Tools
666
657
  - [GraphiQL](https://github.com/graphql/graphiql) - In-browser GraphQL IDE
658
+ - [Apollo Studio](https://studio.apollographql.com) - GraphQL platform
667
659
  - [Postman](https://www.postman.com/) - API testing
668
660
 
669
661
  ### Learning