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,558 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { WebSocket } from 'ws';
3
+ import { Logger } from '../utils/logger.js';
4
+ import {
5
+ DEFAULT_AUTOMATION_HOST,
6
+ DEFAULT_AUTOMATION_PORT,
7
+ DEFAULT_NEGOTIATED_PROTOCOLS,
8
+ DEFAULT_HEARTBEAT_INTERVAL_MS,
9
+ DEFAULT_MAX_PENDING_REQUESTS
10
+ } from '../constants.js';
11
+ import { createRequire } from 'node:module';
12
+ import {
13
+ AutomationBridgeOptions,
14
+ AutomationBridgeStatus,
15
+ AutomationBridgeMessage,
16
+ AutomationBridgeResponseMessage,
17
+ AutomationBridgeEvents
18
+ } from './types.js';
19
+ import { ConnectionManager } from './connection-manager.js';
20
+ import { RequestTracker } from './request-tracker.js';
21
+ import { HandshakeHandler } from './handshake.js';
22
+ import { MessageHandler } from './message-handler.js';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const packageInfo: { name?: string; version?: string } = (() => {
26
+ try {
27
+ return require('../../package.json');
28
+ } catch (error) {
29
+ const log = new Logger('AutomationBridge');
30
+ log.debug('Unable to read package.json for version info', error);
31
+ return {};
32
+ }
33
+ })();
34
+
35
+ export class AutomationBridge extends EventEmitter {
36
+ private readonly host: string;
37
+ private readonly port: number;
38
+ private readonly ports: number[];
39
+ private readonly negotiatedProtocols: string[];
40
+ private readonly capabilityToken?: string;
41
+ private readonly enabled: boolean;
42
+ private readonly serverName: string;
43
+ private readonly serverVersion: string;
44
+ private readonly clientHost: string;
45
+ private readonly clientPort: number;
46
+ private readonly serverLegacyEnabled: boolean;
47
+ private readonly maxConcurrentConnections: number;
48
+
49
+ private connectionManager: ConnectionManager;
50
+ private requestTracker: RequestTracker;
51
+ private handshakeHandler: HandshakeHandler;
52
+ private messageHandler: MessageHandler;
53
+ private log = new Logger('AutomationBridge');
54
+
55
+ private lastHandshakeAt?: Date;
56
+ private lastHandshakeMetadata?: Record<string, unknown>;
57
+ private lastHandshakeAck?: AutomationBridgeMessage;
58
+ private lastHandshakeFailure?: { reason: string; at: Date };
59
+ private lastDisconnect?: { code: number; reason: string; at: Date };
60
+ private lastError?: { message: string; at: Date };
61
+ private requestQueue: Array<() => void> = [];
62
+ private queuedRequestItems: Array<{ resolve: (v: any) => void; reject: (e: any) => void; action: string; payload: any; options: any }> = [];
63
+ private connectionPromise?: Promise<void>;
64
+
65
+ constructor(options: AutomationBridgeOptions = {}) {
66
+ super();
67
+ this.host = options.host ?? process.env.MCP_AUTOMATION_WS_HOST ?? DEFAULT_AUTOMATION_HOST;
68
+
69
+ const sanitizePort = (value: unknown): number | null => {
70
+ if (typeof value === 'number' && Number.isInteger(value)) {
71
+ return value > 0 && value <= 65535 ? value : null;
72
+ }
73
+ if (typeof value === 'string' && value.trim().length > 0) {
74
+ const parsed = Number.parseInt(value.trim(), 10);
75
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= 65535 ? parsed : null;
76
+ }
77
+ return null;
78
+ };
79
+
80
+ const defaultPort = sanitizePort(options.port ?? process.env.MCP_AUTOMATION_WS_PORT) ?? DEFAULT_AUTOMATION_PORT;
81
+ const configuredPortValues: Array<number | string> | undefined = options.ports
82
+ ? options.ports
83
+ : process.env.MCP_AUTOMATION_WS_PORTS
84
+ ?.split(',')
85
+ .map((token) => token.trim())
86
+ .filter((token) => token.length > 0);
87
+
88
+ const sanitizedPorts = Array.isArray(configuredPortValues)
89
+ ? configuredPortValues
90
+ .map((value) => sanitizePort(value))
91
+ .filter((port): port is number => port !== null)
92
+ : [];
93
+
94
+ if (!sanitizedPorts.includes(defaultPort)) {
95
+ sanitizedPorts.unshift(defaultPort);
96
+ }
97
+ if (sanitizedPorts.length === 0) {
98
+ sanitizedPorts.push(DEFAULT_AUTOMATION_PORT);
99
+ }
100
+
101
+ this.ports = Array.from(new Set(sanitizedPorts));
102
+ const defaultProtocols = DEFAULT_NEGOTIATED_PROTOCOLS;
103
+ const userProtocols = Array.isArray(options.protocols)
104
+ ? options.protocols.filter((proto) => typeof proto === 'string' && proto.trim().length > 0)
105
+ : [];
106
+ const envProtocols = process.env.MCP_AUTOMATION_WS_PROTOCOLS
107
+ ? process.env.MCP_AUTOMATION_WS_PROTOCOLS.split(',')
108
+ .map((token) => token.trim())
109
+ .filter((token) => token.length > 0)
110
+ : [];
111
+ this.negotiatedProtocols = Array.from(new Set([...userProtocols, ...envProtocols, ...defaultProtocols]));
112
+ this.port = this.ports[0];
113
+ this.serverLegacyEnabled =
114
+ options.serverLegacyEnabled ?? process.env.MCP_AUTOMATION_SERVER_LEGACY !== 'false';
115
+ this.capabilityToken =
116
+ options.capabilityToken ?? process.env.MCP_AUTOMATION_CAPABILITY_TOKEN ?? undefined;
117
+ this.enabled = options.enabled ?? process.env.MCP_AUTOMATION_BRIDGE_ENABLED !== 'false';
118
+ this.serverName = options.serverName
119
+ ?? process.env.MCP_SERVER_NAME
120
+ ?? packageInfo.name
121
+ ?? 'unreal-engine-mcp';
122
+ this.serverVersion = options.serverVersion
123
+ ?? process.env.MCP_SERVER_VERSION
124
+ ?? packageInfo.version
125
+ ?? process.env.npm_package_version
126
+ ?? '0.0.0';
127
+
128
+ const heartbeatIntervalMs = (options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS) > 0
129
+ ? (options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS)
130
+ : 0;
131
+
132
+ const maxPendingRequests = Math.max(1, options.maxPendingRequests ?? DEFAULT_MAX_PENDING_REQUESTS);
133
+ const maxConcurrentConnections = Math.max(1, options.maxConcurrentConnections ?? 10);
134
+
135
+ this.clientHost = options.clientHost ?? process.env.MCP_AUTOMATION_CLIENT_HOST ?? DEFAULT_AUTOMATION_HOST;
136
+ this.clientPort = options.clientPort ?? sanitizePort(process.env.MCP_AUTOMATION_CLIENT_PORT) ?? DEFAULT_AUTOMATION_PORT;
137
+ this.maxConcurrentConnections = maxConcurrentConnections;
138
+
139
+ // Initialize components
140
+ this.connectionManager = new ConnectionManager(heartbeatIntervalMs);
141
+ this.requestTracker = new RequestTracker(maxPendingRequests);
142
+ this.handshakeHandler = new HandshakeHandler(this.capabilityToken);
143
+ this.messageHandler = new MessageHandler(this.requestTracker);
144
+
145
+ // Forward events from connection manager
146
+ // Note: ConnectionManager doesn't emit 'connected'/'disconnected' directly in the same way,
147
+ // we handle socket events here and use ConnectionManager to track state.
148
+ }
149
+
150
+ override on<K extends keyof AutomationBridgeEvents>(
151
+ event: K,
152
+ listener: AutomationBridgeEvents[K]
153
+ ): this {
154
+ return super.on(event, listener as (...args: unknown[]) => void);
155
+ }
156
+
157
+ override once<K extends keyof AutomationBridgeEvents>(
158
+ event: K,
159
+ listener: AutomationBridgeEvents[K]
160
+ ): this {
161
+ return super.once(event, listener as (...args: unknown[]) => void);
162
+ }
163
+
164
+ override off<K extends keyof AutomationBridgeEvents>(
165
+ event: K,
166
+ listener: AutomationBridgeEvents[K]
167
+ ): this {
168
+ return super.off(event, listener as (...args: unknown[]) => void);
169
+ }
170
+
171
+ start(): void {
172
+ if (!this.enabled) {
173
+ this.log.info('Automation bridge disabled by configuration.');
174
+ return;
175
+ }
176
+
177
+ this.log.info(`Automation bridge connecting to Unreal server at ws://${this.clientHost}:${this.clientPort}`);
178
+ this.startClient();
179
+ }
180
+
181
+ private startClient(): void {
182
+ try {
183
+ const url = `ws://${this.clientHost}:${this.clientPort}`;
184
+ this.log.info(`Connecting to Unreal Engine automation server at ${url}`);
185
+
186
+ this.log.debug(`Negotiated protocols: ${JSON.stringify(this.negotiatedProtocols)}`);
187
+
188
+ // Compatibility fix: If only one protocol, pass as string to ensure ws/plugin compatibility
189
+ const protocols = 'mcp-automation';
190
+
191
+ this.log.debug(`Using WebSocket protocols arg: ${JSON.stringify(protocols)}`);
192
+
193
+ const socket = new WebSocket(url, protocols, {
194
+ headers: this.capabilityToken ? { 'X-MCP-Capability': this.capabilityToken } : undefined,
195
+ perMessageDeflate: false
196
+ });
197
+
198
+ this.handleClientConnection(socket);
199
+ } catch (error) {
200
+ const errorObj = error instanceof Error ? error : new Error(String(error));
201
+ this.lastError = { message: errorObj.message, at: new Date() };
202
+ this.log.error('Failed to create WebSocket client connection', errorObj);
203
+ const errorWithPort = Object.assign(errorObj, { port: this.clientPort });
204
+ this.emitAutomation('error', errorWithPort);
205
+ }
206
+ }
207
+
208
+ private async handleClientConnection(socket: WebSocket): Promise<void> {
209
+ socket.on('open', async () => {
210
+ this.log.info('Automation bridge client connected, starting handshake');
211
+ try {
212
+ const metadata = await this.handshakeHandler.initiateHandshake(socket);
213
+
214
+ this.lastHandshakeAt = new Date();
215
+ this.lastHandshakeMetadata = metadata;
216
+ this.lastHandshakeFailure = undefined;
217
+ this.connectionManager.updateLastMessageTime();
218
+
219
+ // Extract remote address/port
220
+ const underlying: any = (socket as any)._socket || (socket as any).socket;
221
+ const remoteAddr = underlying?.remoteAddress ?? undefined;
222
+ const remotePort = underlying?.remotePort ?? undefined;
223
+
224
+ this.connectionManager.registerSocket(socket, this.clientPort, metadata, remoteAddr, remotePort);
225
+ this.connectionManager.startHeartbeat();
226
+ this.flushQueue();
227
+
228
+ this.emitAutomation('connected', {
229
+ socket,
230
+ metadata,
231
+ port: this.clientPort,
232
+ protocol: socket.protocol || null
233
+ });
234
+
235
+ // Set up message handling for the authenticated socket
236
+ socket.on('message', (data) => {
237
+ try {
238
+ const text = typeof data === 'string' ? data : data.toString('utf8');
239
+ this.log.debug(`[AutomationBridge Client] Received message: ${text.substring(0, 1000)}`);
240
+ const parsed = JSON.parse(text) as AutomationBridgeMessage;
241
+ this.connectionManager.updateLastMessageTime();
242
+ this.messageHandler.handleMessage(parsed);
243
+ this.emitAutomation('message', parsed);
244
+ } catch (error) {
245
+ this.log.error('Error handling message', error);
246
+ }
247
+ });
248
+
249
+ } catch (error) {
250
+ const err = error instanceof Error ? error : new Error(String(error));
251
+ this.lastHandshakeFailure = { reason: err.message, at: new Date() };
252
+ this.emitAutomation('handshakeFailed', { reason: err.message, port: this.clientPort });
253
+ }
254
+ });
255
+
256
+ socket.on('error', (error) => {
257
+ this.log.error('Automation bridge client socket error', error);
258
+ const errObj = error instanceof Error ? error : new Error(String(error));
259
+ this.lastError = { message: errObj.message, at: new Date() };
260
+ const errWithPort = Object.assign(errObj, { port: this.clientPort });
261
+ this.emitAutomation('error', errWithPort);
262
+ });
263
+
264
+ socket.on('close', (code, reasonBuffer) => {
265
+ const reason = reasonBuffer.toString('utf8');
266
+ const socketInfo = this.connectionManager.removeSocket(socket);
267
+
268
+ if (socketInfo) {
269
+ this.lastDisconnect = { code, reason, at: new Date() };
270
+ this.emitAutomation('disconnected', {
271
+ code,
272
+ reason,
273
+ port: socketInfo.port,
274
+ protocol: socketInfo.protocol || null
275
+ });
276
+ this.log.info(`Automation bridge client socket closed (code=${code}, reason=${reason})`);
277
+
278
+ if (!this.connectionManager.isConnected()) {
279
+ this.requestTracker.rejectAll(new Error(reason || 'Connection lost'));
280
+ }
281
+ }
282
+ });
283
+ }
284
+
285
+ stop(): void {
286
+ if (this.isConnected()) {
287
+ this.broadcast({
288
+ type: 'bridge_shutdown',
289
+ timestamp: new Date().toISOString(),
290
+ reason: 'Server shutting down'
291
+ });
292
+ }
293
+ this.connectionManager.closeAll(1001, 'Server shutdown');
294
+ this.lastHandshakeAck = undefined;
295
+ this.requestTracker.rejectAll(new Error('Automation bridge server stopped'));
296
+ }
297
+
298
+ isConnected(): boolean {
299
+ return this.connectionManager.isConnected();
300
+ }
301
+
302
+ getStatus(): AutomationBridgeStatus {
303
+
304
+ const connectionInfos = Array.from(this.connectionManager.getActiveSockets().entries()).map(([socket, info]) => ({
305
+ connectionId: info.connectionId,
306
+ sessionId: info.sessionId ?? null,
307
+ remoteAddress: info.remoteAddress ?? null,
308
+ remotePort: info.remotePort ?? null,
309
+ port: info.port,
310
+ connectedAt: info.connectedAt.toISOString(),
311
+ protocol: info.protocol || null,
312
+ readyState: socket.readyState,
313
+ isPrimary: socket === this.connectionManager.getPrimarySocket()
314
+ }));
315
+
316
+ return {
317
+ enabled: this.enabled,
318
+ host: this.host,
319
+ port: this.port,
320
+ configuredPorts: [...this.ports],
321
+ listeningPorts: [], // We are client-only now
322
+ connected: this.isConnected(),
323
+ connectedAt: connectionInfos.length > 0 ? connectionInfos[0].connectedAt : null,
324
+ activePort: connectionInfos.length > 0 ? connectionInfos[0].port : null,
325
+ negotiatedProtocol: connectionInfos.length > 0 ? connectionInfos[0].protocol : null,
326
+ supportedProtocols: [...this.negotiatedProtocols],
327
+ supportedOpcodes: ['automation_request'],
328
+ expectedResponseOpcodes: ['automation_response'],
329
+ capabilityTokenRequired: Boolean(this.capabilityToken),
330
+ lastHandshakeAt: this.lastHandshakeAt?.toISOString() ?? null,
331
+ lastHandshakeMetadata: this.lastHandshakeMetadata ?? null,
332
+ lastHandshakeAck: this.lastHandshakeAck ?? null,
333
+ lastHandshakeFailure: this.lastHandshakeFailure
334
+ ? { reason: this.lastHandshakeFailure.reason, at: this.lastHandshakeFailure.at.toISOString() }
335
+ : null,
336
+ lastDisconnect: this.lastDisconnect
337
+ ? { code: this.lastDisconnect.code, reason: this.lastDisconnect.reason, at: this.lastDisconnect.at.toISOString() }
338
+ : null,
339
+ lastError: this.lastError
340
+ ? { message: this.lastError.message, at: this.lastError.at.toISOString() }
341
+ : null,
342
+ lastMessageAt: this.connectionManager.getLastMessageTime()?.toISOString() ?? null,
343
+ lastRequestSentAt: null, // TODO: Track this in RequestTracker?
344
+ pendingRequests: this.requestTracker.getPendingCount(),
345
+ pendingRequestDetails: this.requestTracker.getPendingDetails(),
346
+ connections: connectionInfos,
347
+ webSocketListening: false,
348
+ serverLegacyEnabled: this.serverLegacyEnabled,
349
+ serverName: this.serverName,
350
+ serverVersion: this.serverVersion,
351
+ maxConcurrentConnections: this.maxConcurrentConnections,
352
+ maxPendingRequests: 100, // TODO: Expose from RequestTracker
353
+ heartbeatIntervalMs: 30000 // TODO: Expose from ConnectionManager
354
+ };
355
+ }
356
+
357
+ async sendAutomationRequest<T = AutomationBridgeResponseMessage>(
358
+ action: string,
359
+ payload: Record<string, unknown> = {},
360
+ options: { timeoutMs?: number } = {}
361
+ ): Promise<T> {
362
+ if (!this.isConnected()) {
363
+ if (this.enabled) {
364
+ this.log.info('Automation bridge not connected, attempting lazy connection...');
365
+
366
+ // Avoid multiple simultaneous connection attempts
367
+ if (!this.connectionPromise) {
368
+ this.connectionPromise = new Promise<void>((resolve, reject) => {
369
+ const onConnect = () => {
370
+ cleanup(); resolve();
371
+ };
372
+ // We map errors to rejects, but we should be careful about which errors.
373
+ // A socket error might happen during connection.
374
+ const onError = (err: any) => {
375
+ cleanup(); reject(err);
376
+ };
377
+ // Also listen for handshake failure
378
+ const onHandshakeFail = (err: any) => {
379
+ cleanup(); reject(new Error(`Handshake failed: ${err.reason}`));
380
+ };
381
+
382
+ const cleanup = () => {
383
+ this.off('connected', onConnect);
384
+ this.off('error', onError);
385
+ this.off('handshakeFailed', onHandshakeFail);
386
+ // If we failed, clear the promise so next attempt can try again
387
+ if (this.connectionPromise) this.connectionPromise = undefined;
388
+ };
389
+
390
+ this.once('connected', onConnect);
391
+ this.once('error', onError);
392
+ this.once('handshakeFailed', onHandshakeFail);
393
+
394
+ try {
395
+ this.startClient();
396
+ } catch (e) {
397
+ onError(e);
398
+ }
399
+ });
400
+ }
401
+
402
+ try {
403
+ // Wait for connection with a short timeout for the connection itself
404
+ const connectTimeout = 5000;
405
+ await Promise.race([
406
+ this.connectionPromise,
407
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Lazy connection timeout')), connectTimeout))
408
+ ]);
409
+ } catch (err: any) {
410
+ this.log.error('Lazy connection failed', err);
411
+ // We don't throw here immediately, we let the isConnected check fail below
412
+ // or throw a specific error.
413
+ // Actually, if connection failed, we should probably fail the request.
414
+ throw new Error(`Failed to establish connection to Unreal Engine: ${err.message}`);
415
+ }
416
+ } else {
417
+ throw new Error('Automation bridge disabled');
418
+ }
419
+ }
420
+
421
+ if (!this.isConnected()) {
422
+ throw new Error('Automation bridge not connected');
423
+ }
424
+
425
+ // Check if we need to queue (unless it's a priority request which standard ones are not)
426
+ // We use requestTracker directly to check limit as it's the source of truth
427
+ // Note: requestTracker exposes maxPendingRequests via constructor but generic check logic isn't public
428
+ // We assumed getPendingCount() is available
429
+ if (this.requestTracker.getPendingCount() >= (this as any).requestTracker.maxPendingRequests) {
430
+ return new Promise<T>((resolve, reject) => {
431
+ this.queuedRequestItems.push({
432
+ resolve,
433
+ reject,
434
+ action,
435
+ payload,
436
+ options
437
+ });
438
+ });
439
+ }
440
+
441
+ return this.sendRequestInternal<T>(action, payload, options);
442
+ }
443
+
444
+ private async sendRequestInternal<T>(
445
+ action: string,
446
+ payload: Record<string, unknown>,
447
+ options: { timeoutMs?: number }
448
+ ): Promise<T> {
449
+ const timeoutMs = options.timeoutMs ?? 60000; // Increased default timeout to 60s
450
+
451
+ // Check for coalescing
452
+ const coalesceKey = this.requestTracker.createCoalesceKey(action, payload);
453
+ if (coalesceKey) {
454
+ const existing = this.requestTracker.getCoalescedRequest(coalesceKey);
455
+ if (existing) {
456
+ return existing as unknown as T;
457
+ }
458
+ }
459
+
460
+ const { requestId, promise } = this.requestTracker.createRequest(action, payload, timeoutMs);
461
+
462
+ if (coalesceKey) {
463
+ this.requestTracker.setCoalescedRequest(coalesceKey, promise);
464
+ }
465
+
466
+ const message: AutomationBridgeMessage = {
467
+ type: 'automation_request',
468
+ requestId,
469
+ action,
470
+ payload
471
+ };
472
+
473
+ const resultPromise = promise as unknown as Promise<T>;
474
+
475
+ // Ensure we process the queue when this request finishes
476
+ resultPromise.finally(() => {
477
+ this.processRequestQueue();
478
+ }).catch(() => { }); // catch to prevent unhandled rejection during finally chain? no, finally returns new promise
479
+
480
+ if (this.send(message)) {
481
+ return resultPromise;
482
+ } else {
483
+ this.requestTracker.rejectRequest(requestId, new Error('Failed to send request'));
484
+ throw new Error('Failed to send request');
485
+ }
486
+ }
487
+
488
+ private processRequestQueue() {
489
+ if (this.queuedRequestItems.length === 0) return;
490
+
491
+ // while we have capacity and items
492
+ while (
493
+ this.queuedRequestItems.length > 0 &&
494
+ this.requestTracker.getPendingCount() < (this as any).requestTracker.maxPendingRequests
495
+ ) {
496
+ const item = this.queuedRequestItems.shift();
497
+ if (item) {
498
+ this.sendRequestInternal(item.action, item.payload, item.options)
499
+ .then(item.resolve)
500
+ .catch(item.reject);
501
+ }
502
+ }
503
+ }
504
+
505
+ send(payload: AutomationBridgeMessage): boolean {
506
+ const primarySocket = this.connectionManager.getPrimarySocket();
507
+ if (!primarySocket || primarySocket.readyState !== WebSocket.OPEN) {
508
+ this.log.warn('Attempted to send automation message without an active primary connection');
509
+ return false;
510
+ }
511
+ try {
512
+ primarySocket.send(JSON.stringify(payload));
513
+ return true;
514
+ } catch (error) {
515
+ this.log.error('Failed to send automation message', error);
516
+ const errObj = error instanceof Error ? error : new Error(String(error));
517
+ const primaryInfo = this.connectionManager.getActiveSockets().get(primarySocket);
518
+ const errorWithPort = Object.assign(errObj, { port: primaryInfo?.port });
519
+ this.emitAutomation('error', errorWithPort);
520
+ return false;
521
+ }
522
+ }
523
+
524
+ private broadcast(payload: AutomationBridgeMessage): boolean {
525
+ const sockets = this.connectionManager.getActiveSockets();
526
+ if (sockets.size === 0) {
527
+ this.log.warn('Attempted to broadcast automation message without any active connections');
528
+ return false;
529
+ }
530
+ let sentCount = 0;
531
+ for (const [socket] of sockets) {
532
+ if (socket.readyState === WebSocket.OPEN) {
533
+ try {
534
+ socket.send(JSON.stringify(payload));
535
+ sentCount++;
536
+ } catch (error) {
537
+ this.log.error('Failed to broadcast automation message to socket', error);
538
+ }
539
+ }
540
+ }
541
+ return sentCount > 0;
542
+ }
543
+
544
+ private flushQueue(): void {
545
+ if (this.requestQueue.length === 0) return;
546
+ this.log.info(`Flushing ${this.requestQueue.length} queued automation requests`);
547
+ const queue = [...this.requestQueue];
548
+ this.requestQueue = [];
549
+ queue.forEach(fn => fn());
550
+ }
551
+
552
+ private emitAutomation<K extends keyof AutomationBridgeEvents>(
553
+ event: K,
554
+ ...args: Parameters<AutomationBridgeEvents[K]>
555
+ ): void {
556
+ this.emit(event, ...args);
557
+ }
558
+ }
@@ -0,0 +1,130 @@
1
+ import { WebSocket } from 'ws';
2
+ import { Logger } from '../utils/logger.js';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { SocketInfo } from './types.js';
5
+ import { EventEmitter } from 'node:events';
6
+
7
+ export class ConnectionManager extends EventEmitter {
8
+ private activeSockets = new Map<WebSocket, SocketInfo>();
9
+ private primarySocket?: WebSocket;
10
+ private heartbeatTimer?: NodeJS.Timeout;
11
+ private lastMessageAt?: Date;
12
+ private log = new Logger('ConnectionManager');
13
+
14
+ constructor(
15
+ private heartbeatIntervalMs: number
16
+ ) {
17
+ super();
18
+ }
19
+
20
+ public registerSocket(
21
+ socket: WebSocket,
22
+ port: number,
23
+ metadata?: Record<string, unknown>,
24
+ remoteAddress?: string,
25
+ remotePort?: number
26
+ ): void {
27
+ const connectionId = randomUUID();
28
+ const sessionId = metadata && typeof metadata.sessionId === 'string' ? (metadata.sessionId as string) : undefined;
29
+ const socketInfo: SocketInfo = {
30
+ connectionId,
31
+ port,
32
+ connectedAt: new Date(),
33
+ protocol: socket.protocol || undefined,
34
+ sessionId,
35
+ remoteAddress: remoteAddress ?? undefined,
36
+ remotePort: typeof remotePort === 'number' ? remotePort : undefined
37
+ };
38
+
39
+ this.activeSockets.set(socket, socketInfo);
40
+
41
+ // Set as primary socket if this is the first connection
42
+ if (!this.primarySocket) {
43
+ this.primarySocket = socket;
44
+ }
45
+
46
+ // Handle WebSocket pong frames for heartbeat tracking
47
+ socket.on('pong', () => {
48
+ this.lastMessageAt = new Date();
49
+ });
50
+ }
51
+
52
+ public removeSocket(socket: WebSocket): SocketInfo | undefined {
53
+ const info = this.activeSockets.get(socket);
54
+ if (info) {
55
+ this.activeSockets.delete(socket);
56
+ if (socket === this.primarySocket) {
57
+ this.primarySocket = this.activeSockets.size > 0 ? this.activeSockets.keys().next().value : undefined;
58
+ if (this.activeSockets.size === 0) {
59
+ this.stopHeartbeat();
60
+ }
61
+ }
62
+ }
63
+ return info;
64
+ }
65
+
66
+ public getActiveSockets(): Map<WebSocket, SocketInfo> {
67
+ return this.activeSockets;
68
+ }
69
+
70
+ public getPrimarySocket(): WebSocket | undefined {
71
+ return this.primarySocket;
72
+ }
73
+
74
+ public isConnected(): boolean {
75
+ return this.activeSockets.size > 0;
76
+ }
77
+
78
+ public startHeartbeat(): void {
79
+ if (this.heartbeatIntervalMs <= 0) return;
80
+ if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
81
+
82
+ this.heartbeatTimer = setInterval(() => {
83
+ if (this.activeSockets.size === 0) {
84
+ this.stopHeartbeat();
85
+ return;
86
+ }
87
+
88
+ const pingPayload = JSON.stringify({
89
+ type: 'bridge_ping',
90
+ timestamp: new Date().toISOString()
91
+ });
92
+
93
+ for (const [socket] of this.activeSockets) {
94
+ if (socket.readyState === WebSocket.OPEN) {
95
+ try {
96
+ socket.ping();
97
+ socket.send(pingPayload);
98
+ } catch (error) {
99
+ this.log.error('Failed to send heartbeat', error);
100
+ }
101
+ }
102
+ }
103
+ }, this.heartbeatIntervalMs);
104
+ }
105
+
106
+ public stopHeartbeat(): void {
107
+ if (this.heartbeatTimer) {
108
+ clearInterval(this.heartbeatTimer);
109
+ this.heartbeatTimer = undefined;
110
+ }
111
+ }
112
+
113
+ public updateLastMessageTime(): void {
114
+ this.lastMessageAt = new Date();
115
+ }
116
+
117
+ public getLastMessageTime(): Date | undefined {
118
+ return this.lastMessageAt;
119
+ }
120
+
121
+ public closeAll(code?: number, reason?: string): void {
122
+ this.stopHeartbeat();
123
+ for (const [socket] of this.activeSockets) {
124
+ socket.removeAllListeners();
125
+ socket.close(code, reason);
126
+ }
127
+ this.activeSockets.clear();
128
+ this.primarySocket = undefined;
129
+ }
130
+ }