unreal-engine-mcp-server 0.4.7 → 0.5.1

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