unreal-engine-mcp-server 0.4.7 → 0.5.0

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 (438) 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.yml +148 -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 +23 -0
  19. package/.github/workflows/labeler.yml +16 -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 +12 -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 +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. 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
+ }