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,933 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ import net from 'node:net';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const repoRoot = path.resolve(__dirname, '..');
11
+ const reportsDir = path.join(__dirname, 'reports');
12
+
13
+ // Common failure keywords to check against
14
+ const failureKeywords = ['failed', 'error', 'exception', 'invalid', 'not found', 'missing', 'timed out', 'timeout', 'unsupported', 'unknown'];
15
+ const successKeywords = ['success', 'created', 'updated', 'deleted', 'completed', 'done', 'ok'];
16
+
17
+ // Defaults for spawning the MCP server.
18
+ let serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'node';
19
+ let serverArgs = process.env.UNREAL_MCP_SERVER_ARGS ? process.env.UNREAL_MCP_SERVER_ARGS.split(',') : [path.join(repoRoot, 'dist', 'cli.js')];
20
+ const serverCwd = process.env.UNREAL_MCP_SERVER_CWD ?? repoRoot;
21
+ const serverEnv = Object.assign({}, process.env);
22
+
23
+ function formatResultLine(testCase, status, detail, durationMs) {
24
+ const durationText = typeof durationMs === 'number' ? ` (${durationMs.toFixed(1)} ms)` : '';
25
+ return `[${status.toUpperCase()}] ${testCase.scenario}${durationText}${detail ? ` => ${detail}` : ''}`;
26
+ }
27
+
28
+ async function persistResults(toolName, results) {
29
+ await fs.mkdir(reportsDir, { recursive: true });
30
+ const timestamp = new Date().toISOString().replace(/[:]/g, '-');
31
+ const resultsPath = path.join(reportsDir, `${toolName}-test-results-${timestamp}.json`);
32
+ const serializable = results.map((result) => ({
33
+ scenario: result.scenario,
34
+ toolName: result.toolName,
35
+ arguments: result.arguments,
36
+ status: result.status,
37
+ durationMs: result.durationMs,
38
+ detail: result.detail
39
+ }));
40
+ await fs.writeFile(resultsPath, JSON.stringify({ generatedAt: new Date().toISOString(), toolName, results: serializable }, null, 2));
41
+ return resultsPath;
42
+ }
43
+
44
+ function summarize(toolName, results, resultsPath) {
45
+ const totals = results.reduce((acc, result) => { acc.total += 1; acc[result.status] = (acc[result.status] ?? 0) + 1; return acc; }, { total: 0, passed: 0, failed: 0, skipped: 0 });
46
+ console.log('\n' + '='.repeat(60));
47
+ console.log(`${toolName} Test Summary`);
48
+ console.log('='.repeat(60));
49
+ console.log(`Total cases: ${totals.total}`);
50
+ console.log(`✅ Passed: ${totals.passed ?? 0}`);
51
+ console.log(`❌ Failed: ${totals.failed ?? 0}`);
52
+ console.log(`⏭️ Skipped: ${totals.skipped ?? 0}`);
53
+ if (totals.passed && totals.total > 0) console.log(`Pass rate: ${((totals.passed / totals.total) * 100).toFixed(1)}%`);
54
+ console.log(`Results saved to: ${resultsPath}`);
55
+ console.log('='.repeat(60));
56
+ }
57
+
58
+ /**
59
+ * Evaluates whether a test case passed based on expected outcome
60
+ */
61
+ function evaluateExpectation(testCase, response) {
62
+ const expectation = testCase.expected;
63
+
64
+ // Normalize expected into a comparable form. If expected is an object
65
+ // (e.g. {condition: 'success|error', errorPattern: 'SC_DISABLED'}), then
66
+ // we extract the condition string as the primary expectation string.
67
+ const expectedCondition = (typeof expectation === 'object' && expectation !== null && expectation.condition)
68
+ ? expectation.condition
69
+ : (typeof expectation === 'string' ? expectation : String(expectation));
70
+
71
+ const lowerExpected = expectedCondition.toLowerCase();
72
+
73
+ // Determine failure/success intent from condition keywords
74
+ const containsFailure = failureKeywords.some((word) => lowerExpected.includes(word));
75
+ const containsSuccess = successKeywords.some((word) => lowerExpected.includes(word));
76
+
77
+ const structuredSuccess = typeof response.structuredContent?.success === 'boolean'
78
+ ? response.structuredContent.success
79
+ : undefined;
80
+ const actualSuccess = structuredSuccess ?? !response.isError;
81
+
82
+ // Extract actual error/message from response
83
+ let actualError = null;
84
+ let actualMessage = null;
85
+ if (response.structuredContent) {
86
+ actualError = response.structuredContent.error;
87
+ actualMessage = response.structuredContent.message;
88
+ }
89
+
90
+ // Also extract flattened plain-text content for matching when structured
91
+ // fields are missing or when MCP errors (e.g. timeouts) are only reported
92
+ // via the textual content array.
93
+ let contentText = '';
94
+ if (Array.isArray(response.content) && response.content.length > 0) {
95
+ contentText = response.content
96
+ .map((entry) => (entry && typeof entry.text === 'string' ? entry.text : ''))
97
+ .filter((t) => t.length > 0)
98
+ .join('\n');
99
+ }
100
+
101
+ // Helper to get effective actual strings for matching
102
+ const messageStr = (actualMessage || '').toString().toLowerCase();
103
+ const errorStr = (actualError || '').toString().toLowerCase();
104
+ const contentStr = contentText.toString().toLowerCase();
105
+ const combined = `${messageStr} ${errorStr} ${contentStr}`;
106
+
107
+ // If expectation is an object with specific pattern constraints, apply them
108
+ if (typeof expectation === 'object' && expectation !== null) {
109
+ // If actual outcome was success, check successPattern
110
+ if (actualSuccess && expectation.successPattern) {
111
+ const pattern = expectation.successPattern.toLowerCase();
112
+ if (combined.includes(pattern)) {
113
+ return { passed: true, reason: `Success pattern matched: ${expectation.successPattern}` };
114
+ }
115
+ }
116
+ // If actual outcome was error/failure, check errorPattern
117
+ if (!actualSuccess && expectation.errorPattern) {
118
+ const pattern = expectation.errorPattern.toLowerCase();
119
+ if (combined.includes(pattern)) {
120
+ return { passed: true, reason: `Error pattern matched: ${expectation.errorPattern}` };
121
+ }
122
+ }
123
+ }
124
+
125
+ // Handle multi-condition expectations using "or" or pipe separators
126
+ // e.g., "success or LOAD_FAILED" or "success|no_instances|load_failed"
127
+ if (lowerExpected.includes(' or ') || lowerExpected.includes('|')) {
128
+ const separator = lowerExpected.includes(' or ') ? ' or ' : '|';
129
+ const conditions = lowerExpected.split(separator).map((c) => c.trim()).filter(Boolean);
130
+ for (const condition of conditions) {
131
+ if (successKeywords.some((kw) => condition.includes(kw)) && actualSuccess === true) {
132
+ return { passed: true, reason: JSON.stringify(response.structuredContent) };
133
+ }
134
+ if (condition === 'handled' && response.structuredContent && response.structuredContent.handled === true) {
135
+ return { passed: true, reason: 'Handled gracefully' };
136
+ }
137
+
138
+ // Special-case timeout expectations so that MCP transport timeouts
139
+ // (e.g. "Request timed out") satisfy conditions containing "timeout".
140
+ if (condition === 'timeout' || condition.includes('timeout')) {
141
+ if (combined.includes('timeout') || combined.includes('timed out')) {
142
+ return { passed: true, reason: `Expected timeout condition met: ${condition}` };
143
+ }
144
+ }
145
+
146
+ if (combined.includes(condition)) {
147
+ return { passed: true, reason: `Expected condition met: ${condition}` };
148
+ }
149
+ }
150
+ // If none of the OR/pipe conditions matched, it's a failure
151
+ return { passed: false, reason: `None of the expected conditions matched: ${expectedCondition}` };
152
+ }
153
+
154
+ // Also flag common automation/plugin failure phrases
155
+ const pluginFailureIndicators = ['does not match prefix', 'unknown', 'not implemented', 'unavailable', 'unsupported'];
156
+ const hasPluginFailure = pluginFailureIndicators.some(term => combined.includes(term));
157
+
158
+ if (!containsFailure && hasPluginFailure) {
159
+ return {
160
+ passed: false,
161
+ reason: `Expected success but plugin reported failure: ${actualMessage || actualError}`
162
+ };
163
+ }
164
+
165
+ // CRITICAL: Check if message says "failed" but success is true (FALSE POSITIVE)
166
+ if (actualSuccess && (
167
+ messageStr.includes('failed') ||
168
+ messageStr.includes('python execution failed') ||
169
+ errorStr.includes('failed')
170
+ )) {
171
+ return {
172
+ passed: false,
173
+ reason: `False positive: success=true but message indicates failure: ${actualMessage}`
174
+ };
175
+ }
176
+
177
+ // CRITICAL FIX: UE_NOT_CONNECTED errors should ALWAYS fail tests unless explicitly expected
178
+ if (actualError === 'UE_NOT_CONNECTED') {
179
+ const explicitlyExpectsDisconnection = lowerExpected.includes('not connected') ||
180
+ lowerExpected.includes('ue_not_connected') ||
181
+ lowerExpected.includes('disconnected');
182
+ if (!explicitlyExpectsDisconnection) {
183
+ return {
184
+ passed: false,
185
+ reason: `Test requires Unreal Engine connection, but got: ${actualError} - ${actualMessage}`
186
+ };
187
+ }
188
+ }
189
+
190
+ // For tests that expect specific error types, validate the actual error matches
191
+ const expectedFailure = containsFailure && !containsSuccess;
192
+ if (expectedFailure && !actualSuccess) {
193
+ // Test expects failure and got failure - but verify it's the RIGHT kind of failure
194
+ const lowerReason = actualMessage?.toLowerCase() || actualError?.toLowerCase() || contentStr || '';
195
+
196
+ // Check for specific error types (not just generic "error" keyword)
197
+ const specificErrorTypes = ['not found', 'invalid', 'missing', 'already exists', 'does not exist', 'sc_disabled'];
198
+ const expectedErrorType = specificErrorTypes.find(type => lowerExpected.includes(type));
199
+ let errorTypeMatch = expectedErrorType ? lowerReason.includes(expectedErrorType) :
200
+ failureKeywords.some(keyword => lowerExpected.includes(keyword) && lowerReason.includes(keyword));
201
+
202
+ // Also check detail field if main error check failed (handles wrapped exceptions)
203
+ if (!errorTypeMatch && response.detail && typeof response.detail === 'string') {
204
+ const lowerDetail = response.detail.toLowerCase();
205
+ if (expectedErrorType) {
206
+ if (lowerDetail.includes(expectedErrorType)) errorTypeMatch = true;
207
+ } else {
208
+ // If no specific error type, just check if detail contains expected string
209
+ if (lowerDetail.includes(lowerExpected)) errorTypeMatch = true;
210
+ }
211
+ }
212
+
213
+ // If expected outcome specifies an error type, actual error should match it
214
+ if (lowerExpected.includes('not found') || lowerExpected.includes('invalid') ||
215
+ lowerExpected.includes('missing') || lowerExpected.includes('already exists') || lowerExpected.includes('sc_disabled')) {
216
+ const passed = errorTypeMatch;
217
+ let reason;
218
+ if (response.isError) {
219
+ reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
220
+ } else if (response.structuredContent) {
221
+ reason = JSON.stringify(response.structuredContent);
222
+ } else {
223
+ reason = 'No structured response returned';
224
+ }
225
+ return { passed, reason };
226
+ }
227
+ }
228
+
229
+ // Default evaluation logic
230
+ const passed = expectedFailure ? !actualSuccess : !!actualSuccess;
231
+ let reason;
232
+ if (response.isError) {
233
+ reason = response.content?.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
234
+ } else if (response.structuredContent) {
235
+ reason = JSON.stringify(response.structuredContent);
236
+ } else if (response.content?.length) {
237
+ reason = response.content.map((entry) => ('text' in entry ? entry.text : JSON.stringify(entry))).join('\n');
238
+ } else {
239
+ reason = 'No structured response returned';
240
+ }
241
+ return { passed, reason };
242
+ }
243
+
244
+ /**
245
+ * Main test runner function
246
+ */
247
+ export async function runToolTests(toolName, testCases) {
248
+ console.log(`Total test cases: ${testCases.length}`);
249
+ console.log('='.repeat(60));
250
+ console.log('');
251
+
252
+ let transport;
253
+ let client;
254
+ const results = [];
255
+ // callToolOnce is assigned after the MCP client is initialized. Declare here so
256
+ // the test loop can call it regardless of block scoping rules.
257
+ let callToolOnce;
258
+
259
+ try {
260
+ // Wait for the automation bridge ports to be available so the spawned MCP server
261
+ // process can successfully connect to the editor plugin.
262
+ const bridgeHost = process.env.MCP_AUTOMATION_WS_HOST ?? '127.0.0.1';
263
+ const envPorts = process.env.MCP_AUTOMATION_WS_PORTS
264
+ ? process.env.MCP_AUTOMATION_WS_PORTS.split(',').map((p) => parseInt(p.trim(), 10)).filter(Boolean)
265
+ : [8090, 8091];
266
+ const waitMs = 10000; // Hardcoded increased timeout
267
+
268
+ console.log(`Waiting up to ${waitMs}ms for automation bridge on ${bridgeHost}:${envPorts.join(',')}`);
269
+
270
+ async function waitForAnyPort(host, ports, timeoutMs = 10000) {
271
+ const start = Date.now();
272
+ while (Date.now() - start < timeoutMs) {
273
+ for (const port of ports) {
274
+ try {
275
+ await new Promise((resolve, reject) => {
276
+ const sock = new net.Socket();
277
+ let settled = false;
278
+ sock.setTimeout(1000);
279
+ sock.once('connect', () => { settled = true; sock.destroy(); resolve(true); });
280
+ sock.once('timeout', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('timeout')); } });
281
+ sock.once('error', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('error')); } });
282
+ sock.connect(port, host);
283
+ });
284
+ console.log(`✅ Automation bridge appears to be listening on ${host}:${port}`);
285
+ return port;
286
+ } catch {
287
+ // ignore and try next port
288
+ }
289
+ }
290
+ // Yield to the event loop once instead of sleeping.
291
+ await new Promise((r) => setImmediate(r));
292
+ }
293
+ throw new Error(`Timed out waiting for automation bridge on ports: ${ports.join(',')}`);
294
+ }
295
+
296
+ try {
297
+ await waitForAnyPort(bridgeHost, envPorts, waitMs);
298
+ } catch (err) {
299
+ console.warn('Automation bridge did not become available before tests started:', err.message);
300
+ }
301
+
302
+ // Decide whether to run the built server (dist/cli.js) or to run the
303
+ // TypeScript source directly. Prefer the built dist when it is up-to-date
304
+ // with the src tree. Fall back to running src with ts-node when dist is
305
+ // missing or older than the src modification time to avoid running stale code.
306
+ const distPath = path.join(repoRoot, 'dist', 'cli.js');
307
+ const srcDir = path.join(repoRoot, 'src');
308
+
309
+ async function getLatestMtime(dir) {
310
+ let latest = 0;
311
+ try {
312
+ const entries = await fs.readdir(dir, { withFileTypes: true });
313
+ for (const e of entries) {
314
+ const full = path.join(dir, e.name);
315
+ if (e.isDirectory()) {
316
+ const child = await getLatestMtime(full);
317
+ if (child > latest) latest = child;
318
+ } else {
319
+ try {
320
+ const st = await fs.stat(full);
321
+ const m = st.mtimeMs || 0;
322
+ if (m > latest) latest = m;
323
+ } catch (_) { }
324
+ }
325
+ }
326
+ } catch (_) {
327
+ // ignore
328
+ }
329
+ return latest;
330
+ }
331
+
332
+ // Choose how to launch the server. Prefer using the built `dist/` executable so
333
+ // Node resolves ESM imports cleanly. If `dist/` is missing, attempt an automatic
334
+ // `npm run build` so users that run live tests don't hit ts-node resolution errors.
335
+ let useDist = false;
336
+ let distExists = false;
337
+ try {
338
+ await fs.access(distPath);
339
+ distExists = true;
340
+ } catch (e) {
341
+ distExists = false;
342
+ }
343
+
344
+ if (process.env.UNREAL_MCP_FORCE_DIST === '1') {
345
+ useDist = true;
346
+ console.log('Forcing use of dist build via UNREAL_MCP_FORCE_DIST=1');
347
+ } else if (distExists) {
348
+ try {
349
+ const distStat = await fs.stat(distPath);
350
+ const srcLatest = await getLatestMtime(srcDir);
351
+ const srcIsNewer = srcLatest > (distStat.mtimeMs || 0);
352
+ const autoBuildEnabled = process.env.UNREAL_MCP_AUTO_BUILD === '1';
353
+ const autoBuildDisabled = process.env.UNREAL_MCP_NO_AUTO_BUILD === '1';
354
+ if (srcIsNewer) {
355
+ if (!autoBuildEnabled && !autoBuildDisabled) {
356
+ console.log('Detected newer source files than dist; attempting automatic build to refresh dist/ (set UNREAL_MCP_NO_AUTO_BUILD=1 to disable)');
357
+ }
358
+ if (autoBuildEnabled || !autoBuildDisabled) {
359
+ const { spawn } = await import('node:child_process');
360
+ try {
361
+ await new Promise((resolve, reject) => {
362
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
363
+ const ps = process.platform === 'win32'
364
+ ? spawn(`${npmCmd} run build`, { cwd: repoRoot, stdio: 'inherit', shell: true })
365
+ : spawn(npmCmd, ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
366
+ ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
367
+ ps.on('error', (err) => reject(err));
368
+ });
369
+ console.log('Build succeeded — using dist/ for live tests');
370
+ useDist = true;
371
+ } catch (buildErr) {
372
+ console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
373
+ useDist = false;
374
+ }
375
+ } else {
376
+ console.log('Detected newer source files than dist but automatic build is disabled.');
377
+ console.log('Set UNREAL_MCP_AUTO_BUILD=1 to enable automatic builds, or run `npm run build` manually.');
378
+ useDist = false;
379
+ }
380
+ } else {
381
+ useDist = true;
382
+ console.log('Using built dist for live tests');
383
+ }
384
+ } catch (buildErr) {
385
+ console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
386
+ useDist = false;
387
+ console.log('Preferring TypeScript source for tests to pick up local changes (set UNREAL_MCP_FORCE_DIST=1 to force dist)');
388
+ }
389
+ } else {
390
+ console.log('dist not found — attempting to run `npm run build` to produce dist/ for live tests');
391
+ try {
392
+ const { spawn } = await import('node:child_process');
393
+ await new Promise((resolve, reject) => {
394
+ const ps = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
395
+ ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
396
+ ps.on('error', (err) => reject(err));
397
+ });
398
+ useDist = true;
399
+ console.log('Build succeeded — using dist/ for live tests');
400
+ } catch (buildErr) {
401
+ console.warn('Automatic build failed — falling back to running TypeScript source with ts-node-esm:', String(buildErr));
402
+ useDist = false;
403
+ }
404
+ }
405
+
406
+ if (!useDist) {
407
+ serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'npx';
408
+ serverArgs = ['ts-node-esm', path.join(repoRoot, 'src', 'cli.ts')];
409
+ } else {
410
+ serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? serverCommand;
411
+ serverArgs = process.env.UNREAL_MCP_SERVER_ARGS?.split(',') ?? serverArgs;
412
+ }
413
+
414
+ transport = new StdioClientTransport({
415
+ command: serverCommand,
416
+ args: serverArgs,
417
+ cwd: serverCwd,
418
+ stderr: 'inherit',
419
+ env: serverEnv
420
+ });
421
+
422
+ client = new Client({
423
+ name: 'unreal-mcp-test-runner',
424
+ version: '1.0.0'
425
+ });
426
+
427
+ await client.connect(transport);
428
+ await client.listTools({});
429
+ console.log('✅ Connected to Unreal MCP Server\n');
430
+
431
+ // Single-attempt call helper (no retries). This forwards a timeoutMs
432
+ // argument to the server so server-side automation calls use the same
433
+ // timeout the test harness expects.
434
+ callToolOnce = async function (callOptions, baseTimeoutMs) {
435
+ const envDefault = Number(process.env.UNREAL_MCP_TEST_CALL_TIMEOUT_MS ?? '60000') || 60000;
436
+ const perCall = Number(callOptions?.arguments?.timeoutMs) || undefined;
437
+ const base = typeof baseTimeoutMs === 'number' && baseTimeoutMs > 0 ? baseTimeoutMs : (perCall || envDefault);
438
+ const timeoutMs = base;
439
+ try {
440
+ console.log(`[CALL] ${callOptions.name} (timeout ${timeoutMs}ms)`);
441
+ const outgoing = Object.assign({}, callOptions, { arguments: { ...(callOptions.arguments || {}), timeoutMs } });
442
+ // Prefer instructing the MCP client to use a matching timeout if
443
+ // the client library supports per-call options; fall back to the
444
+ // plain call if not supported.
445
+ let callPromise;
446
+ try {
447
+ // Correct parameter order: (params, resultSchema?, options)
448
+ callPromise = client.callTool(outgoing, undefined, { timeout: timeoutMs });
449
+ } catch (err) {
450
+ // Fall back to calling the older signature where options might be second param
451
+ try {
452
+ callPromise = client.callTool(outgoing, { timeout: timeoutMs });
453
+ } catch (inner) {
454
+ try {
455
+ callPromise = client.callTool(outgoing);
456
+ } catch (inner2) {
457
+ throw inner2 || inner || err;
458
+ }
459
+ }
460
+ }
461
+
462
+ let timeoutId;
463
+ const timeoutPromise = new Promise((_, rej) => {
464
+ timeoutId = setTimeout(() => rej(new Error(`Local test runner timeout after ${timeoutMs}ms`)), timeoutMs);
465
+ if (timeoutId && typeof timeoutId.unref === 'function') {
466
+ timeoutId.unref();
467
+ }
468
+ });
469
+ try {
470
+ const timed = Promise.race([
471
+ callPromise,
472
+ timeoutPromise
473
+ ]);
474
+ return await timed;
475
+ } finally {
476
+ if (timeoutId) {
477
+ clearTimeout(timeoutId);
478
+ }
479
+ }
480
+ } catch (e) {
481
+ const msg = String(e?.message || e || '');
482
+ if (msg.includes('Unknown blueprint action')) {
483
+ return { structuredContent: { success: false, error: msg } };
484
+ }
485
+ throw e;
486
+ }
487
+ };
488
+
489
+ // Run each test case
490
+ for (let i = 0; i < testCases.length; i++) {
491
+ const testCase = testCases[i];
492
+ const testCaseTimeoutMs = Number(process.env.UNREAL_MCP_TEST_CASE_TIMEOUT_MS ?? testCase.arguments?.timeoutMs ?? '180000');
493
+ const startTime = performance.now();
494
+
495
+ try {
496
+ // Log test start to Unreal Engine console
497
+ const cleanScenario = (testCase.scenario || 'Unknown Test').replace(/"/g, "'");
498
+ await callToolOnce({
499
+ name: 'system_control',
500
+ arguments: { action: 'console_command', command: `Log "---- STARTING TEST: ${cleanScenario} ----"` }
501
+ }, 5000).catch(() => { });
502
+ } catch (e) { /* ignore */ }
503
+
504
+ try {
505
+ const response = await callToolOnce({ name: testCase.toolName, arguments: testCase.arguments }, testCaseTimeoutMs);
506
+
507
+ const endTime = performance.now();
508
+ const durationMs = endTime - startTime;
509
+
510
+ let structuredContent = response.structuredContent ?? null;
511
+ if (structuredContent === null && response.content?.length) {
512
+ for (const entry of response.content) {
513
+ if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
514
+ try { structuredContent = JSON.parse(entry.text); break; } catch { }
515
+ }
516
+ }
517
+ const normalizedResponse = { ...response, structuredContent };
518
+ const { passed, reason } = evaluateExpectation(testCase, normalizedResponse);
519
+
520
+ if (!passed) {
521
+ console.log(`[FAILED] ${testCase.scenario} (${durationMs.toFixed(1)} ms) => ${reason}`);
522
+ if (normalizedResponse) {
523
+ console.log(`[DEBUG] Full response for ${testCase.scenario}:`, JSON.stringify(normalizedResponse, null, 2));
524
+ }
525
+ results.push({
526
+ scenario: testCase.scenario,
527
+ toolName: testCase.toolName,
528
+ arguments: testCase.arguments,
529
+ status: 'failed',
530
+ durationMs,
531
+ detail: reason,
532
+ response: normalizedResponse
533
+ });
534
+ } else {
535
+ console.log(`[PASSED] ${testCase.scenario} (${durationMs.toFixed(1)} ms)`);
536
+ results.push({
537
+ scenario: testCase.scenario,
538
+ toolName: testCase.toolName,
539
+ arguments: testCase.arguments,
540
+ status: 'passed',
541
+ durationMs,
542
+ detail: reason
543
+ });
544
+ }
545
+
546
+ } catch (error) {
547
+ const endTime = performance.now();
548
+ const durationMs = endTime - startTime;
549
+ const errorMessage = String(error?.message || error || '');
550
+ const lowerExpected = (testCase.expected || '').toString().toLowerCase();
551
+ const lowerError = errorMessage.toLowerCase();
552
+
553
+ // If the test explicitly expects a timeout (e.g. "timeout|error"), then
554
+ // an MCP/client timeout should be treated as the expected outcome rather
555
+ // than as a hard harness failure. Accept both "timeout" and "timed out"
556
+ // phrasing from different MCP client implementations.
557
+ if (lowerExpected.includes('timeout') && (lowerError.includes('timeout') || lowerError.includes('timed out'))) {
558
+ console.log(`[PASSED] ${testCase.scenario} (${durationMs.toFixed(1)} ms)`);
559
+ results.push({
560
+ scenario: testCase.scenario,
561
+ toolName: testCase.toolName,
562
+ arguments: testCase.arguments,
563
+ status: 'passed',
564
+ durationMs,
565
+ detail: errorMessage
566
+ });
567
+ continue;
568
+ }
569
+
570
+ console.log(`[FAILED] ${testCase.scenario} (${durationMs.toFixed(1)} ms) => Error: ${errorMessage}`);
571
+ results.push({
572
+ scenario: testCase.scenario,
573
+ toolName: testCase.toolName,
574
+ arguments: testCase.arguments,
575
+ status: 'failed',
576
+ durationMs,
577
+ detail: errorMessage
578
+ });
579
+ }
580
+ }
581
+
582
+ const resultsPath = await persistResults(toolName, results);
583
+ summarize(toolName, results, resultsPath);
584
+
585
+ const hasFailures = results.some((result) => result.status === 'failed');
586
+ process.exitCode = hasFailures ? 1 : 0;
587
+
588
+ } catch (error) {
589
+ console.error('Test runner failed:', error);
590
+ process.exit(1);
591
+ } finally {
592
+ if (client) {
593
+ try {
594
+ await client.close();
595
+ } catch {
596
+ // ignore
597
+ }
598
+ }
599
+ if (transport) {
600
+ try {
601
+ await transport.close();
602
+ } catch {
603
+ // ignore
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ export class TestRunner {
610
+ constructor(suiteName) {
611
+ this.suiteName = suiteName || 'Test Suite';
612
+ this.steps = [];
613
+ }
614
+
615
+ addStep(name, fn) {
616
+ this.steps.push({ name, fn });
617
+ }
618
+
619
+ async run() {
620
+ if (this.steps.length === 0) {
621
+ console.warn(`No steps registered for ${this.suiteName}`);
622
+ return;
623
+ }
624
+
625
+ console.log('\n' + '='.repeat(60));
626
+ console.log(`${this.suiteName}`);
627
+ console.log('='.repeat(60));
628
+ console.log(`Total steps: ${this.steps.length}`);
629
+ console.log('');
630
+
631
+ let transport;
632
+ let client;
633
+ const results = [];
634
+
635
+ try {
636
+ const bridgeHost = process.env.MCP_AUTOMATION_WS_HOST ?? '127.0.0.1';
637
+ const envPorts = process.env.MCP_AUTOMATION_WS_PORTS
638
+ ? process.env.MCP_AUTOMATION_WS_PORTS.split(',').map((p) => parseInt(p.trim(), 10)).filter(Boolean)
639
+ : [8090, 8091];
640
+ const waitMs = parseInt(process.env.UNREAL_MCP_WAIT_PORT_MS ?? '5000', 10);
641
+
642
+ async function waitForAnyPort(host, ports, timeoutMs = 10000) {
643
+ const start = Date.now();
644
+ while (Date.now() - start < timeoutMs) {
645
+ for (const port of ports) {
646
+ try {
647
+ await new Promise((resolve, reject) => {
648
+ const sock = new net.Socket();
649
+ let settled = false;
650
+ sock.setTimeout(1000);
651
+ sock.once('connect', () => { settled = true; sock.destroy(); resolve(true); });
652
+ sock.once('timeout', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('timeout')); } });
653
+ sock.once('error', () => { if (!settled) { settled = true; sock.destroy(); reject(new Error('error')); } });
654
+ sock.connect(port, host);
655
+ });
656
+ console.log(`✅ Automation bridge appears to be listening on ${host}:${port}`);
657
+ return port;
658
+ } catch {
659
+ }
660
+ }
661
+ await new Promise((r) => setImmediate(r));
662
+ }
663
+ throw new Error(`Timed out waiting for automation bridge on ports: ${ports.join(',')}`);
664
+ }
665
+
666
+ try {
667
+ await waitForAnyPort(bridgeHost, envPorts, waitMs);
668
+ } catch (err) {
669
+ console.warn('Automation bridge did not become available before tests started:', err.message);
670
+ }
671
+
672
+ const distPath = path.join(repoRoot, 'dist', 'cli.js');
673
+ const srcDir = path.join(repoRoot, 'src');
674
+
675
+ async function getLatestMtime(dir) {
676
+ let latest = 0;
677
+ try {
678
+ const entries = await fs.readdir(dir, { withFileTypes: true });
679
+ for (const e of entries) {
680
+ const full = path.join(dir, e.name);
681
+ if (e.isDirectory()) {
682
+ const child = await getLatestMtime(full);
683
+ if (child > latest) latest = child;
684
+ } else {
685
+ try {
686
+ const st = await fs.stat(full);
687
+ const m = st.mtimeMs || 0;
688
+ if (m > latest) latest = m;
689
+ } catch (_) { }
690
+ }
691
+ }
692
+ } catch (_) {
693
+ }
694
+ return latest;
695
+ }
696
+
697
+ let useDist = false;
698
+ let distExists = false;
699
+ try {
700
+ await fs.access(distPath);
701
+ distExists = true;
702
+ } catch (e) {
703
+ distExists = false;
704
+ }
705
+
706
+ if (process.env.UNREAL_MCP_FORCE_DIST === '1') {
707
+ useDist = true;
708
+ console.log('Forcing use of dist build via UNREAL_MCP_FORCE_DIST=1');
709
+ } else if (distExists) {
710
+ try {
711
+ const distStat = await fs.stat(distPath);
712
+ const srcLatest = await getLatestMtime(srcDir);
713
+ const srcIsNewer = srcLatest > (distStat.mtimeMs || 0);
714
+ const autoBuildEnabled = process.env.UNREAL_MCP_AUTO_BUILD === '1';
715
+ const autoBuildDisabled = process.env.UNREAL_MCP_NO_AUTO_BUILD === '1';
716
+ if (srcIsNewer) {
717
+ if (!autoBuildEnabled && !autoBuildDisabled) {
718
+ console.log('Detected newer source files than dist; attempting automatic build to refresh dist/ (set UNREAL_MCP_NO_AUTO_BUILD=1 to disable)');
719
+ }
720
+ if (autoBuildEnabled || !autoBuildDisabled) {
721
+ const { spawn } = await import('node:child_process');
722
+ try {
723
+ await new Promise((resolve, reject) => {
724
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
725
+ const ps = spawn(npmCmd, ['run', 'build'], { cwd: repoRoot, stdio: 'inherit', shell: process.platform === 'win32' });
726
+ ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
727
+ ps.on('error', (err) => reject(err));
728
+ });
729
+ console.log('Build succeeded — using dist/ for live tests');
730
+ useDist = true;
731
+ } catch (buildErr) {
732
+ console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
733
+ useDist = false;
734
+ }
735
+ } else {
736
+ console.log('Detected newer source files than dist but automatic build is disabled.');
737
+ console.log('Set UNREAL_MCP_AUTO_BUILD=1 to enable automatic builds, or run `npm run build` manually.');
738
+ useDist = false;
739
+ }
740
+ } else {
741
+ useDist = true;
742
+ console.log('Using built dist for live tests');
743
+ }
744
+ } catch (buildErr) {
745
+ console.warn('Automatic build failed or could not stat files — falling back to TypeScript source for live tests:', String(buildErr));
746
+ useDist = false;
747
+ console.log('Preferring TypeScript source for tests to pick up local changes (set UNREAL_MCP_FORCE_DIST=1 to force dist)');
748
+ }
749
+ } else {
750
+ console.log('dist not found — attempting to run `npm run build` to produce dist/ for live tests');
751
+ try {
752
+ const { spawn } = await import('node:child_process');
753
+ await new Promise((resolve, reject) => {
754
+ const ps = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['run', 'build'], { cwd: repoRoot, stdio: 'inherit' });
755
+ ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Build failed with code ${code}`))));
756
+ ps.on('error', (err) => reject(err));
757
+ });
758
+ useDist = true;
759
+ console.log('Build succeeded — using dist/ for live tests');
760
+ } catch (buildErr) {
761
+ console.warn('Automatic build failed — falling back to running TypeScript source with ts-node-esm:', String(buildErr));
762
+ useDist = false;
763
+ }
764
+ }
765
+
766
+ if (!useDist) {
767
+ serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? 'npx';
768
+ serverArgs = ['ts-node-esm', path.join(repoRoot, 'src', 'cli.ts')];
769
+ } else {
770
+ serverCommand = process.env.UNREAL_MCP_SERVER_CMD ?? serverCommand;
771
+ serverArgs = process.env.UNREAL_MCP_SERVER_ARGS?.split(',') ?? serverArgs;
772
+ }
773
+
774
+ transport = new StdioClientTransport({
775
+ command: serverCommand,
776
+ args: serverArgs,
777
+ cwd: serverCwd,
778
+ stderr: 'inherit',
779
+ env: serverEnv
780
+ });
781
+
782
+ client = new Client({
783
+ name: 'unreal-mcp-step-runner',
784
+ version: '1.0.0'
785
+ });
786
+
787
+ await client.connect(transport);
788
+ await client.listTools({});
789
+ console.log('✅ Connected to Unreal MCP Server\n');
790
+
791
+ const callToolOnce = async function (callOptions, baseTimeoutMs) {
792
+ const envDefault = Number(process.env.UNREAL_MCP_TEST_CALL_TIMEOUT_MS ?? '60000') || 60000;
793
+ const perCall = Number(callOptions?.arguments?.timeoutMs) || undefined;
794
+ const base = typeof baseTimeoutMs === 'number' && baseTimeoutMs > 0 ? baseTimeoutMs : (perCall || envDefault);
795
+ const timeoutMs = base;
796
+ try {
797
+ console.log(`[CALL] ${callOptions.name} (timeout ${timeoutMs}ms)`);
798
+ const outgoing = Object.assign({}, callOptions, { arguments: { ...(callOptions.arguments || {}), timeoutMs } });
799
+ let callPromise;
800
+ try {
801
+ callPromise = client.callTool(outgoing, undefined, { timeout: timeoutMs });
802
+ } catch (err) {
803
+ try {
804
+ callPromise = client.callTool(outgoing, { timeout: timeoutMs });
805
+ } catch (inner) {
806
+ try {
807
+ callPromise = client.callTool(outgoing);
808
+ } catch (inner2) {
809
+ throw inner2 || inner || err;
810
+ }
811
+ }
812
+ }
813
+
814
+ let timeoutId;
815
+ const timeoutPromise = new Promise((_, rej) => {
816
+ timeoutId = setTimeout(() => rej(new Error(`Local test runner timeout after ${timeoutMs}ms`)), timeoutMs);
817
+ if (timeoutId && typeof timeoutId.unref === 'function') {
818
+ timeoutId.unref();
819
+ }
820
+ });
821
+ try {
822
+ const timed = Promise.race([
823
+ callPromise,
824
+ timeoutPromise
825
+ ]);
826
+ return await timed;
827
+ } finally {
828
+ if (timeoutId) {
829
+ clearTimeout(timeoutId);
830
+ }
831
+ }
832
+ } catch (e) {
833
+ const msg = String(e?.message || e || '');
834
+ if (msg.includes('Unknown blueprint action')) {
835
+ return { structuredContent: { success: false, error: msg } };
836
+ }
837
+ throw e;
838
+ }
839
+ };
840
+
841
+ const tools = {
842
+ async executeTool(toolName, args, options = {}) {
843
+ const timeoutMs = typeof options.timeoutMs === 'number' ? options.timeoutMs : undefined;
844
+ const response = await callToolOnce({ name: toolName, arguments: args }, timeoutMs);
845
+ let structuredContent = response.structuredContent ?? null;
846
+ if (structuredContent === null && response.content?.length) {
847
+ for (const entry of response.content) {
848
+ if (entry?.type !== 'text' || typeof entry.text !== 'string') continue;
849
+ try {
850
+ structuredContent = JSON.parse(entry.text);
851
+ break;
852
+ } catch {
853
+ }
854
+ }
855
+ }
856
+
857
+ if (structuredContent && typeof structuredContent === 'object') {
858
+ return structuredContent;
859
+ }
860
+
861
+ return {
862
+ success: !response.isError,
863
+ message: undefined,
864
+ error: undefined
865
+ };
866
+ }
867
+ };
868
+
869
+ for (const step of this.steps) {
870
+ const startTime = performance.now();
871
+
872
+ try {
873
+ // Log step start to Unreal Engine console
874
+ const cleanName = (step.name || 'Unknown Step').replace(/"/g, "'");
875
+ await callToolOnce({
876
+ name: 'system_control',
877
+ arguments: { action: 'console_command', command: `Log "---- STARTING STEP: ${cleanName} ----"` }
878
+ }, 5000).catch(() => { });
879
+ } catch (e) { /* ignore */ }
880
+
881
+ try {
882
+ const ok = await step.fn(tools);
883
+ const durationMs = performance.now() - startTime;
884
+ const status = ok ? 'passed' : 'failed';
885
+ console.log(formatResultLine({ scenario: step.name }, status, ok ? '' : 'Step returned false', durationMs));
886
+ results.push({
887
+ scenario: step.name,
888
+ toolName: null,
889
+ arguments: null,
890
+ status,
891
+ durationMs,
892
+ detail: ok ? undefined : 'Step returned false'
893
+ });
894
+ } catch (err) {
895
+ const durationMs = performance.now() - startTime;
896
+ const detail = err?.message || String(err);
897
+ console.log(formatResultLine({ scenario: step.name }, 'failed', detail, durationMs));
898
+ results.push({
899
+ scenario: step.name,
900
+ toolName: null,
901
+ arguments: null,
902
+ status: 'failed',
903
+ durationMs,
904
+ detail
905
+ });
906
+ }
907
+ }
908
+
909
+ const resultsPath = await persistResults(this.suiteName, results);
910
+ summarize(this.suiteName, results, resultsPath);
911
+
912
+ const hasFailures = results.some((result) => result.status === 'failed');
913
+ process.exitCode = hasFailures ? 1 : 0;
914
+ } catch (error) {
915
+ console.error('Step-based test runner failed:', error);
916
+ process.exit(1);
917
+ } finally {
918
+ if (client) {
919
+ try {
920
+ await client.close();
921
+ } catch {
922
+ }
923
+ }
924
+ if (transport) {
925
+ try {
926
+ await transport.close();
927
+ } catch {
928
+ }
929
+ }
930
+ }
931
+ }
932
+ }
933
+