unreal-engine-mcp-server 0.4.7 → 0.5.1

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 (454) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter-config.yml +51 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +27 -0
  19. package/.github/workflows/labeler.yml +17 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +13 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +338 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/GEMINI.md +115 -0
  31. package/Public/Plugin_setup_guide.mp4 +0 -0
  32. package/README.md +189 -128
  33. package/claude_desktop_config_example.json +7 -6
  34. package/dist/automation/bridge.d.ts +50 -0
  35. package/dist/automation/bridge.js +452 -0
  36. package/dist/automation/connection-manager.d.ts +23 -0
  37. package/dist/automation/connection-manager.js +107 -0
  38. package/dist/automation/handshake.d.ts +11 -0
  39. package/dist/automation/handshake.js +89 -0
  40. package/dist/automation/index.d.ts +3 -0
  41. package/dist/automation/index.js +3 -0
  42. package/dist/automation/message-handler.d.ts +12 -0
  43. package/dist/automation/message-handler.js +149 -0
  44. package/dist/automation/request-tracker.d.ts +25 -0
  45. package/dist/automation/request-tracker.js +98 -0
  46. package/dist/automation/types.d.ts +130 -0
  47. package/dist/automation/types.js +2 -0
  48. package/dist/cli.js +32 -5
  49. package/dist/config.d.ts +26 -0
  50. package/dist/config.js +59 -0
  51. package/dist/constants.d.ts +16 -0
  52. package/dist/constants.js +16 -0
  53. package/dist/graphql/loaders.d.ts +64 -0
  54. package/dist/graphql/loaders.js +117 -0
  55. package/dist/graphql/resolvers.d.ts +268 -0
  56. package/dist/graphql/resolvers.js +746 -0
  57. package/dist/graphql/schema.d.ts +5 -0
  58. package/dist/graphql/schema.js +437 -0
  59. package/dist/graphql/server.d.ts +26 -0
  60. package/dist/graphql/server.js +117 -0
  61. package/dist/graphql/types.d.ts +9 -0
  62. package/dist/graphql/types.js +2 -0
  63. package/dist/handlers/resource-handlers.d.ts +20 -0
  64. package/dist/handlers/resource-handlers.js +180 -0
  65. package/dist/index.d.ts +33 -18
  66. package/dist/index.js +130 -619
  67. package/dist/resources/actors.d.ts +17 -12
  68. package/dist/resources/actors.js +56 -76
  69. package/dist/resources/assets.d.ts +6 -14
  70. package/dist/resources/assets.js +115 -147
  71. package/dist/resources/levels.d.ts +13 -13
  72. package/dist/resources/levels.js +25 -34
  73. package/dist/server/resource-registry.d.ts +20 -0
  74. package/dist/server/resource-registry.js +37 -0
  75. package/dist/server/tool-registry.d.ts +23 -0
  76. package/dist/server/tool-registry.js +322 -0
  77. package/dist/server-setup.d.ts +20 -0
  78. package/dist/server-setup.js +71 -0
  79. package/dist/services/health-monitor.d.ts +34 -0
  80. package/dist/services/health-monitor.js +105 -0
  81. package/dist/services/metrics-server.d.ts +11 -0
  82. package/dist/services/metrics-server.js +105 -0
  83. package/dist/tools/actors.d.ts +163 -9
  84. package/dist/tools/actors.js +356 -311
  85. package/dist/tools/animation.d.ts +135 -4
  86. package/dist/tools/animation.js +510 -411
  87. package/dist/tools/assets.d.ts +75 -29
  88. package/dist/tools/assets.js +265 -284
  89. package/dist/tools/audio.d.ts +102 -42
  90. package/dist/tools/audio.js +272 -685
  91. package/dist/tools/base-tool.d.ts +17 -0
  92. package/dist/tools/base-tool.js +46 -0
  93. package/dist/tools/behavior-tree.d.ts +94 -0
  94. package/dist/tools/behavior-tree.js +39 -0
  95. package/dist/tools/blueprint.d.ts +208 -126
  96. package/dist/tools/blueprint.js +685 -832
  97. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  98. package/dist/tools/consolidated-tool-definitions.js +829 -496
  99. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  100. package/dist/tools/consolidated-tool-handlers.js +198 -1027
  101. package/dist/tools/debug.d.ts +143 -85
  102. package/dist/tools/debug.js +234 -180
  103. package/dist/tools/dynamic-handler-registry.d.ts +13 -0
  104. package/dist/tools/dynamic-handler-registry.js +23 -0
  105. package/dist/tools/editor.d.ts +30 -83
  106. package/dist/tools/editor.js +247 -244
  107. package/dist/tools/engine.d.ts +10 -4
  108. package/dist/tools/engine.js +13 -5
  109. package/dist/tools/environment.d.ts +30 -0
  110. package/dist/tools/environment.js +267 -0
  111. package/dist/tools/foliage.d.ts +65 -99
  112. package/dist/tools/foliage.js +221 -331
  113. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  114. package/dist/tools/handlers/actor-handlers.js +227 -0
  115. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  116. package/dist/tools/handlers/animation-handlers.js +185 -0
  117. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  118. package/dist/tools/handlers/argument-helper.js +80 -0
  119. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  120. package/dist/tools/handlers/asset-handlers.js +496 -0
  121. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  122. package/dist/tools/handlers/audio-handlers.js +166 -0
  123. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  124. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  125. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  126. package/dist/tools/handlers/common-handlers.js +56 -0
  127. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  128. package/dist/tools/handlers/editor-handlers.js +119 -0
  129. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  130. package/dist/tools/handlers/effect-handlers.js +171 -0
  131. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  132. package/dist/tools/handlers/environment-handlers.js +170 -0
  133. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  134. package/dist/tools/handlers/graph-handlers.js +90 -0
  135. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  136. package/dist/tools/handlers/input-handlers.js +21 -0
  137. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  138. package/dist/tools/handlers/inspect-handlers.js +383 -0
  139. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  140. package/dist/tools/handlers/level-handlers.js +237 -0
  141. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  142. package/dist/tools/handlers/lighting-handlers.js +144 -0
  143. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  144. package/dist/tools/handlers/performance-handlers.js +130 -0
  145. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  146. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  147. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  148. package/dist/tools/handlers/sequence-handlers.js +376 -0
  149. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  150. package/dist/tools/handlers/system-handlers.js +506 -0
  151. package/dist/tools/input.d.ts +19 -0
  152. package/dist/tools/input.js +89 -0
  153. package/dist/tools/introspection.d.ts +103 -40
  154. package/dist/tools/introspection.js +425 -568
  155. package/dist/tools/landscape.d.ts +54 -93
  156. package/dist/tools/landscape.js +284 -409
  157. package/dist/tools/level.d.ts +66 -27
  158. package/dist/tools/level.js +647 -675
  159. package/dist/tools/lighting.d.ts +77 -38
  160. package/dist/tools/lighting.js +445 -943
  161. package/dist/tools/logs.d.ts +3 -3
  162. package/dist/tools/logs.js +5 -57
  163. package/dist/tools/materials.d.ts +91 -24
  164. package/dist/tools/materials.js +194 -118
  165. package/dist/tools/niagara.d.ts +149 -39
  166. package/dist/tools/niagara.js +267 -182
  167. package/dist/tools/performance.d.ts +27 -13
  168. package/dist/tools/performance.js +203 -122
  169. package/dist/tools/physics.d.ts +32 -77
  170. package/dist/tools/physics.js +175 -582
  171. package/dist/tools/property-dictionary.d.ts +13 -0
  172. package/dist/tools/property-dictionary.js +82 -0
  173. package/dist/tools/sequence.d.ts +85 -60
  174. package/dist/tools/sequence.js +208 -747
  175. package/dist/tools/tool-definition-utils.d.ts +59 -0
  176. package/dist/tools/tool-definition-utils.js +35 -0
  177. package/dist/tools/ui.d.ts +64 -34
  178. package/dist/tools/ui.js +134 -214
  179. package/dist/types/automation-responses.d.ts +115 -0
  180. package/dist/types/automation-responses.js +2 -0
  181. package/dist/types/env.d.ts +0 -3
  182. package/dist/types/env.js +0 -7
  183. package/dist/types/responses.d.ts +249 -0
  184. package/dist/types/responses.js +2 -0
  185. package/dist/types/tool-interfaces.d.ts +898 -0
  186. package/dist/types/tool-interfaces.js +2 -0
  187. package/dist/types/tool-types.d.ts +183 -19
  188. package/dist/types/tool-types.js +0 -4
  189. package/dist/unreal-bridge.d.ts +24 -131
  190. package/dist/unreal-bridge.js +364 -1506
  191. package/dist/utils/command-validator.d.ts +9 -0
  192. package/dist/utils/command-validator.js +68 -0
  193. package/dist/utils/elicitation.d.ts +1 -1
  194. package/dist/utils/elicitation.js +12 -15
  195. package/dist/utils/error-handler.d.ts +2 -51
  196. package/dist/utils/error-handler.js +11 -87
  197. package/dist/utils/ini-reader.d.ts +3 -0
  198. package/dist/utils/ini-reader.js +69 -0
  199. package/dist/utils/logger.js +9 -6
  200. package/dist/utils/normalize.d.ts +3 -0
  201. package/dist/utils/normalize.js +56 -0
  202. package/dist/utils/path-security.d.ts +2 -0
  203. package/dist/utils/path-security.js +24 -0
  204. package/dist/utils/response-factory.d.ts +7 -0
  205. package/dist/utils/response-factory.js +27 -0
  206. package/dist/utils/response-validator.d.ts +3 -24
  207. package/dist/utils/response-validator.js +130 -81
  208. package/dist/utils/result-helpers.d.ts +4 -5
  209. package/dist/utils/result-helpers.js +15 -16
  210. package/dist/utils/safe-json.js +5 -11
  211. package/dist/utils/unreal-command-queue.d.ts +24 -0
  212. package/dist/utils/unreal-command-queue.js +120 -0
  213. package/dist/utils/validation.d.ts +0 -40
  214. package/dist/utils/validation.js +1 -78
  215. package/dist/wasm/index.d.ts +70 -0
  216. package/dist/wasm/index.js +535 -0
  217. package/docs/GraphQL-API.md +888 -0
  218. package/docs/Migration-Guide-v0.5.0.md +684 -0
  219. package/docs/Roadmap.md +53 -0
  220. package/docs/WebAssembly-Integration.md +628 -0
  221. package/docs/editor-plugin-extension.md +370 -0
  222. package/docs/handler-mapping.md +242 -0
  223. package/docs/native-automation-progress.md +128 -0
  224. package/docs/testing-guide.md +423 -0
  225. package/mcp-config-example.json +6 -6
  226. package/package.json +67 -28
  227. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  228. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  272. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  273. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  274. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  275. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  276. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  277. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  278. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  279. package/scripts/check-unreal-connection.mjs +19 -0
  280. package/scripts/clean-tmp.js +23 -0
  281. package/scripts/patch-wasm.js +26 -0
  282. package/scripts/run-all-tests.mjs +136 -0
  283. package/scripts/smoke-test.ts +94 -0
  284. package/scripts/sync-mcp-plugin.js +143 -0
  285. package/scripts/test-no-plugin-alternates.mjs +113 -0
  286. package/scripts/validate-server.js +46 -0
  287. package/scripts/verify-automation-bridge.js +200 -0
  288. package/server.json +58 -21
  289. package/src/automation/bridge.ts +558 -0
  290. package/src/automation/connection-manager.ts +130 -0
  291. package/src/automation/handshake.ts +99 -0
  292. package/src/automation/index.ts +2 -0
  293. package/src/automation/message-handler.ts +167 -0
  294. package/src/automation/request-tracker.ts +123 -0
  295. package/src/automation/types.ts +107 -0
  296. package/src/cli.ts +33 -6
  297. package/src/config.ts +73 -0
  298. package/src/constants.ts +19 -0
  299. package/src/graphql/loaders.ts +244 -0
  300. package/src/graphql/resolvers.ts +1008 -0
  301. package/src/graphql/schema.ts +452 -0
  302. package/src/graphql/server.ts +156 -0
  303. package/src/graphql/types.ts +10 -0
  304. package/src/handlers/resource-handlers.ts +186 -0
  305. package/src/index.ts +166 -664
  306. package/src/resources/actors.ts +58 -76
  307. package/src/resources/assets.ts +148 -134
  308. package/src/resources/levels.ts +28 -33
  309. package/src/server/resource-registry.ts +47 -0
  310. package/src/server/tool-registry.ts +354 -0
  311. package/src/server-setup.ts +114 -0
  312. package/src/services/health-monitor.ts +132 -0
  313. package/src/services/metrics-server.ts +142 -0
  314. package/src/tools/actors.ts +426 -323
  315. package/src/tools/animation.ts +672 -461
  316. package/src/tools/assets.ts +364 -289
  317. package/src/tools/audio.ts +323 -766
  318. package/src/tools/base-tool.ts +52 -0
  319. package/src/tools/behavior-tree.ts +45 -0
  320. package/src/tools/blueprint.ts +792 -970
  321. package/src/tools/consolidated-tool-definitions.ts +993 -515
  322. package/src/tools/consolidated-tool-handlers.ts +258 -1146
  323. package/src/tools/debug.ts +292 -187
  324. package/src/tools/dynamic-handler-registry.ts +33 -0
  325. package/src/tools/editor.ts +329 -253
  326. package/src/tools/engine.ts +14 -3
  327. package/src/tools/environment.ts +281 -0
  328. package/src/tools/foliage.ts +330 -392
  329. package/src/tools/handlers/actor-handlers.ts +265 -0
  330. package/src/tools/handlers/animation-handlers.ts +237 -0
  331. package/src/tools/handlers/argument-helper.ts +142 -0
  332. package/src/tools/handlers/asset-handlers.ts +532 -0
  333. package/src/tools/handlers/audio-handlers.ts +194 -0
  334. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  335. package/src/tools/handlers/common-handlers.ts +87 -0
  336. package/src/tools/handlers/editor-handlers.ts +123 -0
  337. package/src/tools/handlers/effect-handlers.ts +220 -0
  338. package/src/tools/handlers/environment-handlers.ts +183 -0
  339. package/src/tools/handlers/graph-handlers.ts +116 -0
  340. package/src/tools/handlers/input-handlers.ts +28 -0
  341. package/src/tools/handlers/inspect-handlers.ts +450 -0
  342. package/src/tools/handlers/level-handlers.ts +252 -0
  343. package/src/tools/handlers/lighting-handlers.ts +147 -0
  344. package/src/tools/handlers/performance-handlers.ts +132 -0
  345. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  346. package/src/tools/handlers/sequence-handlers.ts +415 -0
  347. package/src/tools/handlers/system-handlers.ts +564 -0
  348. package/src/tools/input.ts +101 -0
  349. package/src/tools/introspection.ts +493 -584
  350. package/src/tools/landscape.ts +418 -507
  351. package/src/tools/level.ts +786 -708
  352. package/src/tools/lighting.ts +588 -984
  353. package/src/tools/logs.ts +9 -57
  354. package/src/tools/materials.ts +237 -121
  355. package/src/tools/niagara.ts +335 -168
  356. package/src/tools/performance.ts +320 -169
  357. package/src/tools/physics.ts +274 -613
  358. package/src/tools/property-dictionary.ts +98 -0
  359. package/src/tools/sequence.ts +276 -820
  360. package/src/tools/tool-definition-utils.ts +35 -0
  361. package/src/tools/ui.ts +205 -283
  362. package/src/types/automation-responses.ts +119 -0
  363. package/src/types/env.ts +0 -10
  364. package/src/types/responses.ts +355 -0
  365. package/src/types/tool-interfaces.ts +250 -0
  366. package/src/types/tool-types.ts +243 -21
  367. package/src/unreal-bridge.ts +460 -1550
  368. package/src/utils/command-validator.ts +76 -0
  369. package/src/utils/elicitation.ts +10 -7
  370. package/src/utils/error-handler.ts +14 -90
  371. package/src/utils/ini-reader.ts +86 -0
  372. package/src/utils/logger.ts +8 -3
  373. package/src/utils/normalize.test.ts +162 -0
  374. package/src/utils/normalize.ts +60 -0
  375. package/src/utils/path-security.ts +43 -0
  376. package/src/utils/response-factory.ts +44 -0
  377. package/src/utils/response-validator.ts +176 -56
  378. package/src/utils/result-helpers.ts +21 -19
  379. package/src/utils/safe-json.test.ts +90 -0
  380. package/src/utils/safe-json.ts +14 -11
  381. package/src/utils/unreal-command-queue.ts +152 -0
  382. package/src/utils/validation.test.ts +184 -0
  383. package/src/utils/validation.ts +4 -1
  384. package/src/wasm/index.ts +838 -0
  385. package/test-server.mjs +100 -0
  386. package/tests/run-unreal-tool-tests.mjs +242 -14
  387. package/tests/test-animation.mjs +369 -0
  388. package/tests/test-asset-advanced.mjs +82 -0
  389. package/tests/test-asset-errors.mjs +35 -0
  390. package/tests/test-asset-graph.mjs +311 -0
  391. package/tests/test-audio.mjs +417 -0
  392. package/tests/test-automation-timeouts.mjs +98 -0
  393. package/tests/test-behavior-tree.mjs +444 -0
  394. package/tests/test-blueprint-graph.mjs +410 -0
  395. package/tests/test-blueprint.mjs +577 -0
  396. package/tests/test-client-mode.mjs +86 -0
  397. package/tests/test-console-command.mjs +56 -0
  398. package/tests/test-control-actor.mjs +425 -0
  399. package/tests/test-control-editor.mjs +112 -0
  400. package/tests/test-graphql.mjs +372 -0
  401. package/tests/test-input.mjs +349 -0
  402. package/tests/test-inspect.mjs +302 -0
  403. package/tests/test-landscape.mjs +316 -0
  404. package/tests/test-lighting.mjs +428 -0
  405. package/tests/test-manage-asset.mjs +438 -0
  406. package/tests/test-manage-level.mjs +89 -0
  407. package/tests/test-materials.mjs +356 -0
  408. package/tests/test-niagara.mjs +185 -0
  409. package/tests/test-no-inline-python.mjs +122 -0
  410. package/tests/test-performance.mjs +539 -0
  411. package/tests/test-plugin-handshake.mjs +82 -0
  412. package/tests/test-runner.mjs +933 -0
  413. package/tests/test-sequence.mjs +104 -0
  414. package/tests/test-system.mjs +96 -0
  415. package/tests/test-wasm.mjs +283 -0
  416. package/tests/test-world-partition.mjs +215 -0
  417. package/tsconfig.json +3 -3
  418. package/vitest.config.ts +35 -0
  419. package/wasm/Cargo.lock +363 -0
  420. package/wasm/Cargo.toml +42 -0
  421. package/wasm/LICENSE +21 -0
  422. package/wasm/README.md +253 -0
  423. package/wasm/src/dependency_resolver.rs +377 -0
  424. package/wasm/src/lib.rs +153 -0
  425. package/wasm/src/property_parser.rs +271 -0
  426. package/wasm/src/transform_math.rs +396 -0
  427. package/wasm/tests/integration.rs +109 -0
  428. package/.github/workflows/smithery-build.yml +0 -29
  429. package/dist/prompts/index.d.ts +0 -21
  430. package/dist/prompts/index.js +0 -217
  431. package/dist/tools/build_environment_advanced.d.ts +0 -65
  432. package/dist/tools/build_environment_advanced.js +0 -633
  433. package/dist/tools/rc.d.ts +0 -110
  434. package/dist/tools/rc.js +0 -437
  435. package/dist/tools/visual.d.ts +0 -40
  436. package/dist/tools/visual.js +0 -282
  437. package/dist/utils/http.d.ts +0 -6
  438. package/dist/utils/http.js +0 -151
  439. package/dist/utils/python-output.d.ts +0 -18
  440. package/dist/utils/python-output.js +0 -290
  441. package/dist/utils/python.d.ts +0 -2
  442. package/dist/utils/python.js +0 -4
  443. package/dist/utils/stdio-redirect.d.ts +0 -2
  444. package/dist/utils/stdio-redirect.js +0 -20
  445. package/docs/unreal-tool-test-cases.md +0 -574
  446. package/smithery.yaml +0 -29
  447. package/src/prompts/index.ts +0 -249
  448. package/src/tools/build_environment_advanced.ts +0 -732
  449. package/src/tools/rc.ts +0 -515
  450. package/src/tools/visual.ts +0 -281
  451. package/src/utils/http.ts +0 -187
  452. package/src/utils/python-output.ts +0 -351
  453. package/src/utils/python.ts +0 -3
  454. package/src/utils/stdio-redirect.ts +0 -18
@@ -0,0 +1,76 @@
1
+ export class CommandValidator {
2
+ private static readonly DANGEROUS_COMMANDS = [
3
+ 'quit', 'exit', 'delete', 'destroy', 'kill', 'crash',
4
+ 'viewmode visualizebuffer basecolor',
5
+ 'viewmode visualizebuffer worldnormal',
6
+ 'r.gpucrash',
7
+ 'buildpaths', // Can cause access violation if nav system not initialized
8
+ 'rebuildnavigation', // Can also crash without proper nav setup
9
+ 'obj garbage', 'obj list', 'memreport' // Heavy debug commands that can stall
10
+ ];
11
+
12
+ private static readonly FORBIDDEN_TOKENS = [
13
+ 'rm ', 'rm-', 'del ', 'format ', 'shutdown', 'reboot',
14
+ 'rmdir', 'mklink', 'copy ', 'move ', 'start "', 'system(',
15
+ 'import os', 'import subprocess', 'subprocess.', 'os.system',
16
+ 'exec(', 'eval(', '__import__', 'import sys', 'import importlib',
17
+ 'with open', 'open(', 'write(', 'read('
18
+ ];
19
+
20
+ private static readonly INVALID_PATTERNS = [
21
+ /^\d+$/, // Just numbers
22
+ /^invalid_command/i,
23
+ /^this_is_not_a_valid/i,
24
+ ];
25
+
26
+ static validate(command: string): void {
27
+ if (!command || typeof command !== 'string') {
28
+ throw new Error('Invalid command: must be a non-empty string');
29
+ }
30
+
31
+ const cmdTrimmed = command.trim();
32
+ if (cmdTrimmed.length === 0) {
33
+ return; // Empty commands are technically valid (no-op)
34
+ }
35
+
36
+ if (cmdTrimmed.includes('\n') || cmdTrimmed.includes('\r')) {
37
+ throw new Error('Multi-line console commands are not allowed. Send one command per call.');
38
+ }
39
+
40
+ const cmdLower = cmdTrimmed.toLowerCase();
41
+
42
+ if (cmdLower === 'py' || cmdLower.startsWith('py ')) {
43
+ throw new Error('Python console commands are blocked from external calls for safety.');
44
+ }
45
+
46
+ if (this.DANGEROUS_COMMANDS.some(dangerous => cmdLower.includes(dangerous))) {
47
+ throw new Error(`Dangerous command blocked: ${command}`);
48
+ }
49
+
50
+ if (cmdLower.includes('&&') || cmdLower.includes('||')) {
51
+ throw new Error('Command chaining with && or || is blocked for safety.');
52
+ }
53
+
54
+ if (this.FORBIDDEN_TOKENS.some(token => cmdLower.includes(token))) {
55
+ throw new Error(`Command contains unsafe token and was blocked: ${command}`);
56
+ }
57
+ }
58
+
59
+ static isLikelyInvalid(command: string): boolean {
60
+ const cmdTrimmed = command.trim();
61
+ return this.INVALID_PATTERNS.some(pattern => pattern.test(cmdTrimmed));
62
+ }
63
+
64
+ static getPriority(command: string): number {
65
+ if (command.includes('BuildLighting') || command.includes('BuildPaths')) {
66
+ return 1; // Heavy operation
67
+ } else if (command.includes('summon') || command.includes('spawn')) {
68
+ return 5; // Medium operation
69
+ } else if (command.startsWith('stat')) {
70
+ return 8; // Dedicated throttling for stat commands
71
+ } else if (command.startsWith('show')) {
72
+ return 9; // Light operation
73
+ }
74
+ return 7; // Default priority
75
+ }
76
+ }
@@ -2,7 +2,7 @@ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { Logger } from './logger.js';
3
3
 
4
4
  // Minimal helper to opportunistically use MCP Elicitation when available.
5
- // Safe across clients: validates schema shape, handles timeouts and -32601 fallbacks.
5
+ // Safe across clients: validates schema shape and handles timeouts and -32601 errors.
6
6
  export type PrimitiveSchema =
7
7
  | { type: 'string'; title?: string; description?: string; minLength?: number; maxLength?: number; pattern?: string; format?: 'email'|'uri'|'date'|'date-time'; default?: string }
8
8
  | { type: 'number'|'integer'; title?: string; description?: string; minimum?: number; maximum?: number; default?: number }
@@ -17,7 +17,9 @@ export interface ElicitSchema {
17
17
 
18
18
  export interface ElicitOptions {
19
19
  timeoutMs?: number;
20
- fallback?: () => Promise<{ ok: boolean; value?: any; error?: string }>;
20
+ // Handler invoked when elicitation cannot be performed; previously named
21
+ // Handler invoked when elicitation cannot be performed.
22
+ alternate?: () => Promise<{ ok: boolean; value?: any; error?: string }>;
21
23
  }
22
24
 
23
25
  export function createElicitationHelper(server: Server, log: Logger) {
@@ -77,7 +79,7 @@ export function createElicitationHelper(server: Server, log: Logger) {
77
79
 
78
80
  async function elicit(message: string, requestedSchema: ElicitSchema, opts: ElicitOptions = {}) {
79
81
  if (!supported || !isSafeSchema(requestedSchema)) {
80
- if (opts.fallback) return opts.fallback();
82
+ if (opts.alternate) return opts.alternate();
81
83
  return { ok: false, error: 'elicitation-unsupported' };
82
84
  }
83
85
 
@@ -98,10 +100,10 @@ export function createElicitationHelper(server: Server, log: Logger) {
98
100
 
99
101
  if (action === 'accept') return { ok: true, value: content };
100
102
  if (action === 'decline' || action === 'cancel') {
101
- if (opts.fallback) return opts.fallback();
103
+ if (opts.alternate) return opts.alternate();
102
104
  return { ok: false, error: action };
103
105
  }
104
- if (opts.fallback) return opts.fallback();
106
+ if (opts.alternate) return opts.alternate();
105
107
  return { ok: false, error: 'unexpected-response' };
106
108
  } catch (e: any) {
107
109
  const msg = String(e?.message || e);
@@ -115,8 +117,9 @@ export function createElicitationHelper(server: Server, log: Logger) {
115
117
  ) {
116
118
  supported = false;
117
119
  }
118
- log.debug('Elicitation failed; falling back', { error: msg, code });
119
- if (opts.fallback) return opts.fallback();
120
+ // Use an alternate handler if provided when elicitation fails.
121
+ log.debug('Elicitation failed; using alternate handler', { error: msg, code });
122
+ if (opts.alternate) return opts.alternate();
120
123
  return { ok: false, error: msg.includes('timeout') ? 'timeout' : 'rpc-failed' };
121
124
  }
122
125
  }
@@ -95,7 +95,7 @@ export class ErrorHandler {
95
95
  // Unreal Engine specific errors
96
96
  if (
97
97
  errorMessage.includes('unreal') ||
98
- errorMessage.includes('remote control') ||
98
+ errorMessage.includes('connection failed') ||
99
99
  errorMessage.includes('blueprint') ||
100
100
  errorMessage.includes('actor') ||
101
101
  errorMessage.includes('asset')
@@ -128,7 +128,7 @@ export class ErrorHandler {
128
128
 
129
129
  switch (type) {
130
130
  case ErrorType.CONNECTION:
131
- return 'Failed to connect to Unreal Engine. Please ensure Remote Control is enabled and the engine is running.';
131
+ return 'Failed to connect to Unreal Engine. Please ensure the Automation Bridge plugin is active and the editor is running.';
132
132
 
133
133
  case ErrorType.VALIDATION:
134
134
  return `Invalid input: ${originalMessage}`;
@@ -164,114 +164,38 @@ export class ErrorHandler {
164
164
  }
165
165
 
166
166
  /**
167
- * Retry an async operation with exponential backoff
168
- * Best practice from TypeScript async programming patterns
169
- * @param operation - Async operation to retry
170
- * @param options - Retry configuration
171
- * @returns Result of the operation
167
+ * Retry a function with exponential backoff
172
168
  */
173
169
  static async retryWithBackoff<T>(
174
- operation: () => Promise<T>,
170
+ fn: () => Promise<T>,
175
171
  options: {
176
172
  maxRetries?: number;
177
173
  initialDelay?: number;
178
174
  maxDelay?: number;
179
175
  backoffMultiplier?: number;
180
- shouldRetry?: (error: unknown) => boolean;
176
+ shouldRetry?: (error: any) => boolean;
181
177
  } = {}
182
178
  ): Promise<T> {
183
- const {
184
- maxRetries = 3,
185
- initialDelay = 100,
186
- maxDelay = 10000,
187
- backoffMultiplier = 2,
188
- shouldRetry = (error) => this.isRetriable(error)
189
- } = options;
179
+ const maxRetries = options.maxRetries ?? 3;
180
+ const initialDelay = options.initialDelay ?? 1000;
181
+ const maxDelay = options.maxDelay ?? 10000;
182
+ const multiplier = options.backoffMultiplier ?? 2;
183
+ const shouldRetry = options.shouldRetry ?? ((err) => this.isRetriable(err));
190
184
 
191
- let lastError: unknown;
192
185
  let delay = initialDelay;
193
186
 
194
187
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
195
188
  try {
196
- return await operation();
189
+ return await fn();
197
190
  } catch (error) {
198
- lastError = error;
199
-
200
191
  if (attempt === maxRetries || !shouldRetry(error)) {
201
192
  throw error;
202
193
  }
203
-
204
- log.debug(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`);
194
+
205
195
  await new Promise(resolve => setTimeout(resolve, delay));
206
-
207
- delay = Math.min(delay * backoffMultiplier, maxDelay);
196
+ delay = Math.min(delay * multiplier, maxDelay);
208
197
  }
209
198
  }
210
-
211
- throw lastError;
212
- }
213
-
214
- /**
215
- * Add timeout to any promise
216
- * @param promise - Promise to add timeout to
217
- * @param timeoutMs - Timeout in milliseconds
218
- * @param errorMessage - Custom error message for timeout
219
- * @returns Promise that rejects on timeout
220
- */
221
- static async withTimeout<T>(
222
- promise: Promise<T>,
223
- timeoutMs: number,
224
- errorMessage = 'Operation timed out'
225
- ): Promise<T> {
226
- let timeoutHandle: NodeJS.Timeout | undefined;
227
-
228
- const timeoutPromise = new Promise<never>((_, reject) => {
229
- timeoutHandle = setTimeout(() => {
230
- reject(new Error(errorMessage));
231
- }, timeoutMs);
232
- });
233
-
234
- try {
235
- return await Promise.race([promise, timeoutPromise]);
236
- } finally {
237
- if (timeoutHandle !== undefined) {
238
- clearTimeout(timeoutHandle);
239
- }
240
- }
241
- }
242
-
243
- /**
244
- * Execute multiple operations with Promise.allSettled for better error handling
245
- * Returns detailed results for each operation, including failures
246
- * @param operations - Array of async operations to execute
247
- * @returns Object with successful and failed operations separated
248
- */
249
- static async batchExecute<T>(
250
- operations: Array<() => Promise<T>>
251
- ): Promise<{
252
- successful: Array<{ index: number; value: T }>;
253
- failed: Array<{ index: number; reason: unknown }>;
254
- successCount: number;
255
- failureCount: number;
256
- }> {
257
- const results = await Promise.allSettled(operations.map(op => op()));
258
-
259
- const successful: Array<{ index: number; value: T }> = [];
260
- const failed: Array<{ index: number; reason: unknown }> = [];
261
-
262
- results.forEach((result, index) => {
263
- if (result.status === 'fulfilled') {
264
- successful.push({ index, value: result.value });
265
- } else {
266
- failed.push({ index, reason: result.reason });
267
- }
268
- });
269
-
270
- return {
271
- successful,
272
- failed,
273
- successCount: successful.length,
274
- failureCount: failed.length
275
- };
199
+ throw new Error('Max retries exceeded');
276
200
  }
277
201
  }
@@ -0,0 +1,86 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ export async function readIniFile(filePath: string): Promise<Record<string, Record<string, string>>> {
5
+ try {
6
+ const content = await fs.readFile(filePath, 'utf-8');
7
+ const result: Record<string, Record<string, string>> = {};
8
+ let currentSection = '';
9
+
10
+ const lines = content.split(/\r?\n/);
11
+ for (const line of lines) {
12
+ const trimmed = line.trim();
13
+ if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) {
14
+ continue;
15
+ }
16
+
17
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
18
+ currentSection = trimmed.substring(1, trimmed.length - 1);
19
+ result[currentSection] = {};
20
+ } else if (currentSection) {
21
+ const parts = trimmed.split('=');
22
+ if (parts.length >= 2) {
23
+ const key = parts[0].trim();
24
+ const value = parts.slice(1).join('=').trim();
25
+ result[currentSection][key] = value;
26
+ }
27
+ }
28
+ }
29
+
30
+ return result;
31
+ } catch (error) {
32
+ throw new Error(`Failed to read INI file at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
33
+ }
34
+ }
35
+
36
+ export async function getProjectSetting(projectPath: string, category: string, sectionName: string, key?: string): Promise<any> {
37
+ // Normalize project path to directory
38
+ let dirPath = projectPath;
39
+ if (dirPath.toLowerCase().endsWith('.uproject')) {
40
+ dirPath = path.dirname(dirPath);
41
+ }
42
+
43
+ // Possible file names/locations in order of preference (Config/DefaultX.ini, Saved/Config/WindowsEditor/X.ini)
44
+ // category is usually 'Project', 'Engine', 'Game', 'Input', etc.
45
+ const cleanCategory = category.replace(/^Default/, ''); // If caller passed 'DefaultEngine', normalize to 'Engine'
46
+
47
+ const candidates = [
48
+ path.join(dirPath, 'Config', `Default${cleanCategory}.ini`),
49
+ path.join(dirPath, 'Saved', 'Config', 'WindowsEditor', `${cleanCategory}.ini`),
50
+ path.join(dirPath, 'Saved', 'Config', 'Windows', `${cleanCategory}.ini`),
51
+ path.join(dirPath, 'Saved', 'Config', 'Mac', `${cleanCategory}.ini`),
52
+ path.join(dirPath, 'Saved', 'Config', 'Linux', `${cleanCategory}.ini`)
53
+ ];
54
+
55
+ for (const configPath of candidates) {
56
+ try {
57
+ const iniData = await readIniFile(configPath);
58
+ // If we successfully read the file, check for the section
59
+ if (sectionName) {
60
+ const section = iniData[sectionName];
61
+ if (section) {
62
+ if (key) {
63
+ return section[key];
64
+ }
65
+ return section;
66
+ }
67
+ // If section not found in this file, continue to next candidate?
68
+ // Usually we want the most authoritative, but if it's missing the section, maybe it's in another?
69
+ // For now, if we find the file, we return the data from it or null if section missing.
70
+ // Merging is complex without a proper config hierarchy implementation.
71
+ // We will assume if the file exists, it's the one we want, or if section is missing, we fail for this file.
72
+ // But 'Default' might lack user overrides.
73
+ // Given this is a simple reader, we'll return the first match that contains the section,
74
+ // or if sectionName is empty, the first file found.
75
+ } else {
76
+ if (Object.keys(iniData).length > 0) {
77
+ return iniData;
78
+ }
79
+ }
80
+ } catch (_e) {
81
+ // Continue to next candidate
82
+ }
83
+ }
84
+
85
+ return null;
86
+ }
@@ -16,13 +16,18 @@ export class Logger {
16
16
  }
17
17
 
18
18
  debug(...args: any[]) {
19
- if (this.shouldLog('debug')) console.error(`[${this.scope}]`, ...args);
19
+ if (!this.shouldLog('debug')) return;
20
+ // Write to stderr to avoid corrupting MCP stdout stream
21
+ console.error(`[${this.scope}]`, ...args);
20
22
  }
21
23
  info(...args: any[]) {
22
- if (this.shouldLog('info')) console.error(`[${this.scope}]`, ...args);
24
+ if (!this.shouldLog('info')) return;
25
+ // Write to stderr to avoid corrupting MCP stdout stream
26
+ console.error(`[${this.scope}]`, ...args);
23
27
  }
24
28
  warn(...args: any[]) {
25
- if (this.shouldLog('warn')) console.error(`[${this.scope}]`, ...args);
29
+ if (!this.shouldLog('warn')) return;
30
+ console.warn(`[${this.scope}]`, ...args);
26
31
  }
27
32
  error(...args: any[]) {
28
33
  if (this.shouldLog('error')) console.error(`[${this.scope}]`, ...args);
@@ -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
+ });
@@ -62,3 +62,63 @@ export function toRotTuple(input: any): Rot3Tuple | null {
62
62
  return [pitch, yaw, roll];
63
63
  }
64
64
 
65
+ /**
66
+ * Parse a raw value into a finite number when possible.
67
+ * Accepts strings like "1.0" and returns number or undefined when invalid.
68
+ */
69
+ export function toFiniteNumber(raw: unknown): number | undefined {
70
+ if (typeof raw === 'number' && Number.isFinite(raw)) return raw;
71
+ if (typeof raw === 'string') {
72
+ const trimmed = raw.trim();
73
+ if (trimmed.length === 0) return undefined;
74
+ const parsed = Number(trimmed);
75
+ if (Number.isFinite(parsed)) return parsed;
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ /**
81
+ * Normalize a partial vector input. Unlike toVec3Object, this accepts
82
+ * partial specifications and returns an object containing only present
83
+ * components (x/y/z) when any are provided; otherwise returns undefined.
84
+ */
85
+ export function normalizePartialVector(value: any, alternateKeys: string[] = ['x', 'y', 'z']): Record<string, number> | undefined {
86
+ if (value === undefined || value === null) return undefined;
87
+ const result: Record<string, number> = {};
88
+ const assignIfPresent = (component: 'x' | 'y' | 'z', raw: unknown) => {
89
+ const num = toFiniteNumber(raw);
90
+ if (num !== undefined) result[component] = num;
91
+ };
92
+
93
+ if (Array.isArray(value)) {
94
+ if (value.length > 0) assignIfPresent('x', value[0]);
95
+ if (value.length > 1) assignIfPresent('y', value[1]);
96
+ if (value.length > 2) assignIfPresent('z', value[2]);
97
+ } else if (typeof value === 'object') {
98
+ const obj = value as Record<string, unknown>;
99
+ assignIfPresent('x', obj.x ?? obj[alternateKeys[0]]);
100
+ assignIfPresent('y', obj.y ?? obj[alternateKeys[1]]);
101
+ assignIfPresent('z', obj.z ?? obj[alternateKeys[2]]);
102
+ } else {
103
+ assignIfPresent('x', value);
104
+ }
105
+
106
+ return Object.keys(result).length > 0 ? result : undefined;
107
+ }
108
+
109
+ /**
110
+ * Normalize a transform-like input into a minimal object containing
111
+ * location/rotation/scale partial descriptors when present.
112
+ */
113
+ export function normalizeTransformInput(transform: any): Record<string, unknown> | undefined {
114
+ if (!transform || typeof transform !== 'object') return undefined;
115
+ const result: Record<string, unknown> = {};
116
+ const location = normalizePartialVector(transform.location);
117
+ if (location) result.location = location;
118
+ const rotation = normalizePartialVector(transform.rotation, ['pitch', 'yaw', 'roll']);
119
+ if (rotation) result.rotation = rotation;
120
+ const scale = normalizePartialVector(transform.scale);
121
+ if (scale) result.scale = scale;
122
+ return Object.keys(result).length > 0 ? result : undefined;
123
+ }
124
+
@@ -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
+ }