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,2670 @@
1
+ #include "LevelSequence.h"
2
+ #include "McpAutomationBridgeGlobals.h"
3
+ #include "McpAutomationBridgeHelpers.h"
4
+ #include "McpAutomationBridgeSubsystem.h"
5
+ #include "MovieScene.h"
6
+ #include "MovieSceneBinding.h"
7
+ #include "MovieSceneSection.h"
8
+ #include "MovieSceneSequence.h"
9
+ #include "MovieSceneTrack.h"
10
+
11
+ #if WITH_EDITOR
12
+ #include "Editor.h"
13
+ #include "EditorAssetLibrary.h"
14
+ #if __has_include("Subsystems/EditorActorSubsystem.h")
15
+ #include "Subsystems/EditorActorSubsystem.h"
16
+ #define MCP_HAS_EDITOR_ACTOR_SUBSYSTEM 1
17
+ #elif __has_include("EditorActorSubsystem.h")
18
+ #include "EditorActorSubsystem.h"
19
+ #define MCP_HAS_EDITOR_ACTOR_SUBSYSTEM 1
20
+ #else
21
+ #define MCP_HAS_EDITOR_ACTOR_SUBSYSTEM 0
22
+ #endif
23
+
24
+ #include "AssetRegistry/AssetRegistryModule.h"
25
+ #include "AssetToolsModule.h"
26
+ #include "Editor/EditorEngine.h"
27
+ #include "Engine/Selection.h"
28
+ #include "IAssetTools.h"
29
+ #include "LevelSequenceEditorBlueprintLibrary.h"
30
+ #include "Subsystems/AssetEditorSubsystem.h"
31
+
32
+ // Header checks removed causing issues with private headers
33
+
34
+ #if __has_include("LevelSequenceEditorSubsystem.h")
35
+ #include "LevelSequenceEditorSubsystem.h"
36
+ #define MCP_HAS_LEVELSEQUENCE_EDITOR_SUBSYSTEM 1
37
+ #else
38
+ #define MCP_HAS_LEVELSEQUENCE_EDITOR_SUBSYSTEM 0
39
+ #endif
40
+
41
+ #if __has_include("ILevelSequenceEditorToolkit.h")
42
+ #include "ILevelSequenceEditorToolkit.h"
43
+ #endif
44
+
45
+ #if __has_include("ISequencer.h")
46
+ #include "ISequencer.h"
47
+ #include "MovieSceneSequencePlayer.h"
48
+ #endif
49
+
50
+ #if __has_include("Tracks/MovieSceneFloatTrack.h")
51
+ #include "Sections/MovieSceneFloatSection.h"
52
+ #include "Tracks/MovieSceneFloatTrack.h"
53
+
54
+ #endif
55
+
56
+ #if __has_include("Tracks/MovieSceneBoolTrack.h")
57
+ #include "Sections/MovieSceneBoolSection.h"
58
+ #include "Tracks/MovieSceneBoolTrack.h"
59
+
60
+ #endif
61
+
62
+ #if __has_include("Tracks/MovieScene3DTransformTrack.h")
63
+ #include "Tracks/MovieScene3DTransformTrack.h"
64
+ #endif
65
+
66
+ #include "Tracks/MovieSceneAudioTrack.h"
67
+ #include "Tracks/MovieSceneEventTrack.h"
68
+
69
+ #if __has_include("Sections/MovieScene3DTransformSection.h")
70
+ #include "Sections/MovieScene3DTransformSection.h"
71
+ #endif
72
+ #if __has_include("Channels/MovieSceneDoubleChannel.h")
73
+ #include "Channels/MovieSceneDoubleChannel.h"
74
+ #endif
75
+ #if __has_include("Channels/MovieSceneChannelProxy.h")
76
+ #include "Channels/MovieSceneChannelProxy.h"
77
+ #endif
78
+
79
+ // Optional components check
80
+ #if __has_include("Misc/ScopedTransaction.h")
81
+ #include "Misc/ScopedTransaction.h"
82
+ #endif
83
+ #if __has_include("Camera/CameraActor.h")
84
+ #include "Camera/CameraActor.h"
85
+ #endif
86
+ #endif
87
+
88
+ FString UMcpAutomationBridgeSubsystem::ResolveSequencePath(
89
+ const TSharedPtr<FJsonObject> &Payload) {
90
+ FString Path;
91
+ if (Payload.IsValid() && Payload->TryGetStringField(TEXT("path"), Path) &&
92
+ !Path.IsEmpty()) {
93
+ #if WITH_EDITOR
94
+ // Check existence first to avoid error log spam
95
+ if (UEditorAssetLibrary::DoesAssetExist(Path)) {
96
+ UObject *Obj = UEditorAssetLibrary::LoadAsset(Path);
97
+ if (Obj) {
98
+ return Obj->GetPathName();
99
+ }
100
+ }
101
+ #endif
102
+ return Path;
103
+ }
104
+ if (!GCurrentSequencePath.IsEmpty())
105
+ return GCurrentSequencePath;
106
+ return FString();
107
+ }
108
+
109
+ TSharedPtr<FJsonObject>
110
+ UMcpAutomationBridgeSubsystem::EnsureSequenceEntry(const FString &SeqPath) {
111
+ if (SeqPath.IsEmpty())
112
+ return nullptr;
113
+ if (TSharedPtr<FJsonObject> *Found = GSequenceRegistry.Find(SeqPath))
114
+ return *Found;
115
+ TSharedPtr<FJsonObject> NewObj = MakeShared<FJsonObject>();
116
+ NewObj->SetStringField(TEXT("sequencePath"), SeqPath);
117
+ GSequenceRegistry.Add(SeqPath, NewObj);
118
+ return NewObj;
119
+ }
120
+
121
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceCreate(
122
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
123
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
124
+ #if WITH_EDITOR
125
+ TSharedPtr<FJsonObject> LocalPayload =
126
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
127
+ FString Name;
128
+ LocalPayload->TryGetStringField(TEXT("name"), Name);
129
+ FString Path;
130
+ LocalPayload->TryGetStringField(TEXT("path"), Path);
131
+ if (Name.IsEmpty()) {
132
+ SendAutomationResponse(Socket, RequestId, false,
133
+ TEXT("sequence_create requires name"), nullptr,
134
+ TEXT("INVALID_ARGUMENT"));
135
+ return true;
136
+ }
137
+ FString FullPath = Path.IsEmpty()
138
+ ? FString::Printf(TEXT("/Game/%s"), *Name)
139
+ : FString::Printf(TEXT("%s/%s"), *Path, *Name);
140
+
141
+ FString DestFolder = Path.IsEmpty() ? TEXT("/Game") : Path;
142
+ if (DestFolder.StartsWith(TEXT("/Content"), ESearchCase::IgnoreCase)) {
143
+ DestFolder = FString::Printf(TEXT("/Game%s"), *DestFolder.RightChop(8));
144
+ }
145
+
146
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
147
+ FString RequestIdArg = RequestId;
148
+
149
+ // Execute on Game Thread
150
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
151
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
152
+ TEXT("HandleSequenceCreate: Handing RequestID=%s Path=%s"),
153
+ *RequestIdArg, *FullPath);
154
+
155
+ // Check existence first to avoid error log spam
156
+ if (UEditorAssetLibrary::DoesAssetExist(FullPath)) {
157
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
158
+ Resp->SetStringField(TEXT("sequencePath"), FullPath);
159
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
160
+ TEXT("HandleSequenceCreate: Sequence exists, sending response for "
161
+ "RequestID=%s"),
162
+ *RequestIdArg);
163
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
164
+ TEXT("Sequence already exists"), Resp,
165
+ FString());
166
+ return true;
167
+ }
168
+
169
+ // Dynamic factory lookup
170
+ UClass *FactoryClass = FindObject<UClass>(
171
+ nullptr, TEXT("/Script/LevelSequenceEditor.LevelSequenceFactoryNew"));
172
+ if (!FactoryClass)
173
+ FactoryClass = LoadClass<UClass>(
174
+ nullptr, TEXT("/Script/LevelSequenceEditor.LevelSequenceFactoryNew"));
175
+
176
+ if (FactoryClass) {
177
+ UFactory *Factory =
178
+ NewObject<UFactory>(GetTransientPackage(), FactoryClass);
179
+ FAssetToolsModule &AssetToolsModule =
180
+ FModuleManager::LoadModuleChecked<FAssetToolsModule>(
181
+ TEXT("AssetTools"));
182
+ UObject *NewObj = AssetToolsModule.Get().CreateAsset(
183
+ Name, DestFolder, ULevelSequence::StaticClass(), Factory);
184
+ if (NewObj) {
185
+ UEditorAssetLibrary::SaveAsset(FullPath);
186
+ GCurrentSequencePath = FullPath;
187
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
188
+ Resp->SetStringField(TEXT("sequencePath"), FullPath);
189
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
190
+ TEXT("HandleSequenceCreate: Created sequence, sending response "
191
+ "for RequestID=%s"),
192
+ *RequestIdArg);
193
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
194
+ TEXT("Sequence created"), Resp,
195
+ FString());
196
+ } else {
197
+ UE_LOG(
198
+ LogMcpAutomationBridgeSubsystem, Error,
199
+ TEXT("HandleSequenceCreate: Failed to create asset for RequestID=%s"),
200
+ *RequestIdArg);
201
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
202
+ TEXT("Failed to create sequence asset"),
203
+ nullptr, TEXT("CREATE_ASSET_FAILED"));
204
+ }
205
+ } else {
206
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Error,
207
+ TEXT("HandleSequenceCreate: Factory not found for RequestID=%s"),
208
+ *RequestIdArg);
209
+ Subsystem->SendAutomationResponse(
210
+ Socket, RequestIdArg, false,
211
+ TEXT("LevelSequenceFactoryNew class not found (Module not loaded?)"),
212
+ nullptr, TEXT("FACTORY_NOT_AVAILABLE"));
213
+ }
214
+ return true;
215
+ return true;
216
+
217
+ #else
218
+ SendAutomationResponse(Socket, RequestId, false,
219
+ TEXT("sequence_create requires editor build"), nullptr,
220
+ TEXT("NOT_AVAILABLE"));
221
+ return true;
222
+ #endif
223
+ }
224
+
225
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetDisplayRate(
226
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
227
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
228
+ #if WITH_EDITOR
229
+ TSharedPtr<FJsonObject> LocalPayload =
230
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
231
+ FString SeqPath = ResolveSequencePath(LocalPayload);
232
+ if (SeqPath.IsEmpty()) {
233
+ SendAutomationResponse(
234
+ Socket, RequestId, false,
235
+ TEXT("sequence_set_display_rate requires a sequence path"), nullptr,
236
+ TEXT("INVALID_SEQUENCE"));
237
+ return true;
238
+ }
239
+
240
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
241
+ if (!SeqObj) {
242
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
243
+ nullptr, TEXT("INVALID_SEQUENCE"));
244
+ return true;
245
+ }
246
+
247
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
248
+ if (UMovieScene *MovieScene = LevelSeq->GetMovieScene()) {
249
+ FString FrameRateStr;
250
+ double FrameRateVal = 0.0;
251
+
252
+ FFrameRate NewRate;
253
+ bool bRateFound = false;
254
+
255
+ if (LocalPayload->TryGetStringField(TEXT("frameRate"), FrameRateStr)) {
256
+ // Parse "30fps", "24000/1001", etc.
257
+ // Simple parsing for standard rates
258
+ if (FrameRateStr.EndsWith(TEXT("fps"))) {
259
+ FrameRateStr.RemoveFromEnd(TEXT("fps"));
260
+ NewRate = FFrameRate(FCString::Atoi(*FrameRateStr), 1);
261
+ bRateFound = true;
262
+ } else if (FrameRateStr.Contains(TEXT("/"))) {
263
+ // Rational
264
+ FString NumStr, DenomStr;
265
+ if (FrameRateStr.Split(TEXT("/"), &NumStr, &DenomStr)) {
266
+ NewRate =
267
+ FFrameRate(FCString::Atoi(*NumStr), FCString::Atoi(*DenomStr));
268
+ bRateFound = true;
269
+ }
270
+ } else {
271
+ // Decimal string?
272
+ if (FrameRateStr.IsNumeric()) {
273
+ NewRate = FFrameRate(FCString::Atoi(*FrameRateStr), 1);
274
+ bRateFound = true;
275
+ }
276
+ }
277
+ } else if (LocalPayload->TryGetNumberField(TEXT("frameRate"),
278
+ FrameRateVal)) {
279
+ NewRate = FFrameRate(FMath::RoundToInt(FrameRateVal), 1);
280
+ bRateFound = true;
281
+ }
282
+
283
+ if (bRateFound) {
284
+ MovieScene->SetDisplayRate(NewRate);
285
+ MovieScene->Modify();
286
+
287
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
288
+ Resp->SetBoolField(TEXT("success"), true);
289
+ Resp->SetStringField(TEXT("displayRate"),
290
+ NewRate.ToPrettyText().ToString());
291
+ SendAutomationResponse(Socket, RequestId, true,
292
+ TEXT("Display rate set"), Resp, FString());
293
+ return true;
294
+ }
295
+
296
+ SendAutomationResponse(Socket, RequestId, false,
297
+ TEXT("Invalid frameRate format"), nullptr,
298
+ TEXT("INVALID_ARGUMENT"));
299
+ return true;
300
+ }
301
+ }
302
+
303
+ SendAutomationResponse(Socket, RequestId, false,
304
+ TEXT("Invalid sequence type"), nullptr,
305
+ TEXT("INVALID_SEQUENCE"));
306
+ return true;
307
+ #else
308
+ SendAutomationResponse(
309
+ Socket, RequestId, false,
310
+ TEXT("sequence_set_display_rate requires editor build"), nullptr,
311
+ TEXT("NOT_IMPLEMENTED"));
312
+ return true;
313
+ #endif
314
+ }
315
+
316
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetProperties(
317
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
318
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
319
+ TSharedPtr<FJsonObject> LocalPayload =
320
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
321
+ FString SeqPath = ResolveSequencePath(LocalPayload);
322
+ if (SeqPath.IsEmpty()) {
323
+ SendAutomationResponse(
324
+ Socket, RequestId, false,
325
+ TEXT("sequence_set_properties requires a sequence path"), nullptr,
326
+ TEXT("INVALID_SEQUENCE"));
327
+ return true;
328
+ }
329
+
330
+ #if WITH_EDITOR
331
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
332
+ FString RequestIdArg = RequestId;
333
+
334
+ // Capture simple types. For JsonObject, we need to capture the data, not the
335
+ // pointer if we want to be safe, but since we parsed it above, we should
336
+ // capture the parsed values. Parsing logic happens above. We'll capture the
337
+ // parsed variables. But wait, the parsing logic in the original code is
338
+ // INSIDE the block I'm replacing (lines 176-185). I need to include the
339
+ // parsing inside the Async task or move it out. I'll move the parsing INSIDE
340
+ // the Async task, but I need to capture LocalPayload. LocalPayload is a
341
+ // SharedPtr, so it's safe to capture.
342
+
343
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
344
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
345
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
346
+ if (!SeqObj) {
347
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
348
+ TEXT("Sequence not found"), nullptr,
349
+ TEXT("INVALID_SEQUENCE"));
350
+ return true;
351
+ }
352
+
353
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
354
+ if (UMovieScene *MovieScene = LevelSeq->GetMovieScene()) {
355
+ bool bModified = false;
356
+ double FrameRateValue = 0.0;
357
+ double LengthInFramesValue = 0.0;
358
+ double PlaybackStartValue = 0.0;
359
+ double PlaybackEndValue = 0.0;
360
+
361
+ const bool bHasFrameRate =
362
+ LocalPayload->TryGetNumberField(TEXT("frameRate"), FrameRateValue);
363
+ const bool bHasLengthInFrames = LocalPayload->TryGetNumberField(
364
+ TEXT("lengthInFrames"), LengthInFramesValue);
365
+ const bool bHasPlaybackStart = LocalPayload->TryGetNumberField(
366
+ TEXT("playbackStart"), PlaybackStartValue);
367
+ const bool bHasPlaybackEnd = LocalPayload->TryGetNumberField(
368
+ TEXT("playbackEnd"), PlaybackEndValue);
369
+
370
+ if (bHasFrameRate) {
371
+ if (FrameRateValue <= 0.0) {
372
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
373
+ TEXT("frameRate must be > 0"),
374
+ nullptr, TEXT("INVALID_ARGUMENT"));
375
+ return true;
376
+ }
377
+ const int32 Rounded =
378
+ FMath::Clamp<int32>(FMath::RoundToInt(FrameRateValue), 1, 960);
379
+ FFrameRate CurrentRate = MovieScene->GetDisplayRate();
380
+ FFrameRate NewRate(Rounded, 1);
381
+ if (NewRate != CurrentRate) {
382
+ MovieScene->SetDisplayRate(NewRate);
383
+ bModified = true;
384
+ }
385
+ }
386
+
387
+ if (bHasPlaybackStart || bHasPlaybackEnd || bHasLengthInFrames) {
388
+ TRange<FFrameNumber> ExistingRange = MovieScene->GetPlaybackRange();
389
+ FFrameNumber StartFrame = ExistingRange.GetLowerBoundValue();
390
+ FFrameNumber EndFrame = ExistingRange.GetUpperBoundValue();
391
+
392
+ if (bHasPlaybackStart)
393
+ StartFrame = FFrameNumber(static_cast<int32>(PlaybackStartValue));
394
+ if (bHasPlaybackEnd)
395
+ EndFrame = FFrameNumber(static_cast<int32>(PlaybackEndValue));
396
+ else if (bHasLengthInFrames)
397
+ EndFrame =
398
+ StartFrame +
399
+ FMath::Max<int32>(0, static_cast<int32>(LengthInFramesValue));
400
+
401
+ if (EndFrame < StartFrame)
402
+ EndFrame = StartFrame;
403
+ MovieScene->SetPlaybackRange(
404
+ TRange<FFrameNumber>(StartFrame, EndFrame));
405
+ bModified = true;
406
+ }
407
+
408
+ if (bModified)
409
+ MovieScene->Modify();
410
+
411
+ FFrameRate FR = MovieScene->GetDisplayRate();
412
+ TSharedPtr<FJsonObject> FrameRateObj = MakeShared<FJsonObject>();
413
+ FrameRateObj->SetNumberField(TEXT("numerator"), FR.Numerator);
414
+ FrameRateObj->SetNumberField(TEXT("denominator"), FR.Denominator);
415
+ Resp->SetObjectField(TEXT("frameRate"), FrameRateObj);
416
+
417
+ TRange<FFrameNumber> Range = MovieScene->GetPlaybackRange();
418
+ const double Start =
419
+ static_cast<double>(Range.GetLowerBoundValue().Value);
420
+ const double End = static_cast<double>(Range.GetUpperBoundValue().Value);
421
+ Resp->SetNumberField(TEXT("playbackStart"), Start);
422
+ Resp->SetNumberField(TEXT("playbackEnd"), End);
423
+ Resp->SetNumberField(TEXT("duration"), End - Start);
424
+ Resp->SetBoolField(TEXT("applied"), bModified);
425
+
426
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
427
+ TEXT("properties updated"), Resp,
428
+ FString());
429
+ return true;
430
+ }
431
+ }
432
+ Resp->SetObjectField(TEXT("frameRate"), MakeShared<FJsonObject>());
433
+ Resp->SetNumberField(TEXT("playbackStart"), 0.0);
434
+ Resp->SetNumberField(TEXT("playbackEnd"), 0.0);
435
+ Resp->SetNumberField(TEXT("duration"), 0.0);
436
+ Resp->SetBoolField(TEXT("applied"), false);
437
+ Subsystem->SendAutomationResponse(
438
+ Socket, RequestIdArg, false,
439
+ TEXT("sequence_set_properties is not available in this editor build or "
440
+ "for this sequence type"),
441
+ Resp, TEXT("NOT_IMPLEMENTED"));
442
+ return true;
443
+ return true;
444
+ #else
445
+ SendAutomationResponse(Socket, RequestId, false,
446
+ TEXT("sequence_set_properties requires editor build."),
447
+ nullptr, TEXT("NOT_IMPLEMENTED"));
448
+ return true;
449
+ #endif
450
+ }
451
+
452
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceOpen(
453
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
454
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
455
+ TSharedPtr<FJsonObject> LocalPayload =
456
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
457
+ FString SeqPath = ResolveSequencePath(LocalPayload);
458
+ if (SeqPath.IsEmpty()) {
459
+ SendAutomationResponse(Socket, RequestId, false,
460
+ TEXT("sequence_open requires a sequence path"),
461
+ nullptr, TEXT("INVALID_SEQUENCE"));
462
+ return true;
463
+ }
464
+
465
+ #if WITH_EDITOR
466
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
467
+ FString RequestIdArg = RequestId;
468
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
469
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
470
+ TEXT("HandleSequenceOpen: Opening sequence %s for RequestID=%s"),
471
+ *SeqPath, *RequestIdArg);
472
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
473
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
474
+ if (!SeqObj) {
475
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
476
+ TEXT("Sequence not found"), nullptr,
477
+ TEXT("INVALID_SEQUENCE"));
478
+ return true;
479
+ }
480
+
481
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
482
+ if (GEditor) {
483
+ if (ULevelSequenceEditorSubsystem *LSES =
484
+ GEditor->GetEditorSubsystem<ULevelSequenceEditorSubsystem>()) {
485
+ if (UAssetEditorSubsystem *AssetEditorSS =
486
+ GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()) {
487
+ AssetEditorSS->OpenEditorForAsset(LevelSeq);
488
+ Resp->SetStringField(TEXT("sequencePath"), SeqPath);
489
+ Resp->SetStringField(TEXT("message"), TEXT("Sequence opened"));
490
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
491
+ TEXT("HandleSequenceOpen: Successfully opened in LSES, "
492
+ "sending response for RequestID=%s"),
493
+ *RequestIdArg);
494
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
495
+ TEXT("Sequence opened"), Resp,
496
+ FString());
497
+ return true;
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ if (GEditor) {
504
+ if (UAssetEditorSubsystem *AssetEditorSS =
505
+ GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()) {
506
+ AssetEditorSS->OpenEditorForAsset(SeqObj);
507
+ }
508
+ }
509
+ Resp->SetStringField(TEXT("sequencePath"), SeqPath);
510
+ Resp->SetStringField(TEXT("message"), TEXT("Sequence opened (asset editor)"));
511
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
512
+ TEXT("HandleSequenceOpen: Opened via AssetEditorSS, sending response "
513
+ "for RequestID=%s"),
514
+ *RequestIdArg);
515
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
516
+ TEXT("Sequence opened"), Resp, FString());
517
+ return true;
518
+ return true;
519
+ #else
520
+ SendAutomationResponse(Socket, RequestId, false,
521
+ TEXT("sequence_open requires editor build."), nullptr,
522
+ TEXT("NOT_AVAILABLE"));
523
+ return true;
524
+ #endif
525
+ }
526
+
527
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddCamera(
528
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
529
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
530
+ TSharedPtr<FJsonObject> LocalPayload =
531
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
532
+ FString SeqPath = ResolveSequencePath(LocalPayload);
533
+ if (SeqPath.IsEmpty()) {
534
+ SendAutomationResponse(Socket, RequestId, false,
535
+ TEXT("sequence_add_camera requires a sequence path"),
536
+ nullptr, TEXT("INVALID_SEQUENCE"));
537
+ return true;
538
+ }
539
+
540
+ #if WITH_EDITOR
541
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
542
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
543
+ if (!SeqObj) {
544
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
545
+ nullptr, TEXT("INVALID_SEQUENCE"));
546
+ return true;
547
+ }
548
+
549
+ #if MCP_HAS_EDITOR_ACTOR_SUBSYSTEM
550
+ if (GEditor) {
551
+ UClass *CameraClass = ACameraActor::StaticClass();
552
+ AActor *Spawned = SpawnActorInActiveWorld<AActor>(
553
+ CameraClass, FVector::ZeroVector, FRotator::ZeroRotator,
554
+ TEXT("SequenceCamera"));
555
+ if (Spawned) {
556
+ // Fix for Issue #6: Auto-bind the camera to the sequence
557
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
558
+ if (UMovieScene *MovieScene = LevelSeq->GetMovieScene()) {
559
+ FGuid BindingGuid = MovieScene->AddPossessable(
560
+ Spawned->GetActorLabel(), Spawned->GetClass());
561
+ if (MovieScene->FindPossessable(BindingGuid)) {
562
+ MovieScene->Modify();
563
+ Resp->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
564
+ }
565
+ }
566
+ }
567
+
568
+ Resp->SetBoolField(TEXT("success"), true);
569
+ Resp->SetStringField(TEXT("actorLabel"), Spawned->GetActorLabel());
570
+ SendAutomationResponse(Socket, RequestId, true,
571
+ TEXT("Camera actor spawned and bound to sequence"),
572
+ Resp, FString());
573
+ return true;
574
+ }
575
+ }
576
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Failed to add camera"),
577
+ nullptr, TEXT("ADD_CAMERA_FAILED"));
578
+ return true;
579
+ #else
580
+ SendAutomationResponse(Socket, RequestId, false,
581
+ TEXT("UEditorActorSubsystem not available"), nullptr,
582
+ TEXT("NOT_AVAILABLE"));
583
+ return true;
584
+ #endif
585
+ #else
586
+ SendAutomationResponse(Socket, RequestId, false,
587
+ TEXT("sequence_add_camera requires editor build."),
588
+ nullptr, TEXT("NOT_IMPLEMENTED"));
589
+ return true;
590
+ #endif
591
+ }
592
+
593
+ bool UMcpAutomationBridgeSubsystem::HandleSequencePlay(
594
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
595
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
596
+ TSharedPtr<FJsonObject> LocalPayload =
597
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
598
+ FString SeqPath = ResolveSequencePath(LocalPayload);
599
+ if (SeqPath.IsEmpty()) {
600
+ SendAutomationResponse(Socket, RequestId, false,
601
+ TEXT("No sequence selected or path provided"),
602
+ nullptr, TEXT("INVALID_SEQUENCE"));
603
+ return true;
604
+ }
605
+
606
+ #if WITH_EDITOR
607
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
608
+ FString RequestIdArg = RequestId;
609
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
610
+ ULevelSequence *LevelSeq =
611
+ Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SeqPath));
612
+ if (LevelSeq) {
613
+ if (ULevelSequenceEditorBlueprintLibrary::OpenLevelSequence(LevelSeq)) {
614
+ ULevelSequenceEditorBlueprintLibrary::Play();
615
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
616
+ TEXT("Sequence playing"), nullptr);
617
+ return true;
618
+ }
619
+ }
620
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
621
+ TEXT("Failed to open or play sequence"),
622
+ nullptr, TEXT("EXECUTION_ERROR"));
623
+ return true;
624
+ return true;
625
+ #else
626
+ SendAutomationResponse(Socket, RequestId, false,
627
+ TEXT("sequence_play requires editor build."), nullptr,
628
+ TEXT("NOT_AVAILABLE"));
629
+ return true;
630
+ #endif
631
+ }
632
+
633
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddActor(
634
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
635
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
636
+ TSharedPtr<FJsonObject> LocalPayload =
637
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
638
+ FString ActorName;
639
+ LocalPayload->TryGetStringField(TEXT("actorName"), ActorName);
640
+ if (ActorName.IsEmpty()) {
641
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
642
+ nullptr, TEXT("INVALID_ARGUMENT"));
643
+ return true;
644
+ }
645
+ FString SeqPath = ResolveSequencePath(LocalPayload);
646
+ if (SeqPath.IsEmpty()) {
647
+ SendAutomationResponse(Socket, RequestId, false,
648
+ TEXT("sequence_add_actor requires a sequence path"),
649
+ nullptr, TEXT("INVALID_SEQUENCE"));
650
+ return true;
651
+ }
652
+
653
+ #if WITH_EDITOR
654
+ // Reuse multi-actor binding logic for a single actor by forwarding to
655
+ // HandleSequenceAddActors with a one-element actorNames array and the
656
+ // resolved sequence path. This ensures real LevelSequence bindings are
657
+ // applied when supported by the editor build.
658
+ TSharedPtr<FJsonObject> ForwardPayload = MakeShared<FJsonObject>();
659
+ ForwardPayload->SetStringField(TEXT("path"), SeqPath);
660
+ TArray<TSharedPtr<FJsonValue>> NamesArray;
661
+ NamesArray.Add(MakeShared<FJsonValueString>(ActorName));
662
+ ForwardPayload->SetArrayField(TEXT("actorNames"), NamesArray);
663
+
664
+ return HandleSequenceAddActors(RequestId, ForwardPayload, Socket);
665
+ #else
666
+ SendAutomationResponse(Socket, RequestId, false,
667
+ TEXT("sequence_add_actor requires editor build."),
668
+ nullptr, TEXT("NOT_IMPLEMENTED"));
669
+ return true;
670
+ #endif
671
+ }
672
+
673
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddActors(
674
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
675
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
676
+ TSharedPtr<FJsonObject> LocalPayload =
677
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
678
+ const TArray<TSharedPtr<FJsonValue>> *Arr = nullptr;
679
+ LocalPayload->TryGetArrayField(TEXT("actorNames"), Arr);
680
+ if (!Arr || Arr->Num() == 0) {
681
+ SendAutomationResponse(Socket, RequestId, false,
682
+ TEXT("actorNames required"), nullptr,
683
+ TEXT("INVALID_ARGUMENT"));
684
+ return true;
685
+ }
686
+ FString SeqPath = ResolveSequencePath(LocalPayload);
687
+ if (SeqPath.IsEmpty()) {
688
+ SendAutomationResponse(Socket, RequestId, false,
689
+ TEXT("sequence_add_actors requires a sequence path"),
690
+ nullptr, TEXT("INVALID_SEQUENCE"));
691
+ return true;
692
+ }
693
+
694
+ #if WITH_EDITOR
695
+ TArray<FString> Names;
696
+ Names.Reserve(Arr->Num());
697
+ for (const TSharedPtr<FJsonValue> &V : *Arr) {
698
+ if (V.IsValid() && V->Type == EJson::String)
699
+ Names.Add(V->AsString());
700
+ }
701
+
702
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
703
+ FString RequestIdArg = RequestId;
704
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
705
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
706
+ if (!SeqObj) {
707
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
708
+ TEXT("Sequence not found"), nullptr,
709
+ TEXT("INVALID_SEQUENCE"));
710
+ return true;
711
+ }
712
+ if (!GEditor) {
713
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
714
+ TEXT("Editor not available"), nullptr,
715
+ TEXT("EDITOR_NOT_AVAILABLE"));
716
+ return true;
717
+ }
718
+
719
+ #if MCP_HAS_EDITOR_ACTOR_SUBSYSTEM
720
+ if (UEditorActorSubsystem *ActorSS =
721
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
722
+ TArray<TSharedPtr<FJsonValue>> Results;
723
+ Results.Reserve(Names.Num());
724
+ for (const FString &Name : Names) {
725
+ TSharedPtr<FJsonObject> Item = MakeShared<FJsonObject>();
726
+ Item->SetStringField(TEXT("name"), Name);
727
+ // Use robust actor lookup that checks label, name, and UAID
728
+ AActor *Found = Subsystem->FindActorByName(Name);
729
+
730
+ if (!Found) {
731
+ Item->SetBoolField(TEXT("success"), false);
732
+ Item->SetStringField(TEXT("error"), TEXT("Actor not found"));
733
+ } else {
734
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
735
+ UMovieScene *MovieScene = LevelSeq->GetMovieScene();
736
+ if (MovieScene) {
737
+ FGuid BindingGuid = MovieScene->AddPossessable(
738
+ Found->GetActorLabel(), Found->GetClass());
739
+ if (MovieScene->FindPossessable(BindingGuid)) {
740
+ Item->SetBoolField(TEXT("success"), true);
741
+ Item->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
742
+ MovieScene->Modify();
743
+ } else {
744
+ Item->SetBoolField(TEXT("success"), false);
745
+ Item->SetStringField(
746
+ TEXT("error"), TEXT("Failed to create possessable binding"));
747
+ }
748
+ } else {
749
+ Item->SetBoolField(TEXT("success"), false);
750
+ Item->SetStringField(TEXT("error"),
751
+ TEXT("Sequence has no MovieScene"));
752
+ }
753
+ } else {
754
+ Item->SetBoolField(TEXT("success"), false);
755
+ Item->SetStringField(TEXT("error"),
756
+ TEXT("Sequence object is not a LevelSequence"));
757
+ }
758
+ }
759
+ Results.Add(MakeShared<FJsonValueObject>(Item));
760
+ }
761
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
762
+ Out->SetArrayField(TEXT("results"), Results);
763
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
764
+ TEXT("Actors processed"), Out, FString());
765
+ return true;
766
+ }
767
+ Subsystem->SendAutomationResponse(
768
+ Socket, RequestIdArg, false, TEXT("EditorActorSubsystem not available"),
769
+ nullptr, TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
770
+ return true;
771
+ #else
772
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
773
+ TEXT("UEditorActorSubsystem not available"),
774
+ nullptr, TEXT("NOT_AVAILABLE"));
775
+ #endif
776
+ return true;
777
+ #else
778
+ SendAutomationResponse(Socket, RequestId, false,
779
+ TEXT("sequence_add_actors requires editor build."),
780
+ nullptr, TEXT("NOT_IMPLEMENTED"));
781
+ return true;
782
+ #endif
783
+ }
784
+
785
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddSpawnable(
786
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
787
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
788
+ TSharedPtr<FJsonObject> LocalPayload =
789
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
790
+ FString ClassName;
791
+ LocalPayload->TryGetStringField(TEXT("className"), ClassName);
792
+ if (ClassName.IsEmpty()) {
793
+ SendAutomationResponse(Socket, RequestId, false, TEXT("className required"),
794
+ nullptr, TEXT("INVALID_ARGUMENT"));
795
+ return true;
796
+ }
797
+ FString SeqPath = ResolveSequencePath(LocalPayload);
798
+ if (SeqPath.IsEmpty()) {
799
+ SendAutomationResponse(
800
+ Socket, RequestId, false,
801
+ TEXT("sequence_add_spawnable_from_class requires a sequence path"),
802
+ nullptr, TEXT("INVALID_SEQUENCE"));
803
+ return true;
804
+ }
805
+
806
+ #if WITH_EDITOR
807
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
808
+ if (!SeqObj) {
809
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
810
+ nullptr, TEXT("INVALID_SEQUENCE"));
811
+ return true;
812
+ }
813
+
814
+ UClass *ResolvedClass = nullptr;
815
+ if (ClassName.StartsWith(TEXT("/")) || ClassName.Contains(TEXT("/"))) {
816
+ if (UObject *Loaded = UEditorAssetLibrary::LoadAsset(ClassName)) {
817
+ if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
818
+ ResolvedClass = BP->GeneratedClass;
819
+ else if (UClass *C = Cast<UClass>(Loaded))
820
+ ResolvedClass = C;
821
+ }
822
+ }
823
+ if (!ResolvedClass)
824
+ ResolvedClass = ResolveClassByName(ClassName);
825
+ if (!ResolvedClass) {
826
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Class not found"),
827
+ nullptr, TEXT("CLASS_NOT_FOUND"));
828
+ return true;
829
+ }
830
+
831
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
832
+ UMovieScene *MovieScene = LevelSeq->GetMovieScene();
833
+ if (MovieScene) {
834
+ UObject *DefaultObject = ResolvedClass->GetDefaultObject();
835
+ if (DefaultObject) {
836
+ FGuid BindingGuid = MovieScene->AddSpawnable(ClassName, *DefaultObject);
837
+ if (MovieScene->FindSpawnable(BindingGuid)) {
838
+ MovieScene->Modify();
839
+ TSharedPtr<FJsonObject> SpawnableResp = MakeShared<FJsonObject>();
840
+ SpawnableResp->SetBoolField(TEXT("success"), true);
841
+ SpawnableResp->SetStringField(TEXT("className"), ClassName);
842
+ SpawnableResp->SetStringField(TEXT("bindingGuid"),
843
+ BindingGuid.ToString());
844
+ SendAutomationResponse(Socket, RequestId, true,
845
+ TEXT("Spawnable added to sequence"),
846
+ SpawnableResp, FString());
847
+ return true;
848
+ }
849
+ }
850
+ }
851
+ SendAutomationResponse(Socket, RequestId, false,
852
+ TEXT("Failed to create spawnable binding"), nullptr,
853
+ TEXT("SPAWNABLE_CREATION_FAILED"));
854
+ return true;
855
+ }
856
+ SendAutomationResponse(Socket, RequestId, false,
857
+ TEXT("Sequence object is not a LevelSequence"),
858
+ nullptr, TEXT("INVALID_SEQUENCE_TYPE"));
859
+ return true;
860
+ #else
861
+ SendAutomationResponse(
862
+ Socket, RequestId, false,
863
+ TEXT("sequence_add_spawnable_from_class requires editor build."), nullptr,
864
+ TEXT("NOT_IMPLEMENTED"));
865
+ return true;
866
+ #endif
867
+ }
868
+
869
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceRemoveActors(
870
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
871
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
872
+ TSharedPtr<FJsonObject> LocalPayload =
873
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
874
+ const TArray<TSharedPtr<FJsonValue>> *Arr = nullptr;
875
+ LocalPayload->TryGetArrayField(TEXT("actorNames"), Arr);
876
+ if (!Arr || Arr->Num() == 0) {
877
+ SendAutomationResponse(Socket, RequestId, false,
878
+ TEXT("actorNames required"), nullptr,
879
+ TEXT("INVALID_ARGUMENT"));
880
+ return true;
881
+ }
882
+ FString SeqPath = ResolveSequencePath(LocalPayload);
883
+ if (SeqPath.IsEmpty()) {
884
+ SendAutomationResponse(
885
+ Socket, RequestId, false,
886
+ TEXT("sequence_remove_actors requires a sequence path"), nullptr,
887
+ TEXT("INVALID_SEQUENCE"));
888
+ return true;
889
+ }
890
+
891
+ #if WITH_EDITOR
892
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
893
+ if (!SeqObj) {
894
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
895
+ nullptr, TEXT("INVALID_SEQUENCE"));
896
+ return true;
897
+ }
898
+ if (!GEditor) {
899
+ SendAutomationResponse(Socket, RequestId, false,
900
+ TEXT("Editor not available"), nullptr,
901
+ TEXT("EDITOR_NOT_AVAILABLE"));
902
+ return true;
903
+ }
904
+
905
+ #if MCP_HAS_EDITOR_ACTOR_SUBSYSTEM
906
+ if (UEditorActorSubsystem *ActorSS =
907
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
908
+ TArray<TSharedPtr<FJsonValue>> Removed;
909
+ int32 RemovedCount = 0;
910
+ for (const TSharedPtr<FJsonValue> &V : *Arr) {
911
+ if (!V.IsValid() || V->Type != EJson::String)
912
+ continue;
913
+ FString Name = V->AsString();
914
+ TSharedPtr<FJsonObject> Item = MakeShared<FJsonObject>();
915
+ Item->SetStringField(TEXT("name"), Name);
916
+
917
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
918
+ UMovieScene *MovieScene = LevelSeq->GetMovieScene();
919
+ if (MovieScene) {
920
+ bool bRemoved = false;
921
+ for (const FMovieSceneBinding &Binding :
922
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
923
+ FString BindingName;
924
+ if (FMovieScenePossessable *Possessable =
925
+ MovieScene->FindPossessable(Binding.GetObjectGuid())) {
926
+ BindingName = Possessable->GetName();
927
+ } else if (FMovieSceneSpawnable *Spawnable =
928
+ MovieScene->FindSpawnable(Binding.GetObjectGuid())) {
929
+ BindingName = Spawnable->GetName();
930
+ }
931
+
932
+ if (BindingName.Equals(Name, ESearchCase::IgnoreCase)) {
933
+ MovieScene->RemovePossessable(Binding.GetObjectGuid());
934
+ MovieScene->Modify();
935
+ bRemoved = true;
936
+ break;
937
+ }
938
+ }
939
+ if (bRemoved) {
940
+ Item->SetBoolField(TEXT("success"), true);
941
+ Item->SetStringField(TEXT("status"), TEXT("Actor removed"));
942
+ RemovedCount++;
943
+ } else {
944
+ Item->SetBoolField(TEXT("success"), false);
945
+ Item->SetStringField(TEXT("error"),
946
+ TEXT("Actor not found in sequence bindings"));
947
+ }
948
+ } else {
949
+ Item->SetBoolField(TEXT("success"), false);
950
+ Item->SetStringField(TEXT("error"),
951
+ TEXT("Sequence has no MovieScene"));
952
+ }
953
+ } else {
954
+ Item->SetBoolField(TEXT("success"), false);
955
+ Item->SetStringField(TEXT("error"),
956
+ TEXT("Sequence object is not a LevelSequence"));
957
+ }
958
+ Removed.Add(MakeShared<FJsonValueObject>(Item));
959
+ }
960
+ TSharedPtr<FJsonObject> Out = MakeShared<FJsonObject>();
961
+ Out->SetArrayField(TEXT("removedActors"), Removed);
962
+ Out->SetNumberField(TEXT("bindingsProcessed"), RemovedCount);
963
+ SendAutomationResponse(Socket, RequestId, true,
964
+ TEXT("Actors processed for removal"), Out,
965
+ FString());
966
+ return true;
967
+ }
968
+ SendAutomationResponse(Socket, RequestId, false,
969
+ TEXT("EditorActorSubsystem not available"), nullptr,
970
+ TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
971
+ return true;
972
+ #else
973
+ SendAutomationResponse(Socket, RequestId, false,
974
+ TEXT("UEditorActorSubsystem not available"), nullptr,
975
+ TEXT("NOT_AVAILABLE"));
976
+ return true;
977
+ #endif
978
+ #else
979
+ SendAutomationResponse(Socket, RequestId, false,
980
+ TEXT("sequence_remove_actors requires editor build."),
981
+ nullptr, TEXT("NOT_IMPLEMENTED"));
982
+ return true;
983
+ #endif
984
+ }
985
+
986
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceGetBindings(
987
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
988
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
989
+ TSharedPtr<FJsonObject> LocalPayload =
990
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
991
+ FString SeqPath = ResolveSequencePath(LocalPayload);
992
+ if (SeqPath.IsEmpty()) {
993
+ SendAutomationResponse(
994
+ Socket, RequestId, false,
995
+ TEXT("sequence_get_bindings requires a sequence path"), nullptr,
996
+ TEXT("INVALID_SEQUENCE"));
997
+ return true;
998
+ }
999
+ #if WITH_EDITOR
1000
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1001
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
1002
+ if (!SeqObj) {
1003
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1004
+ nullptr, TEXT("INVALID_SEQUENCE"));
1005
+ return true;
1006
+ }
1007
+
1008
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
1009
+ if (UMovieScene *MovieScene = LevelSeq->GetMovieScene()) {
1010
+ TArray<TSharedPtr<FJsonValue>> BindingsArray;
1011
+ for (const FMovieSceneBinding &B :
1012
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
1013
+ TSharedPtr<FJsonObject> Bobj = MakeShared<FJsonObject>();
1014
+ Bobj->SetStringField(TEXT("id"), B.GetObjectGuid().ToString());
1015
+
1016
+ FString BindingName;
1017
+ if (FMovieScenePossessable *Possessable =
1018
+ MovieScene->FindPossessable(B.GetObjectGuid())) {
1019
+ BindingName = Possessable->GetName();
1020
+ } else if (FMovieSceneSpawnable *Spawnable =
1021
+ MovieScene->FindSpawnable(B.GetObjectGuid())) {
1022
+ BindingName = Spawnable->GetName();
1023
+ }
1024
+
1025
+ Bobj->SetStringField(TEXT("name"), BindingName);
1026
+ BindingsArray.Add(MakeShared<FJsonValueObject>(Bobj));
1027
+ }
1028
+ Resp->SetArrayField(TEXT("bindings"), BindingsArray);
1029
+ SendAutomationResponse(Socket, RequestId, true, TEXT("bindings listed"),
1030
+ Resp, FString());
1031
+ return true;
1032
+ }
1033
+ }
1034
+ Resp->SetArrayField(TEXT("bindings"), TArray<TSharedPtr<FJsonValue>>());
1035
+ SendAutomationResponse(Socket, RequestId, true,
1036
+ TEXT("bindings listed (empty)"), Resp, FString());
1037
+ return true;
1038
+ #else
1039
+ SendAutomationResponse(Socket, RequestId, false,
1040
+ TEXT("sequence_get_bindings requires editor build."),
1041
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1042
+ return true;
1043
+ #endif
1044
+ }
1045
+
1046
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceGetProperties(
1047
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1048
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1049
+ TSharedPtr<FJsonObject> LocalPayload =
1050
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1051
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1052
+ if (SeqPath.IsEmpty()) {
1053
+ SendAutomationResponse(
1054
+ Socket, RequestId, false,
1055
+ TEXT("sequence_get_properties requires a sequence path"), nullptr,
1056
+ TEXT("INVALID_SEQUENCE"));
1057
+ return true;
1058
+ }
1059
+ #if WITH_EDITOR
1060
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1061
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
1062
+ if (!SeqObj) {
1063
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1064
+ nullptr, TEXT("INVALID_SEQUENCE"));
1065
+ return true;
1066
+ }
1067
+
1068
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
1069
+ if (UMovieScene *MovieScene = LevelSeq->GetMovieScene()) {
1070
+ FFrameRate FR = MovieScene->GetDisplayRate();
1071
+ TSharedPtr<FJsonObject> FrameRateObj = MakeShared<FJsonObject>();
1072
+ FrameRateObj->SetNumberField(TEXT("numerator"), FR.Numerator);
1073
+ FrameRateObj->SetNumberField(TEXT("denominator"), FR.Denominator);
1074
+ Resp->SetObjectField(TEXT("frameRate"), FrameRateObj);
1075
+ TRange<FFrameNumber> Range = MovieScene->GetPlaybackRange();
1076
+ const double Start =
1077
+ static_cast<double>(Range.GetLowerBoundValue().Value);
1078
+ const double End = static_cast<double>(Range.GetUpperBoundValue().Value);
1079
+ Resp->SetNumberField(TEXT("playbackStart"), Start);
1080
+ Resp->SetNumberField(TEXT("playbackEnd"), End);
1081
+ Resp->SetNumberField(TEXT("duration"), End - Start);
1082
+ SendAutomationResponse(Socket, RequestId, true,
1083
+ TEXT("properties retrieved"), Resp, FString());
1084
+ return true;
1085
+ }
1086
+ }
1087
+ Resp->SetObjectField(TEXT("frameRate"), MakeShared<FJsonObject>());
1088
+ Resp->SetNumberField(TEXT("playbackStart"), 0.0);
1089
+ Resp->SetNumberField(TEXT("playbackEnd"), 0.0);
1090
+ Resp->SetNumberField(TEXT("duration"), 0.0);
1091
+ SendAutomationResponse(Socket, RequestId, true, TEXT("properties retrieved"),
1092
+ Resp, FString());
1093
+ return true;
1094
+ #else
1095
+ SendAutomationResponse(Socket, RequestId, false,
1096
+ TEXT("sequence_get_properties requires editor build."),
1097
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1098
+ return true;
1099
+ #endif
1100
+ }
1101
+
1102
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetPlaybackSpeed(
1103
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1104
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1105
+ TSharedPtr<FJsonObject> LocalPayload =
1106
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1107
+ double Speed = 1.0;
1108
+ LocalPayload->TryGetNumberField(TEXT("speed"), Speed);
1109
+ if (Speed <= 0.0) {
1110
+ SendAutomationResponse(Socket, RequestId, false,
1111
+ TEXT("Invalid speed (must be > 0)"), nullptr,
1112
+ TEXT("INVALID_ARGUMENT"));
1113
+ return true;
1114
+ }
1115
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1116
+ if (SeqPath.IsEmpty()) {
1117
+ SendAutomationResponse(
1118
+ Socket, RequestId, false,
1119
+ TEXT("sequence_set_playback_speed requires a sequence path"), nullptr,
1120
+ TEXT("INVALID_SEQUENCE"));
1121
+ return true;
1122
+ }
1123
+
1124
+ #if WITH_EDITOR
1125
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
1126
+ FString RequestIdArg = RequestId; // Capture
1127
+
1128
+ // Execute on Game Thread
1129
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
1130
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
1131
+ if (!SeqObj) {
1132
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, false,
1133
+ TEXT("Sequence not found"), nullptr,
1134
+ TEXT("INVALID_SEQUENCE"));
1135
+ return true;
1136
+ }
1137
+
1138
+ if (GEditor) {
1139
+ if (UAssetEditorSubsystem *AssetEditorSS =
1140
+ GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()) {
1141
+ IAssetEditorInstance *Editor =
1142
+ AssetEditorSS->FindEditorForAsset(SeqObj, false);
1143
+ if (Editor && Editor->GetEditorName() == FName("LevelSequenceEditor")) {
1144
+ // We assume it implements ILevelSequenceEditorToolkit if the name
1145
+ // matches
1146
+ ILevelSequenceEditorToolkit *LSEditor =
1147
+ static_cast<ILevelSequenceEditorToolkit *>(Editor);
1148
+ if (LSEditor && LSEditor->GetSequencer().IsValid()) {
1149
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1150
+ TEXT("HandleSequenceSetPlaybackSpeed: Setting speed to %.2f"),
1151
+ Speed);
1152
+ LSEditor->GetSequencer()->SetPlaybackSpeed(static_cast<float>(Speed));
1153
+ Subsystem->SendAutomationResponse(
1154
+ Socket, RequestIdArg, true,
1155
+ FString::Printf(TEXT("Playback speed set to %.2f"), Speed),
1156
+ nullptr);
1157
+ return true;
1158
+ } else {
1159
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Error,
1160
+ TEXT("HandleSequenceSetPlaybackSpeed: Sequencer invalid for "
1161
+ "asset %s"),
1162
+ *SeqObj->GetName());
1163
+ }
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ Subsystem->SendAutomationResponse(
1169
+ Socket, RequestIdArg, false,
1170
+ TEXT("Sequence editor not open or interface unavailable"), nullptr,
1171
+ TEXT("EDITOR_NOT_OPEN"));
1172
+ return true;
1173
+ return true;
1174
+ #else
1175
+ SendAutomationResponse(
1176
+ Socket, RequestId, false,
1177
+ TEXT("sequence_set_playback_speed requires editor build."), nullptr,
1178
+ TEXT("NOT_AVAILABLE"));
1179
+ return true;
1180
+ #endif
1181
+ }
1182
+
1183
+ bool UMcpAutomationBridgeSubsystem::HandleSequencePause(
1184
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1185
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1186
+ TSharedPtr<FJsonObject> LocalPayload =
1187
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1188
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1189
+ if (SeqPath.IsEmpty()) {
1190
+ SendAutomationResponse(Socket, RequestId, false,
1191
+ TEXT("sequence_pause requires a sequence path"),
1192
+ nullptr, TEXT("INVALID_SEQUENCE"));
1193
+ return true;
1194
+ }
1195
+ #if WITH_EDITOR
1196
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
1197
+ FString RequestIdArg = RequestId;
1198
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
1199
+
1200
+ ULevelSequence *LevelSeq =
1201
+ Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SeqPath));
1202
+ if (LevelSeq) {
1203
+ // Ensure it's the active one
1204
+ if (ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence() ==
1205
+ LevelSeq) {
1206
+ ULevelSequenceEditorBlueprintLibrary::Pause();
1207
+ Subsystem->SendAutomationResponse(Socket, RequestIdArg, true,
1208
+ TEXT("Sequence paused"), nullptr);
1209
+ return true;
1210
+ }
1211
+ }
1212
+ Subsystem->SendAutomationResponse(
1213
+ Socket, RequestIdArg, false,
1214
+ TEXT("Sequence not currently open in editor"), nullptr,
1215
+ TEXT("EXECUTION_ERROR"));
1216
+ return true;
1217
+ return true;
1218
+ #else
1219
+ SendAutomationResponse(Socket, RequestId, false,
1220
+ TEXT("sequence_pause requires editor build."), nullptr,
1221
+ TEXT("NOT_AVAILABLE"));
1222
+ return true;
1223
+ #endif
1224
+ }
1225
+
1226
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceStop(
1227
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1228
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1229
+ TSharedPtr<FJsonObject> LocalPayload =
1230
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1231
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1232
+ if (SeqPath.IsEmpty()) {
1233
+ SendAutomationResponse(Socket, RequestId, false,
1234
+ TEXT("sequence_stop requires a sequence path"),
1235
+ nullptr, TEXT("INVALID_SEQUENCE"));
1236
+ return true;
1237
+ }
1238
+ #if WITH_EDITOR
1239
+ TWeakObjectPtr<UMcpAutomationBridgeSubsystem> WeakSubsystem(this);
1240
+ FString RequestIdArg = RequestId;
1241
+ UMcpAutomationBridgeSubsystem *Subsystem = this;
1242
+
1243
+ ULevelSequence *LevelSeq =
1244
+ Cast<ULevelSequence>(UEditorAssetLibrary::LoadAsset(SeqPath));
1245
+ if (LevelSeq) {
1246
+ if (ULevelSequenceEditorBlueprintLibrary::GetCurrentLevelSequence() ==
1247
+ LevelSeq) {
1248
+ ULevelSequenceEditorBlueprintLibrary::Pause();
1249
+
1250
+ FMovieSceneSequencePlaybackParams PlaybackParams;
1251
+ PlaybackParams.Frame = FFrameTime(0);
1252
+ PlaybackParams.UpdateMethod = EUpdatePositionMethod::Scrub;
1253
+ ULevelSequenceEditorBlueprintLibrary::SetGlobalPosition(PlaybackParams);
1254
+
1255
+ Subsystem->SendAutomationResponse(
1256
+ Socket, RequestIdArg, true, TEXT("Sequence stopped (reset to start)"),
1257
+ nullptr);
1258
+ return true;
1259
+ }
1260
+ }
1261
+ Subsystem->SendAutomationResponse(
1262
+ Socket, RequestIdArg, false,
1263
+ TEXT("Sequence not currently open in editor"), nullptr,
1264
+ TEXT("EXECUTION_ERROR"));
1265
+ return true;
1266
+ return true;
1267
+ #else
1268
+ SendAutomationResponse(Socket, RequestId, false,
1269
+ TEXT("sequence_stop requires editor build."), nullptr,
1270
+ TEXT("NOT_AVAILABLE"));
1271
+ return true;
1272
+ #endif
1273
+ }
1274
+
1275
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceList(
1276
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1277
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1278
+ #if WITH_EDITOR
1279
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1280
+ TArray<TSharedPtr<FJsonValue>> SequencesArray;
1281
+
1282
+ // Use Asset Registry to find all LevelSequence assets, not string matching
1283
+ FAssetRegistryModule &AssetRegistryModule =
1284
+ FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
1285
+ IAssetRegistry &AssetRegistry = AssetRegistryModule.Get();
1286
+
1287
+ FARFilter Filter;
1288
+ Filter.ClassPaths.Add(ULevelSequence::StaticClass()->GetClassPathName());
1289
+ Filter.bRecursiveClasses = true;
1290
+ Filter.bRecursivePaths = true;
1291
+ Filter.PackagePaths.Add(FName("/Game"));
1292
+
1293
+ TArray<FAssetData> AssetList;
1294
+ AssetRegistry.GetAssets(Filter, AssetList);
1295
+
1296
+ for (const FAssetData &Asset : AssetList) {
1297
+ TSharedPtr<FJsonObject> SeqObj = MakeShared<FJsonObject>();
1298
+ SeqObj->SetStringField(TEXT("path"), Asset.GetObjectPathString());
1299
+ SeqObj->SetStringField(TEXT("name"), Asset.AssetName.ToString());
1300
+ SequencesArray.Add(MakeShared<FJsonValueObject>(SeqObj));
1301
+ }
1302
+
1303
+ Resp->SetArrayField(TEXT("sequences"), SequencesArray);
1304
+ Resp->SetNumberField(TEXT("count"), SequencesArray.Num());
1305
+ SendAutomationResponse(
1306
+ Socket, RequestId, true,
1307
+ FString::Printf(TEXT("Found %d sequences"), SequencesArray.Num()), Resp,
1308
+ FString());
1309
+ return true;
1310
+ #else
1311
+ SendAutomationResponse(Socket, RequestId, false,
1312
+ TEXT("sequence_list requires editor build."), nullptr,
1313
+ TEXT("NOT_AVAILABLE"));
1314
+ return true;
1315
+ #endif
1316
+ }
1317
+
1318
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceDuplicate(
1319
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1320
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1321
+ TSharedPtr<FJsonObject> LocalPayload =
1322
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1323
+ FString SourcePath;
1324
+ LocalPayload->TryGetStringField(TEXT("path"), SourcePath);
1325
+ FString DestinationPath;
1326
+ LocalPayload->TryGetStringField(TEXT("destinationPath"), DestinationPath);
1327
+ if (SourcePath.IsEmpty() || DestinationPath.IsEmpty()) {
1328
+ SendAutomationResponse(
1329
+ Socket, RequestId, false,
1330
+ TEXT("sequence_duplicate requires path and destinationPath"), nullptr,
1331
+ TEXT("INVALID_ARGUMENT"));
1332
+ return true;
1333
+ }
1334
+
1335
+ // Auto-resolve relative destination path (if just a name is provided)
1336
+ if (!DestinationPath.IsEmpty() && !DestinationPath.StartsWith(TEXT("/"))) {
1337
+ FString ParentPath = FPaths::GetPath(SourcePath);
1338
+ DestinationPath =
1339
+ FString::Printf(TEXT("%s/%s"), *ParentPath, *DestinationPath);
1340
+ }
1341
+
1342
+ #if WITH_EDITOR
1343
+ UObject *SourceSeq = UEditorAssetLibrary::LoadAsset(SourcePath);
1344
+ if (!SourceSeq) {
1345
+ SendAutomationResponse(
1346
+ Socket, RequestId, false,
1347
+ FString::Printf(TEXT("Source sequence not found: %s"), *SourcePath),
1348
+ nullptr, TEXT("INVALID_SEQUENCE"));
1349
+ return true;
1350
+ }
1351
+ UObject *DuplicatedSeq =
1352
+ UEditorAssetLibrary::DuplicateAsset(SourcePath, DestinationPath);
1353
+ if (DuplicatedSeq) {
1354
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1355
+ Resp->SetStringField(TEXT("sourcePath"), SourcePath);
1356
+ Resp->SetStringField(TEXT("destinationPath"), DestinationPath);
1357
+ Resp->SetStringField(TEXT("duplicatedPath"), DuplicatedSeq->GetPathName());
1358
+ SendAutomationResponse(Socket, RequestId, true,
1359
+ TEXT("Sequence duplicated successfully"), Resp,
1360
+ FString());
1361
+ return true;
1362
+ }
1363
+ SendAutomationResponse(Socket, RequestId, false,
1364
+ TEXT("Failed to duplicate sequence"), nullptr,
1365
+ TEXT("OPERATION_FAILED"));
1366
+ return true;
1367
+ #else
1368
+ SendAutomationResponse(Socket, RequestId, false,
1369
+ TEXT("sequence_duplicate requires editor build."),
1370
+ nullptr, TEXT("NOT_AVAILABLE"));
1371
+ return true;
1372
+ #endif
1373
+ }
1374
+
1375
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceRename(
1376
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1377
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1378
+ TSharedPtr<FJsonObject> LocalPayload =
1379
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1380
+ FString Path;
1381
+ LocalPayload->TryGetStringField(TEXT("path"), Path);
1382
+ FString NewName;
1383
+ LocalPayload->TryGetStringField(TEXT("newName"), NewName);
1384
+ if (Path.IsEmpty() || NewName.IsEmpty()) {
1385
+ SendAutomationResponse(Socket, RequestId, false,
1386
+ TEXT("sequence_rename requires path and newName"),
1387
+ nullptr, TEXT("INVALID_ARGUMENT"));
1388
+ return true;
1389
+ }
1390
+
1391
+ // Auto-resolve relative new name to full path
1392
+ if (!NewName.IsEmpty() && !NewName.StartsWith(TEXT("/"))) {
1393
+ FString ParentPath = FPaths::GetPath(Path);
1394
+ NewName = FString::Printf(TEXT("%s/%s"), *ParentPath, *NewName);
1395
+ }
1396
+
1397
+ #if WITH_EDITOR
1398
+ if (UEditorAssetLibrary::RenameAsset(Path, NewName)) {
1399
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1400
+ Resp->SetStringField(TEXT("oldPath"), Path);
1401
+ Resp->SetStringField(TEXT("newName"), NewName);
1402
+ SendAutomationResponse(Socket, RequestId, true,
1403
+ TEXT("Sequence renamed successfully"), Resp,
1404
+ FString());
1405
+ return true;
1406
+ }
1407
+ SendAutomationResponse(Socket, RequestId, false,
1408
+ TEXT("Failed to rename sequence"), nullptr,
1409
+ TEXT("OPERATION_FAILED"));
1410
+ return true;
1411
+ #else
1412
+ SendAutomationResponse(Socket, RequestId, false,
1413
+ TEXT("sequence_rename requires editor build."),
1414
+ nullptr, TEXT("NOT_AVAILABLE"));
1415
+ return true;
1416
+ #endif
1417
+ }
1418
+
1419
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceDelete(
1420
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1421
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1422
+ TSharedPtr<FJsonObject> LocalPayload =
1423
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1424
+ FString Path;
1425
+ LocalPayload->TryGetStringField(TEXT("path"), Path);
1426
+ if (Path.IsEmpty()) {
1427
+ SendAutomationResponse(Socket, RequestId, false,
1428
+ TEXT("sequence_delete requires path"), nullptr,
1429
+ TEXT("INVALID_ARGUMENT"));
1430
+ return true;
1431
+ }
1432
+ #if WITH_EDITOR
1433
+ if (!UEditorAssetLibrary::DoesAssetExist(Path)) {
1434
+ // Idempotent success - if it's already gone, good.
1435
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1436
+ Resp->SetStringField(TEXT("deletedPath"), Path);
1437
+ SendAutomationResponse(Socket, RequestId, true,
1438
+ TEXT("Sequence deleted (or did not exist)"), Resp,
1439
+ FString());
1440
+ return true;
1441
+ }
1442
+
1443
+ if (UEditorAssetLibrary::DeleteAsset(Path)) {
1444
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1445
+ Resp->SetStringField(TEXT("deletedPath"), Path);
1446
+ SendAutomationResponse(Socket, RequestId, true,
1447
+ TEXT("Sequence deleted successfully"), Resp,
1448
+ FString());
1449
+ return true;
1450
+ }
1451
+ SendAutomationResponse(Socket, RequestId, false,
1452
+ TEXT("Failed to delete sequence"), nullptr,
1453
+ TEXT("OPERATION_FAILED"));
1454
+ return true;
1455
+ #else
1456
+ SendAutomationResponse(Socket, RequestId, false,
1457
+ TEXT("sequence_delete requires editor build."),
1458
+ nullptr, TEXT("NOT_AVAILABLE"));
1459
+ return true;
1460
+ #endif
1461
+ }
1462
+
1463
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceGetMetadata(
1464
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1465
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1466
+ TSharedPtr<FJsonObject> LocalPayload =
1467
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1468
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1469
+ if (SeqPath.IsEmpty()) {
1470
+ SendAutomationResponse(
1471
+ Socket, RequestId, false,
1472
+ TEXT("sequence_get_metadata requires a sequence path"), nullptr,
1473
+ TEXT("INVALID_SEQUENCE"));
1474
+ return true;
1475
+ }
1476
+ #if WITH_EDITOR
1477
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
1478
+ if (!SeqObj) {
1479
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1480
+ nullptr, TEXT("INVALID_SEQUENCE"));
1481
+ return true;
1482
+ }
1483
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1484
+ Resp->SetStringField(TEXT("path"), SeqPath);
1485
+ Resp->SetStringField(TEXT("name"), SeqObj->GetName());
1486
+ Resp->SetStringField(TEXT("class"), SeqObj->GetClass()->GetName());
1487
+ SendAutomationResponse(Socket, RequestId, true,
1488
+ TEXT("Sequence metadata retrieved"), Resp, FString());
1489
+ return true;
1490
+ #else
1491
+ SendAutomationResponse(Socket, RequestId, false,
1492
+ TEXT("sequence_get_metadata requires editor build."),
1493
+ nullptr, TEXT("NOT_AVAILABLE"));
1494
+ return true;
1495
+ #endif
1496
+ }
1497
+
1498
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddKeyframe(
1499
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1500
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1501
+ TSharedPtr<FJsonObject> LocalPayload =
1502
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
1503
+ FString SeqPath = ResolveSequencePath(LocalPayload);
1504
+ if (SeqPath.IsEmpty()) {
1505
+ SendAutomationResponse(
1506
+ Socket, RequestId, false,
1507
+ TEXT("sequence_add_keyframe requires a sequence path"), nullptr,
1508
+ TEXT("INVALID_SEQUENCE"));
1509
+ return true;
1510
+ }
1511
+
1512
+ FString BindingIdStr;
1513
+ LocalPayload->TryGetStringField(TEXT("bindingId"), BindingIdStr);
1514
+ FString ActorName;
1515
+ LocalPayload->TryGetStringField(TEXT("actorName"), ActorName);
1516
+ FString PropertyName;
1517
+ LocalPayload->TryGetStringField(TEXT("property"), PropertyName);
1518
+
1519
+ if (BindingIdStr.IsEmpty() && ActorName.IsEmpty()) {
1520
+ SendAutomationResponse(
1521
+ Socket, RequestId, false,
1522
+ TEXT("Either bindingId or actorName must be provided. bindingId is the "
1523
+ "GUID from add_actor/get_bindings. actorName is the label of an "
1524
+ "actor already bound to the sequence. Example: {\"actorName\": "
1525
+ "\"MySphere\", \"property\": \"Location\", \"frame\": 0, "
1526
+ "\"value\": {\"x\":0,\"y\":0,\"z\":0}}"),
1527
+ nullptr, TEXT("INVALID_ARGUMENT"));
1528
+ return true;
1529
+ }
1530
+
1531
+ double Frame = 0.0;
1532
+ if (!LocalPayload->TryGetNumberField(TEXT("frame"), Frame)) {
1533
+ SendAutomationResponse(Socket, RequestId, false,
1534
+ TEXT("frame number is required. Example: "
1535
+ "{\"frame\": 30} for keyframe at frame 30"),
1536
+ nullptr, TEXT("INVALID_ARGUMENT"));
1537
+ return true;
1538
+ }
1539
+
1540
+ #if WITH_EDITOR
1541
+ UObject *SeqObj = UEditorAssetLibrary::LoadAsset(SeqPath);
1542
+ if (!SeqObj) {
1543
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1544
+ nullptr, TEXT("INVALID_SEQUENCE"));
1545
+ return true;
1546
+ }
1547
+
1548
+ if (ULevelSequence *LevelSeq = Cast<ULevelSequence>(SeqObj)) {
1549
+ UMovieScene *MovieScene = LevelSeq->GetMovieScene();
1550
+ if (MovieScene) {
1551
+ FGuid BindingGuid;
1552
+ if (!BindingIdStr.IsEmpty()) {
1553
+ FGuid::Parse(BindingIdStr, BindingGuid);
1554
+ } else if (!ActorName.IsEmpty()) {
1555
+ for (const FMovieSceneBinding &Binding :
1556
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
1557
+ FString BindingName;
1558
+ if (FMovieScenePossessable *Possessable =
1559
+ MovieScene->FindPossessable(Binding.GetObjectGuid())) {
1560
+ BindingName = Possessable->GetName();
1561
+ } else if (FMovieSceneSpawnable *Spawnable =
1562
+ MovieScene->FindSpawnable(Binding.GetObjectGuid())) {
1563
+ BindingName = Spawnable->GetName();
1564
+ }
1565
+
1566
+ if (BindingName.Equals(ActorName, ESearchCase::IgnoreCase)) {
1567
+ BindingGuid = Binding.GetObjectGuid();
1568
+ break;
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ if (!BindingGuid.IsValid()) {
1574
+ FString Target = !BindingIdStr.IsEmpty() ? BindingIdStr : ActorName;
1575
+ SendAutomationResponse(
1576
+ Socket, RequestId, false,
1577
+ FString::Printf(TEXT("Binding not found for '%s'. Ensure actor is "
1578
+ "bound to sequence."),
1579
+ *Target),
1580
+ nullptr, TEXT("BINDING_NOT_FOUND"));
1581
+ return true;
1582
+ }
1583
+
1584
+ FMovieSceneBinding *Binding = MovieScene->FindBinding(BindingGuid);
1585
+ if (!Binding) {
1586
+ SendAutomationResponse(Socket, RequestId, false,
1587
+ TEXT("Binding object not found in sequence"),
1588
+ nullptr, TEXT("BINDING_NOT_FOUND"));
1589
+ return true;
1590
+ }
1591
+
1592
+ if (PropertyName.Equals(TEXT("Transform"), ESearchCase::IgnoreCase)) {
1593
+ UMovieScene3DTransformTrack *Track =
1594
+ MovieScene->FindTrack<UMovieScene3DTransformTrack>(
1595
+ BindingGuid, FName("Transform"));
1596
+ if (!Track) {
1597
+ Track =
1598
+ MovieScene->AddTrack<UMovieScene3DTransformTrack>(BindingGuid);
1599
+ }
1600
+
1601
+ if (Track) {
1602
+ bool bSectionAdded = false;
1603
+ UMovieScene3DTransformSection *Section =
1604
+ Cast<UMovieScene3DTransformSection>(
1605
+ Track->FindOrAddSection(0, bSectionAdded));
1606
+ if (Section) {
1607
+ FFrameRate TickResolution = MovieScene->GetTickResolution();
1608
+ FFrameRate DisplayRate = MovieScene->GetDisplayRate();
1609
+ FFrameNumber FrameNum = FFrameNumber(static_cast<int32>(Frame));
1610
+ FFrameNumber TickFrame =
1611
+ FFrameRate::TransformTime(FFrameTime(FrameNum), DisplayRate,
1612
+ TickResolution)
1613
+ .FloorToFrame();
1614
+
1615
+ bool bModified = false;
1616
+ const TSharedPtr<FJsonObject> *ValueObj = nullptr;
1617
+
1618
+ FMovieSceneChannelProxy &Proxy = Section->GetChannelProxy();
1619
+ TArrayView<FMovieSceneDoubleChannel *> Channels =
1620
+ Proxy.GetChannels<FMovieSceneDoubleChannel>();
1621
+
1622
+ if (LocalPayload->TryGetObjectField(TEXT("value"), ValueObj) &&
1623
+ ValueObj && Channels.Num() >= 9) {
1624
+ const TSharedPtr<FJsonObject> *LocObj = nullptr;
1625
+ if ((*ValueObj)->TryGetObjectField(TEXT("location"), LocObj)) {
1626
+ double X, Y, Z;
1627
+ if ((*LocObj)->TryGetNumberField(TEXT("x"), X)) {
1628
+ Channels[0]->GetData().AddKey(TickFrame,
1629
+ FMovieSceneDoubleValue(X));
1630
+ bModified = true;
1631
+ }
1632
+ if ((*LocObj)->TryGetNumberField(TEXT("y"), Y)) {
1633
+ Channels[1]->GetData().AddKey(TickFrame,
1634
+ FMovieSceneDoubleValue(Y));
1635
+ bModified = true;
1636
+ }
1637
+ if ((*LocObj)->TryGetNumberField(TEXT("z"), Z)) {
1638
+ Channels[2]->GetData().AddKey(TickFrame,
1639
+ FMovieSceneDoubleValue(Z));
1640
+ bModified = true;
1641
+ }
1642
+ }
1643
+
1644
+ const TSharedPtr<FJsonObject> *RotObj = nullptr;
1645
+ if ((*ValueObj)->TryGetObjectField(TEXT("rotation"), RotObj)) {
1646
+ double P, Yaw, R;
1647
+ // 0=Roll(X), 1=Pitch(Y), 2=Yaw(Z) in Transform Track channels
1648
+ // usually. Channels 3, 4, 5.
1649
+ if ((*RotObj)->TryGetNumberField(TEXT("roll"), R)) {
1650
+ Channels[3]->GetData().AddKey(TickFrame,
1651
+ FMovieSceneDoubleValue(R));
1652
+ bModified = true;
1653
+ }
1654
+ if ((*RotObj)->TryGetNumberField(TEXT("pitch"), P)) {
1655
+ Channels[4]->GetData().AddKey(TickFrame,
1656
+ FMovieSceneDoubleValue(P));
1657
+ bModified = true;
1658
+ }
1659
+ if ((*RotObj)->TryGetNumberField(TEXT("yaw"), Yaw)) {
1660
+ Channels[5]->GetData().AddKey(TickFrame,
1661
+ FMovieSceneDoubleValue(Yaw));
1662
+ bModified = true;
1663
+ }
1664
+ }
1665
+
1666
+ const TSharedPtr<FJsonObject> *ScaleObj = nullptr;
1667
+ if ((*ValueObj)->TryGetObjectField(TEXT("scale"), ScaleObj)) {
1668
+ double X, Y, Z;
1669
+ if ((*ScaleObj)->TryGetNumberField(TEXT("x"), X)) {
1670
+ Channels[6]->GetData().AddKey(TickFrame,
1671
+ FMovieSceneDoubleValue(X));
1672
+ bModified = true;
1673
+ }
1674
+ if ((*ScaleObj)->TryGetNumberField(TEXT("y"), Y)) {
1675
+ Channels[7]->GetData().AddKey(TickFrame,
1676
+ FMovieSceneDoubleValue(Y));
1677
+ bModified = true;
1678
+ }
1679
+ if ((*ScaleObj)->TryGetNumberField(TEXT("z"), Z)) {
1680
+ Channels[8]->GetData().AddKey(TickFrame,
1681
+ FMovieSceneDoubleValue(Z));
1682
+ bModified = true;
1683
+ }
1684
+ }
1685
+ }
1686
+
1687
+ if (bModified) {
1688
+ MovieScene->Modify();
1689
+ SendAutomationResponse(Socket, RequestId, true,
1690
+ TEXT("Keyframe added"), nullptr,
1691
+ FString());
1692
+ return true;
1693
+ }
1694
+ }
1695
+ }
1696
+ } else {
1697
+ // Try generic property tracks
1698
+ const TSharedPtr<FJsonValue> Val =
1699
+ LocalPayload->TryGetField(TEXT("value"));
1700
+ if (Val.IsValid() && Val->Type == EJson::Number) {
1701
+ UMovieSceneFloatTrack *Track =
1702
+ MovieScene->FindTrack<UMovieSceneFloatTrack>(
1703
+ BindingGuid, FName(*PropertyName));
1704
+ if (!Track) {
1705
+ Track = MovieScene->AddTrack<UMovieSceneFloatTrack>(BindingGuid);
1706
+ if (Track)
1707
+ Track->SetPropertyNameAndPath(FName(*PropertyName), PropertyName);
1708
+ }
1709
+ if (Track) {
1710
+ bool bSectionAdded = false;
1711
+ UMovieSceneFloatSection *Section = Cast<UMovieSceneFloatSection>(
1712
+ Track->FindOrAddSection(0, bSectionAdded));
1713
+ if (Section) {
1714
+ FFrameRate TickResolution = MovieScene->GetTickResolution();
1715
+ FFrameRate DisplayRate = MovieScene->GetDisplayRate();
1716
+ FFrameNumber FrameNum = FFrameNumber(static_cast<int32>(Frame));
1717
+ FFrameNumber TickFrame =
1718
+ FFrameRate::TransformTime(FFrameTime(FrameNum), DisplayRate,
1719
+ TickResolution)
1720
+ .FloorToFrame();
1721
+
1722
+ FMovieSceneFloatChannel *Channel =
1723
+ Section->GetChannelProxy()
1724
+ .GetChannel<FMovieSceneFloatChannel>(0);
1725
+ if (Channel) {
1726
+ Channel->GetData().UpdateOrAddKey(
1727
+ TickFrame, FMovieSceneFloatValue((float)Val->AsNumber()));
1728
+ MovieScene->Modify();
1729
+ SendAutomationResponse(Socket, RequestId, true,
1730
+ TEXT("Float Keyframe added"), nullptr);
1731
+ return true;
1732
+ }
1733
+ }
1734
+ }
1735
+ } else if (Val.IsValid() && Val->Type == EJson::Boolean) {
1736
+ UMovieSceneBoolTrack *Track =
1737
+ MovieScene->FindTrack<UMovieSceneBoolTrack>(BindingGuid,
1738
+ FName(*PropertyName));
1739
+ if (!Track) {
1740
+ Track = MovieScene->AddTrack<UMovieSceneBoolTrack>(BindingGuid);
1741
+ if (Track)
1742
+ Track->SetPropertyNameAndPath(FName(*PropertyName), PropertyName);
1743
+ }
1744
+ if (Track) {
1745
+ bool bSectionAdded = false;
1746
+ UMovieSceneBoolSection *Section = Cast<UMovieSceneBoolSection>(
1747
+ Track->FindOrAddSection(0, bSectionAdded));
1748
+ if (Section) {
1749
+ FFrameRate TickResolution = MovieScene->GetTickResolution();
1750
+ FFrameRate DisplayRate = MovieScene->GetDisplayRate();
1751
+ FFrameNumber FrameNum = FFrameNumber(static_cast<int32>(Frame));
1752
+ FFrameNumber TickFrame =
1753
+ FFrameRate::TransformTime(FFrameTime(FrameNum), DisplayRate,
1754
+ TickResolution)
1755
+ .FloorToFrame();
1756
+
1757
+ FMovieSceneBoolChannel *Channel =
1758
+ Section->GetChannelProxy().GetChannel<FMovieSceneBoolChannel>(
1759
+ 0);
1760
+ if (Channel) {
1761
+ Channel->GetData().UpdateOrAddKey(TickFrame, Val->AsBool());
1762
+ MovieScene->Modify();
1763
+ SendAutomationResponse(Socket, RequestId, true,
1764
+ TEXT("Bool Keyframe added"), nullptr);
1765
+ return true;
1766
+ }
1767
+ }
1768
+ }
1769
+ }
1770
+ }
1771
+
1772
+ SendAutomationResponse(
1773
+ Socket, RequestId, false,
1774
+ TEXT("Unsupported property or failed to create track"), nullptr,
1775
+ TEXT("UNSUPPORTED_PROPERTY"));
1776
+ return true;
1777
+ }
1778
+ }
1779
+ SendAutomationResponse(Socket, RequestId, false,
1780
+ TEXT("Sequence object is not a LevelSequence"),
1781
+ nullptr, TEXT("INVALID_SEQUENCE_TYPE"));
1782
+ return true;
1783
+ #else
1784
+ SendAutomationResponse(Socket, RequestId, false,
1785
+ TEXT("sequence_add_keyframe requires editor build."),
1786
+ nullptr, TEXT("NOT_IMPLEMENTED"));
1787
+ return true;
1788
+ #endif
1789
+ }
1790
+
1791
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAddSection(
1792
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1793
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1794
+ #if WITH_EDITOR
1795
+ FString SeqPath = ResolveSequencePath(Payload);
1796
+ if (SeqPath.IsEmpty()) {
1797
+ SendAutomationResponse(
1798
+ Socket, RequestId, false,
1799
+ TEXT("sequence_add_section requires a sequence path"), nullptr,
1800
+ TEXT("INVALID_SEQUENCE"));
1801
+ return true;
1802
+ }
1803
+
1804
+ FString TrackName;
1805
+ Payload->TryGetStringField(TEXT("trackName"), TrackName);
1806
+ FString ActorName;
1807
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
1808
+ double StartFrame = 0.0, EndFrame = 100.0;
1809
+ Payload->TryGetNumberField(TEXT("startFrame"), StartFrame);
1810
+ Payload->TryGetNumberField(TEXT("endFrame"), EndFrame);
1811
+
1812
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
1813
+ if (!Sequence || !Sequence->GetMovieScene()) {
1814
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1815
+ nullptr, TEXT("SEQUENCE_NOT_FOUND"));
1816
+ return true;
1817
+ }
1818
+
1819
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
1820
+
1821
+ // Find the track - either from master tracks or from actor binding
1822
+ UMovieSceneTrack *Track = nullptr;
1823
+
1824
+ // First check master tracks
1825
+ for (UMovieSceneTrack *MasterTrack : MovieScene->GetTracks()) {
1826
+ if (MasterTrack &&
1827
+ (MasterTrack->GetName().Contains(TrackName) ||
1828
+ MasterTrack->GetDisplayName().ToString().Contains(TrackName))) {
1829
+ Track = MasterTrack;
1830
+ break;
1831
+ }
1832
+ }
1833
+
1834
+ // If not found in master tracks, check bindings
1835
+ // Search all bindings if ActorName is empty, or filter by ActorName if
1836
+ // provided
1837
+ if (!Track) {
1838
+ for (const FMovieSceneBinding &Binding :
1839
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
1840
+ FString BindingName;
1841
+ if (FMovieScenePossessable *Possessable =
1842
+ MovieScene->FindPossessable(Binding.GetObjectGuid())) {
1843
+ BindingName = Possessable->GetName();
1844
+ } else if (FMovieSceneSpawnable *Spawnable =
1845
+ MovieScene->FindSpawnable(Binding.GetObjectGuid())) {
1846
+ BindingName = Spawnable->GetName();
1847
+ }
1848
+
1849
+ // If ActorName is provided, filter by it; otherwise search all bindings
1850
+ if (ActorName.IsEmpty() || BindingName.Contains(ActorName)) {
1851
+ for (UMovieSceneTrack *BindingTrack : Binding.GetTracks()) {
1852
+ if (BindingTrack &&
1853
+ (BindingTrack->GetName().Contains(TrackName) ||
1854
+ BindingTrack->GetDisplayName().ToString().Contains(TrackName))) {
1855
+ Track = BindingTrack;
1856
+ break;
1857
+ }
1858
+ }
1859
+ if (Track)
1860
+ break;
1861
+ }
1862
+ }
1863
+ }
1864
+
1865
+ if (!Track) {
1866
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Track not found"),
1867
+ nullptr, TEXT("TRACK_NOT_FOUND"));
1868
+ return true;
1869
+ }
1870
+
1871
+ // Create the section
1872
+ UMovieSceneSection *NewSection = Track->CreateNewSection();
1873
+ if (NewSection) {
1874
+ FFrameRate TickResolution = MovieScene->GetTickResolution();
1875
+ FFrameNumber Start((int32)FMath::RoundToInt(StartFrame));
1876
+ FFrameNumber End((int32)FMath::RoundToInt(EndFrame));
1877
+ NewSection->SetRange(TRange<FFrameNumber>(Start, End));
1878
+ Track->AddSection(*NewSection);
1879
+ MovieScene->Modify();
1880
+
1881
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1882
+ Resp->SetStringField(TEXT("trackName"), Track->GetName());
1883
+ Resp->SetNumberField(TEXT("startFrame"), StartFrame);
1884
+ Resp->SetNumberField(TEXT("endFrame"), EndFrame);
1885
+ SendAutomationResponse(Socket, RequestId, true,
1886
+ TEXT("Section added to track"), Resp);
1887
+ } else {
1888
+ SendAutomationResponse(Socket, RequestId, false,
1889
+ TEXT("Failed to create section"), nullptr,
1890
+ TEXT("SECTION_CREATION_FAILED"));
1891
+ }
1892
+ return true;
1893
+ #else
1894
+ SendAutomationResponse(Socket, RequestId, false,
1895
+ TEXT("sequence_add_section requires editor build"),
1896
+ nullptr, TEXT("EDITOR_ONLY"));
1897
+ return true;
1898
+ #endif
1899
+ }
1900
+
1901
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetTickResolution(
1902
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1903
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1904
+ #if WITH_EDITOR
1905
+ FString ResolutionStr;
1906
+ Payload->TryGetStringField(TEXT("resolution"), ResolutionStr);
1907
+
1908
+ FString SeqPath = ResolveSequencePath(Payload);
1909
+ if (SeqPath.IsEmpty()) {
1910
+ SendAutomationResponse(Socket, RequestId, false, TEXT("path required"),
1911
+ nullptr, TEXT("INVALID_ARGUMENT"));
1912
+ return true;
1913
+ }
1914
+
1915
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
1916
+ if (Sequence && Sequence->GetMovieScene()) {
1917
+ FFrameRate TickResolution;
1918
+ // Simplified parsing
1919
+ if (ResolutionStr.Contains(TEXT("24000")))
1920
+ TickResolution = FFrameRate(24000, 1);
1921
+ else if (ResolutionStr.Contains(TEXT("60000")))
1922
+ TickResolution = FFrameRate(60000, 1);
1923
+ else
1924
+ TickResolution = FFrameRate(24000, 1); // Default
1925
+
1926
+ Sequence->GetMovieScene()->SetTickResolutionDirectly(TickResolution);
1927
+ Sequence->GetMovieScene()->Modify();
1928
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Tick resolution set"),
1929
+ nullptr);
1930
+ } else {
1931
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1932
+ nullptr, TEXT("NOT_FOUND"));
1933
+ }
1934
+ return true;
1935
+ #else
1936
+ return false;
1937
+ #endif
1938
+ }
1939
+
1940
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetViewRange(
1941
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1942
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1943
+ #if WITH_EDITOR
1944
+ double Start = 0;
1945
+ double End = 10;
1946
+ Payload->TryGetNumberField(TEXT("start"), Start);
1947
+ Payload->TryGetNumberField(TEXT("end"), End);
1948
+ FString SeqPath = ResolveSequencePath(Payload);
1949
+
1950
+ if (SeqPath.IsEmpty()) {
1951
+ SendAutomationResponse(Socket, RequestId, false, TEXT("path required"),
1952
+ nullptr, TEXT("INVALID_ARGUMENT"));
1953
+ return true;
1954
+ }
1955
+
1956
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
1957
+ if (Sequence && Sequence->GetMovieScene()) {
1958
+ Sequence->GetMovieScene()->SetViewRange(Start, End);
1959
+ Sequence->GetMovieScene()->Modify();
1960
+ SendAutomationResponse(Socket, RequestId, true, TEXT("View range set"),
1961
+ nullptr);
1962
+ } else {
1963
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1964
+ nullptr, TEXT("NOT_FOUND"));
1965
+ }
1966
+ return true;
1967
+ #else
1968
+ return false;
1969
+ #endif
1970
+ }
1971
+
1972
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetTrackMuted(
1973
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1974
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1975
+ #if WITH_EDITOR
1976
+ FString SeqPath = ResolveSequencePath(Payload);
1977
+ if (SeqPath.IsEmpty()) {
1978
+ SendAutomationResponse(Socket, RequestId, false,
1979
+ TEXT("sequence path required"), nullptr,
1980
+ TEXT("INVALID_SEQUENCE"));
1981
+ return true;
1982
+ }
1983
+
1984
+ FString TrackName;
1985
+ Payload->TryGetStringField(TEXT("trackName"), TrackName);
1986
+ bool bMuted = true;
1987
+ Payload->TryGetBoolField(TEXT("muted"), bMuted);
1988
+
1989
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
1990
+ if (!Sequence || !Sequence->GetMovieScene()) {
1991
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
1992
+ nullptr, TEXT("SEQUENCE_NOT_FOUND"));
1993
+ return true;
1994
+ }
1995
+
1996
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
1997
+ UMovieSceneTrack *Track = nullptr;
1998
+
1999
+ // Search master tracks and binding tracks
2000
+ for (UMovieSceneTrack *MasterTrack : MovieScene->GetTracks()) {
2001
+ if (MasterTrack && MasterTrack->GetName().Contains(TrackName)) {
2002
+ Track = MasterTrack;
2003
+ break;
2004
+ }
2005
+ }
2006
+
2007
+ if (!Track) {
2008
+ for (const FMovieSceneBinding &Binding :
2009
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
2010
+ for (UMovieSceneTrack *BindingTrack : Binding.GetTracks()) {
2011
+ if (BindingTrack && BindingTrack->GetName().Contains(TrackName)) {
2012
+ Track = BindingTrack;
2013
+ break;
2014
+ }
2015
+ }
2016
+ if (Track)
2017
+ break;
2018
+ }
2019
+ }
2020
+
2021
+ if (!Track) {
2022
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Track not found"),
2023
+ nullptr, TEXT("TRACK_NOT_FOUND"));
2024
+ return true;
2025
+ }
2026
+
2027
+ // Set muted state via EvalOptions
2028
+ Track->SetEvalDisabled(bMuted);
2029
+ MovieScene->Modify();
2030
+
2031
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2032
+ Resp->SetStringField(TEXT("trackName"), Track->GetName());
2033
+ Resp->SetBoolField(TEXT("muted"), bMuted);
2034
+ SendAutomationResponse(Socket, RequestId, true,
2035
+ bMuted ? TEXT("Track muted") : TEXT("Track unmuted"),
2036
+ Resp);
2037
+ return true;
2038
+ #else
2039
+ SendAutomationResponse(Socket, RequestId, false,
2040
+ TEXT("sequence_set_track_muted requires editor build"),
2041
+ nullptr, TEXT("EDITOR_ONLY"));
2042
+ return true;
2043
+ #endif
2044
+ }
2045
+
2046
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetTrackSolo(
2047
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2048
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2049
+ #if WITH_EDITOR
2050
+ // Note: UE doesn't have a direct "solo" property on tracks, but we can
2051
+ // simulate it by muting all other tracks
2052
+ FString SeqPath = ResolveSequencePath(Payload);
2053
+ if (SeqPath.IsEmpty()) {
2054
+ SendAutomationResponse(Socket, RequestId, false,
2055
+ TEXT("sequence path required"), nullptr,
2056
+ TEXT("INVALID_SEQUENCE"));
2057
+ return true;
2058
+ }
2059
+
2060
+ FString TrackName;
2061
+ Payload->TryGetStringField(TEXT("trackName"), TrackName);
2062
+ bool bSolo = true;
2063
+ Payload->TryGetBoolField(TEXT("solo"), bSolo);
2064
+
2065
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2066
+ if (!Sequence || !Sequence->GetMovieScene()) {
2067
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
2068
+ nullptr, TEXT("SEQUENCE_NOT_FOUND"));
2069
+ return true;
2070
+ }
2071
+
2072
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2073
+ UMovieSceneTrack *SoloTrack = nullptr;
2074
+
2075
+ // Find the track to solo
2076
+ TArray<UMovieSceneTrack *> AllTracks;
2077
+ for (UMovieSceneTrack *Track : MovieScene->GetTracks()) {
2078
+ if (Track) {
2079
+ AllTracks.Add(Track);
2080
+ if (Track->GetName().Contains(TrackName)) {
2081
+ SoloTrack = Track;
2082
+ }
2083
+ }
2084
+ }
2085
+
2086
+ for (const FMovieSceneBinding &Binding :
2087
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
2088
+ for (UMovieSceneTrack *Track : Binding.GetTracks()) {
2089
+ if (Track) {
2090
+ AllTracks.Add(Track);
2091
+ if (Track->GetName().Contains(TrackName)) {
2092
+ SoloTrack = Track;
2093
+ }
2094
+ }
2095
+ }
2096
+ }
2097
+
2098
+ if (!SoloTrack) {
2099
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Track not found"),
2100
+ nullptr, TEXT("TRACK_NOT_FOUND"));
2101
+ return true;
2102
+ }
2103
+
2104
+ // If enabling solo, mute all other tracks; if disabling, unmute all
2105
+ for (UMovieSceneTrack *Track : AllTracks) {
2106
+ if (bSolo) {
2107
+ Track->SetEvalDisabled(Track != SoloTrack);
2108
+ } else {
2109
+ Track->SetEvalDisabled(false);
2110
+ }
2111
+ }
2112
+ MovieScene->Modify();
2113
+
2114
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2115
+ Resp->SetStringField(TEXT("trackName"), SoloTrack->GetName());
2116
+ Resp->SetBoolField(TEXT("solo"), bSolo);
2117
+ SendAutomationResponse(
2118
+ Socket, RequestId, true,
2119
+ bSolo ? TEXT("Track solo enabled") : TEXT("Solo disabled"), Resp);
2120
+ return true;
2121
+ #else
2122
+ SendAutomationResponse(Socket, RequestId, false,
2123
+ TEXT("sequence_set_track_solo requires editor build"),
2124
+ nullptr, TEXT("EDITOR_ONLY"));
2125
+ return true;
2126
+ #endif
2127
+ }
2128
+
2129
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceSetTrackLocked(
2130
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2131
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2132
+ #if WITH_EDITOR
2133
+ FString SeqPath = ResolveSequencePath(Payload);
2134
+ if (SeqPath.IsEmpty()) {
2135
+ SendAutomationResponse(Socket, RequestId, false,
2136
+ TEXT("sequence path required"), nullptr,
2137
+ TEXT("INVALID_SEQUENCE"));
2138
+ return true;
2139
+ }
2140
+
2141
+ FString TrackName;
2142
+ Payload->TryGetStringField(TEXT("trackName"), TrackName);
2143
+ bool bLocked = true;
2144
+ Payload->TryGetBoolField(TEXT("locked"), bLocked);
2145
+
2146
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2147
+ if (!Sequence || !Sequence->GetMovieScene()) {
2148
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
2149
+ nullptr, TEXT("SEQUENCE_NOT_FOUND"));
2150
+ return true;
2151
+ }
2152
+
2153
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2154
+ UMovieSceneTrack *Track = nullptr;
2155
+
2156
+ // Search for track
2157
+ for (UMovieSceneTrack *MasterTrack : MovieScene->GetTracks()) {
2158
+ if (MasterTrack && MasterTrack->GetName().Contains(TrackName)) {
2159
+ Track = MasterTrack;
2160
+ break;
2161
+ }
2162
+ }
2163
+
2164
+ if (!Track) {
2165
+ for (const FMovieSceneBinding &Binding :
2166
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
2167
+ for (UMovieSceneTrack *BindingTrack : Binding.GetTracks()) {
2168
+ if (BindingTrack && BindingTrack->GetName().Contains(TrackName)) {
2169
+ Track = BindingTrack;
2170
+ break;
2171
+ }
2172
+ }
2173
+ if (Track)
2174
+ break;
2175
+ }
2176
+ }
2177
+
2178
+ if (!Track) {
2179
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Track not found"),
2180
+ nullptr, TEXT("TRACK_NOT_FOUND"));
2181
+ return true;
2182
+ }
2183
+
2184
+ // Lock all sections in the track
2185
+ for (UMovieSceneSection *Section : Track->GetAllSections()) {
2186
+ if (Section) {
2187
+ Section->SetIsLocked(bLocked);
2188
+ }
2189
+ }
2190
+ MovieScene->Modify();
2191
+
2192
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2193
+ Resp->SetStringField(TEXT("trackName"), Track->GetName());
2194
+ Resp->SetBoolField(TEXT("locked"), bLocked);
2195
+ SendAutomationResponse(
2196
+ Socket, RequestId, true,
2197
+ bLocked ? TEXT("Track locked") : TEXT("Track unlocked"), Resp);
2198
+ return true;
2199
+ #else
2200
+ SendAutomationResponse(
2201
+ Socket, RequestId, false,
2202
+ TEXT("sequence_set_track_locked requires editor build"), nullptr,
2203
+ TEXT("EDITOR_ONLY"));
2204
+ return true;
2205
+ #endif
2206
+ }
2207
+
2208
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceRemoveTrack(
2209
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2210
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2211
+ #if WITH_EDITOR
2212
+ FString SeqPath = ResolveSequencePath(Payload);
2213
+ if (SeqPath.IsEmpty()) {
2214
+ SendAutomationResponse(Socket, RequestId, false,
2215
+ TEXT("sequence path required"), nullptr,
2216
+ TEXT("INVALID_SEQUENCE"));
2217
+ return true;
2218
+ }
2219
+
2220
+ FString TrackName;
2221
+ Payload->TryGetStringField(TEXT("trackName"), TrackName);
2222
+
2223
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2224
+ if (!Sequence || !Sequence->GetMovieScene()) {
2225
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Sequence not found"),
2226
+ nullptr, TEXT("SEQUENCE_NOT_FOUND"));
2227
+ return true;
2228
+ }
2229
+
2230
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2231
+ bool bRemoved = false;
2232
+ FString RemovedTrackName;
2233
+
2234
+ // Try to remove from master tracks first
2235
+ for (UMovieSceneTrack *Track : MovieScene->GetTracks()) {
2236
+ if (Track && Track->GetName().Contains(TrackName)) {
2237
+ RemovedTrackName = Track->GetName();
2238
+ MovieScene->RemoveTrack(*Track);
2239
+ bRemoved = true;
2240
+ break;
2241
+ }
2242
+ }
2243
+
2244
+ // If not found, try binding tracks
2245
+ if (!bRemoved) {
2246
+ for (const FMovieSceneBinding &Binding :
2247
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
2248
+ for (UMovieSceneTrack *Track : Binding.GetTracks()) {
2249
+ if (Track && Track->GetName().Contains(TrackName)) {
2250
+ RemovedTrackName = Track->GetName();
2251
+ MovieScene->RemoveTrack(*Track);
2252
+ bRemoved = true;
2253
+ break;
2254
+ }
2255
+ }
2256
+ if (bRemoved)
2257
+ break;
2258
+ }
2259
+ }
2260
+
2261
+ if (bRemoved) {
2262
+ MovieScene->Modify();
2263
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2264
+ Resp->SetStringField(TEXT("trackName"), RemovedTrackName);
2265
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Track removed"),
2266
+ Resp);
2267
+ } else {
2268
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Track not found"),
2269
+ nullptr, TEXT("TRACK_NOT_FOUND"));
2270
+ }
2271
+ return true;
2272
+ #else
2273
+ SendAutomationResponse(Socket, RequestId, false,
2274
+ TEXT("sequence_remove_track requires editor build"),
2275
+ nullptr, TEXT("EDITOR_ONLY"));
2276
+ return true;
2277
+ #endif
2278
+ }
2279
+
2280
+ bool UMcpAutomationBridgeSubsystem::HandleSequenceAction(
2281
+ const FString &RequestId, const FString &Action,
2282
+ const TSharedPtr<FJsonObject> &Payload,
2283
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
2284
+ const FString Lower = Action.ToLower();
2285
+ // Also handle manage_sequence which acts as a dispatcher for sub-actions
2286
+ if (!Lower.StartsWith(TEXT("sequence_")) &&
2287
+ !Lower.Equals(TEXT("manage_sequence")))
2288
+ return false;
2289
+
2290
+ TSharedPtr<FJsonObject> LocalPayload =
2291
+ Payload.IsValid() ? Payload : MakeShared<FJsonObject>();
2292
+ FString EffectiveAction = Lower;
2293
+
2294
+ // If generic manage_sequence, extract the sub-action to determine behavior
2295
+ if (Lower.Equals(TEXT("manage_sequence"))) {
2296
+ FString Sub;
2297
+ if (LocalPayload->TryGetStringField(TEXT("subAction"), Sub) &&
2298
+ !Sub.IsEmpty()) {
2299
+ EffectiveAction = Sub.ToLower();
2300
+ // If subAction is just "create", map to "sequence_create" for consistency
2301
+ if (EffectiveAction == TEXT("create"))
2302
+ EffectiveAction = TEXT("sequence_create");
2303
+ else if (!EffectiveAction.StartsWith(TEXT("sequence_")))
2304
+ EffectiveAction = TEXT("sequence_") + EffectiveAction;
2305
+ }
2306
+ }
2307
+
2308
+ if (EffectiveAction == TEXT("sequence_create"))
2309
+ return HandleSequenceCreate(RequestId, LocalPayload, RequestingSocket);
2310
+ if (EffectiveAction == TEXT("sequence_set_display_rate"))
2311
+ return HandleSequenceSetDisplayRate(RequestId, LocalPayload,
2312
+ RequestingSocket);
2313
+ if (EffectiveAction == TEXT("sequence_set_properties"))
2314
+ return HandleSequenceSetProperties(RequestId, LocalPayload,
2315
+ RequestingSocket);
2316
+ if (EffectiveAction == TEXT("sequence_open"))
2317
+ return HandleSequenceOpen(RequestId, LocalPayload, RequestingSocket);
2318
+ if (EffectiveAction == TEXT("sequence_add_camera"))
2319
+ return HandleSequenceAddCamera(RequestId, LocalPayload, RequestingSocket);
2320
+ if (EffectiveAction == TEXT("sequence_play"))
2321
+ return HandleSequencePlay(RequestId, LocalPayload, RequestingSocket);
2322
+ if (EffectiveAction == TEXT("sequence_add_actor"))
2323
+ return HandleSequenceAddActor(RequestId, LocalPayload, RequestingSocket);
2324
+ if (EffectiveAction == TEXT("sequence_add_actors"))
2325
+ return HandleSequenceAddActors(RequestId, LocalPayload, RequestingSocket);
2326
+ if (EffectiveAction == TEXT("sequence_add_spawnable_from_class"))
2327
+ return HandleSequenceAddSpawnable(RequestId, LocalPayload,
2328
+ RequestingSocket);
2329
+ if (EffectiveAction == TEXT("sequence_remove_actors"))
2330
+ return HandleSequenceRemoveActors(RequestId, LocalPayload,
2331
+ RequestingSocket);
2332
+ if (EffectiveAction == TEXT("sequence_get_bindings"))
2333
+ return HandleSequenceGetBindings(RequestId, LocalPayload, RequestingSocket);
2334
+ if (EffectiveAction == TEXT("sequence_get_properties"))
2335
+ return HandleSequenceGetProperties(RequestId, LocalPayload,
2336
+ RequestingSocket);
2337
+ if (EffectiveAction == TEXT("sequence_set_playback_speed"))
2338
+ return HandleSequenceSetPlaybackSpeed(RequestId, LocalPayload,
2339
+ RequestingSocket);
2340
+ if (EffectiveAction == TEXT("sequence_pause"))
2341
+ return HandleSequencePause(RequestId, LocalPayload, RequestingSocket);
2342
+ if (EffectiveAction == TEXT("sequence_stop"))
2343
+ return HandleSequenceStop(RequestId, LocalPayload, RequestingSocket);
2344
+ if (EffectiveAction == TEXT("sequence_list"))
2345
+ return HandleSequenceList(RequestId, LocalPayload, RequestingSocket);
2346
+ if (EffectiveAction == TEXT("sequence_duplicate"))
2347
+ return HandleSequenceDuplicate(RequestId, LocalPayload, RequestingSocket);
2348
+ if (EffectiveAction == TEXT("sequence_rename"))
2349
+ return HandleSequenceRename(RequestId, LocalPayload, RequestingSocket);
2350
+ if (EffectiveAction == TEXT("sequence_delete"))
2351
+ return HandleSequenceDelete(RequestId, LocalPayload, RequestingSocket);
2352
+ if (EffectiveAction == TEXT("sequence_get_metadata"))
2353
+ return HandleSequenceGetMetadata(RequestId, LocalPayload, RequestingSocket);
2354
+ if (EffectiveAction == TEXT("sequence_add_keyframe"))
2355
+ return HandleSequenceAddKeyframe(RequestId, LocalPayload, RequestingSocket);
2356
+
2357
+ // New handlers
2358
+ if (EffectiveAction == TEXT("sequence_add_section"))
2359
+ return HandleSequenceAddSection(RequestId, LocalPayload, RequestingSocket);
2360
+ if (EffectiveAction == TEXT("sequence_set_tick_resolution"))
2361
+ return HandleSequenceSetTickResolution(RequestId, LocalPayload,
2362
+ RequestingSocket);
2363
+ if (EffectiveAction == TEXT("sequence_set_view_range"))
2364
+ return HandleSequenceSetViewRange(RequestId, LocalPayload,
2365
+ RequestingSocket);
2366
+ if (EffectiveAction == TEXT("sequence_set_track_muted"))
2367
+ return HandleSequenceSetTrackMuted(RequestId, LocalPayload,
2368
+ RequestingSocket);
2369
+ if (EffectiveAction == TEXT("sequence_set_track_solo"))
2370
+ return HandleSequenceSetTrackSolo(RequestId, LocalPayload,
2371
+ RequestingSocket);
2372
+ if (EffectiveAction == TEXT("sequence_set_track_locked"))
2373
+ return HandleSequenceSetTrackLocked(RequestId, LocalPayload,
2374
+ RequestingSocket);
2375
+ if (EffectiveAction == TEXT("sequence_remove_track"))
2376
+ return HandleSequenceRemoveTrack(RequestId, LocalPayload, RequestingSocket);
2377
+
2378
+ if (EffectiveAction == TEXT("sequence_add_track")) {
2379
+ // add_track action: Add a track to a binding in a level sequence
2380
+ FString SeqPath = ResolveSequencePath(LocalPayload);
2381
+ if (SeqPath.IsEmpty()) {
2382
+ SendAutomationResponse(
2383
+ RequestingSocket, RequestId, false,
2384
+ TEXT("sequence_add_track requires a sequence path"), nullptr,
2385
+ TEXT("INVALID_SEQUENCE"));
2386
+ return true;
2387
+ }
2388
+
2389
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2390
+ if (!Sequence) {
2391
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2392
+ TEXT("Level sequence not found"), nullptr,
2393
+ TEXT("SEQUENCE_NOT_FOUND"));
2394
+ return true;
2395
+ }
2396
+
2397
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2398
+ if (!MovieScene) {
2399
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2400
+ TEXT("MovieScene not available"), nullptr,
2401
+ TEXT("MOVIESCENE_UNAVAILABLE"));
2402
+ return true;
2403
+ }
2404
+
2405
+ FString TrackType;
2406
+ LocalPayload->TryGetStringField(TEXT("trackType"), TrackType);
2407
+ if (TrackType.IsEmpty()) {
2408
+ SendAutomationResponse(
2409
+ RequestingSocket, RequestId, false,
2410
+ TEXT("trackType required (e.g., Transform, Animation, Audio, Event)"),
2411
+ nullptr, TEXT("INVALID_ARGUMENT"));
2412
+ return true;
2413
+ }
2414
+
2415
+ FString TrackName;
2416
+ LocalPayload->TryGetStringField(TEXT("trackName"), TrackName);
2417
+
2418
+ FString ActorName;
2419
+ LocalPayload->TryGetStringField(TEXT("actorName"), ActorName);
2420
+
2421
+ // Find or use master track if no actor specified
2422
+ FGuid BindingGuid;
2423
+ if (!ActorName.IsEmpty()) {
2424
+ // Find binding by actor name
2425
+ // Use const interface to avoid deprecation warning
2426
+ const UMovieScene *ConstMovieScene = MovieScene;
2427
+ for (const FMovieSceneBinding &Binding : ConstMovieScene->GetBindings()) {
2428
+ FString BindingName;
2429
+ if (FMovieScenePossessable *Possessable =
2430
+ MovieScene->FindPossessable(Binding.GetObjectGuid())) {
2431
+ BindingName = Possessable->GetName();
2432
+ } else if (FMovieSceneSpawnable *Spawnable =
2433
+ MovieScene->FindSpawnable(Binding.GetObjectGuid())) {
2434
+ BindingName = Spawnable->GetName();
2435
+ }
2436
+
2437
+ if (BindingName.Contains(ActorName)) {
2438
+ BindingGuid = Binding.GetObjectGuid();
2439
+ break;
2440
+ }
2441
+ }
2442
+ if (!BindingGuid.IsValid()) {
2443
+ SendAutomationResponse(
2444
+ RequestingSocket, RequestId, false,
2445
+ FString::Printf(TEXT("Binding not found for actor: %s"),
2446
+ *ActorName),
2447
+ nullptr, TEXT("BINDING_NOT_FOUND"));
2448
+ return true;
2449
+ }
2450
+ }
2451
+
2452
+ // Add the track
2453
+ UMovieSceneTrack *NewTrack = nullptr;
2454
+ FString TrackTypeLower = TrackType.ToLower();
2455
+
2456
+ #if __has_include("Tracks/MovieScene3DTransformTrack.h")
2457
+ if (TrackTypeLower == TEXT("transform") ||
2458
+ TrackTypeLower == TEXT("3dtransform")) {
2459
+ if (BindingGuid.IsValid()) {
2460
+ NewTrack = MovieScene->AddTrack(
2461
+ UMovieScene3DTransformTrack::StaticClass(), BindingGuid);
2462
+ }
2463
+ }
2464
+ #endif
2465
+
2466
+ if (TrackTypeLower == TEXT("audio")) {
2467
+ if (BindingGuid.IsValid()) {
2468
+ NewTrack = MovieScene->AddTrack(UMovieSceneAudioTrack::StaticClass(),
2469
+ BindingGuid);
2470
+ } else {
2471
+ NewTrack = MovieScene->AddTrack(UMovieSceneAudioTrack::StaticClass());
2472
+ }
2473
+ }
2474
+
2475
+ if (TrackTypeLower == TEXT("event")) {
2476
+ if (BindingGuid.IsValid()) {
2477
+ NewTrack = MovieScene->AddTrack(UMovieSceneEventTrack::StaticClass(),
2478
+ BindingGuid);
2479
+ } else {
2480
+ NewTrack = MovieScene->AddTrack(UMovieSceneEventTrack::StaticClass());
2481
+ }
2482
+ }
2483
+
2484
+ // For master tracks or other track types, would need additional
2485
+ // implementation based on available track classes
2486
+
2487
+ if (NewTrack) {
2488
+ Sequence->MarkPackageDirty();
2489
+
2490
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2491
+ Resp->SetBoolField(TEXT("success"), true);
2492
+ Resp->SetStringField(TEXT("sequencePath"), SeqPath);
2493
+ Resp->SetStringField(TEXT("trackType"), TrackType);
2494
+ Resp->SetStringField(TEXT("trackName"),
2495
+ TrackName.IsEmpty() ? TrackType : TrackName);
2496
+ if (!ActorName.IsEmpty()) {
2497
+ Resp->SetStringField(TEXT("actorName"), ActorName);
2498
+ Resp->SetStringField(TEXT("bindingGuid"), BindingGuid.ToString());
2499
+ }
2500
+ SendAutomationResponse(RequestingSocket, RequestId, true,
2501
+ TEXT("Track added successfully"), Resp, FString());
2502
+ } else {
2503
+ SendAutomationResponse(
2504
+ RequestingSocket, RequestId, false,
2505
+ FString::Printf(TEXT("Failed to add track of type: %s"), *TrackType),
2506
+ nullptr, TEXT("TRACK_CREATION_FAILED"));
2507
+ }
2508
+ return true;
2509
+ }
2510
+
2511
+ // sequence_list_tracks: List all tracks for a sequence binding
2512
+ if (EffectiveAction == TEXT("sequence_list_tracks")) {
2513
+ FString SeqPath = ResolveSequencePath(LocalPayload);
2514
+ if (SeqPath.IsEmpty()) {
2515
+ SendAutomationResponse(
2516
+ RequestingSocket, RequestId, false,
2517
+ TEXT("sequence_list_tracks requires a sequence path"), nullptr,
2518
+ TEXT("INVALID_SEQUENCE"));
2519
+ return true;
2520
+ }
2521
+
2522
+ #if WITH_EDITOR
2523
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2524
+ if (!Sequence) {
2525
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2526
+ TEXT("Level sequence not found"), nullptr,
2527
+ TEXT("SEQUENCE_NOT_FOUND"));
2528
+ return true;
2529
+ }
2530
+
2531
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2532
+ if (!MovieScene) {
2533
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2534
+ TEXT("MovieScene not available"), nullptr,
2535
+ TEXT("MOVIESCENE_UNAVAILABLE"));
2536
+ return true;
2537
+ }
2538
+
2539
+ TArray<TSharedPtr<FJsonValue>> TracksArray;
2540
+
2541
+ // Get Tracks (formerly GetMasterTracks)
2542
+ for (UMovieSceneTrack *Track : MovieScene->GetTracks()) {
2543
+ if (!Track)
2544
+ continue;
2545
+ TSharedPtr<FJsonObject> TrackObj = MakeShared<FJsonObject>();
2546
+ TrackObj->SetStringField(TEXT("trackName"), Track->GetName());
2547
+ TrackObj->SetStringField(TEXT("trackType"), Track->GetClass()->GetName());
2548
+ TrackObj->SetStringField(TEXT("displayName"),
2549
+ Track->GetDisplayName().ToString());
2550
+ TrackObj->SetBoolField(TEXT("isMasterTrack"), true);
2551
+ TrackObj->SetNumberField(TEXT("sectionCount"),
2552
+ Track->GetAllSections().Num());
2553
+ TracksArray.Add(MakeShared<FJsonValueObject>(TrackObj));
2554
+ }
2555
+
2556
+ // Get tracks from bindings
2557
+ for (const FMovieSceneBinding &Binding :
2558
+ const_cast<const UMovieScene *>(MovieScene)->GetBindings()) {
2559
+ FString BindingName;
2560
+ if (FMovieScenePossessable *Possessable =
2561
+ MovieScene->FindPossessable(Binding.GetObjectGuid())) {
2562
+ BindingName = Possessable->GetName();
2563
+ } else if (FMovieSceneSpawnable *Spawnable =
2564
+ MovieScene->FindSpawnable(Binding.GetObjectGuid())) {
2565
+ BindingName = Spawnable->GetName();
2566
+ }
2567
+
2568
+ for (UMovieSceneTrack *Track : Binding.GetTracks()) {
2569
+ if (!Track)
2570
+ continue;
2571
+ TSharedPtr<FJsonObject> TrackObj = MakeShared<FJsonObject>();
2572
+ TrackObj->SetStringField(TEXT("trackName"), Track->GetName());
2573
+ TrackObj->SetStringField(TEXT("trackType"),
2574
+ Track->GetClass()->GetName());
2575
+ TrackObj->SetStringField(TEXT("displayName"),
2576
+ Track->GetDisplayName().ToString());
2577
+ TrackObj->SetBoolField(TEXT("isMasterTrack"), false);
2578
+ TrackObj->SetStringField(TEXT("bindingName"), BindingName);
2579
+ TrackObj->SetStringField(TEXT("bindingGuid"),
2580
+ Binding.GetObjectGuid().ToString());
2581
+ TrackObj->SetNumberField(TEXT("sectionCount"),
2582
+ Track->GetAllSections().Num());
2583
+ TracksArray.Add(MakeShared<FJsonValueObject>(TrackObj));
2584
+ }
2585
+ }
2586
+
2587
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2588
+ Resp->SetArrayField(TEXT("tracks"), TracksArray);
2589
+ Resp->SetNumberField(TEXT("trackCount"), TracksArray.Num());
2590
+ Resp->SetStringField(TEXT("sequencePath"), SeqPath);
2591
+ SendAutomationResponse(
2592
+ RequestingSocket, RequestId, true,
2593
+ FString::Printf(TEXT("Found %d tracks"), TracksArray.Num()), Resp,
2594
+ FString());
2595
+ return true;
2596
+ #else
2597
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2598
+ TEXT("sequence_list_tracks requires editor build"),
2599
+ nullptr, TEXT("EDITOR_ONLY"));
2600
+ return true;
2601
+ #endif
2602
+ }
2603
+
2604
+ // sequence_set_work_range: Set the work range of a sequence
2605
+ if (EffectiveAction == TEXT("sequence_set_work_range")) {
2606
+ FString SeqPath = ResolveSequencePath(LocalPayload);
2607
+ if (SeqPath.IsEmpty()) {
2608
+ SendAutomationResponse(
2609
+ RequestingSocket, RequestId, false,
2610
+ TEXT("sequence_set_work_range requires a sequence path"), nullptr,
2611
+ TEXT("INVALID_SEQUENCE"));
2612
+ return true;
2613
+ }
2614
+
2615
+ #if WITH_EDITOR
2616
+ ULevelSequence *Sequence = LoadObject<ULevelSequence>(nullptr, *SeqPath);
2617
+ if (!Sequence) {
2618
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2619
+ TEXT("Level sequence not found"), nullptr,
2620
+ TEXT("SEQUENCE_NOT_FOUND"));
2621
+ return true;
2622
+ }
2623
+
2624
+ UMovieScene *MovieScene = Sequence->GetMovieScene();
2625
+ if (!MovieScene) {
2626
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2627
+ TEXT("MovieScene not available"), nullptr,
2628
+ TEXT("MOVIESCENE_UNAVAILABLE"));
2629
+ return true;
2630
+ }
2631
+
2632
+ double Start = 0.0, End = 0.0;
2633
+ LocalPayload->TryGetNumberField(TEXT("start"), Start);
2634
+ LocalPayload->TryGetNumberField(TEXT("end"), End);
2635
+
2636
+ FFrameRate TickResolution = MovieScene->GetTickResolution();
2637
+ // Round to int32 for FFrameNumber constructor
2638
+ FFrameNumber StartFrame(
2639
+ (int32)FMath::RoundToInt(Start * TickResolution.AsDecimal()));
2640
+ FFrameNumber EndFrame(
2641
+ (int32)FMath::RoundToInt(End * TickResolution.AsDecimal()));
2642
+
2643
+ // SetWorkingRange expects seconds (double)
2644
+ MovieScene->SetWorkingRange(Start, End);
2645
+ MovieScene->Modify();
2646
+
2647
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2648
+ Resp->SetNumberField(TEXT("startFrame"), StartFrame.Value);
2649
+ Resp->SetNumberField(TEXT("endFrame"), EndFrame.Value);
2650
+ Resp->SetStringField(TEXT("sequencePath"), SeqPath);
2651
+ SendAutomationResponse(RequestingSocket, RequestId, true,
2652
+ TEXT("Work range set successfully"), Resp,
2653
+ FString());
2654
+ return true;
2655
+ #else
2656
+ SendAutomationResponse(
2657
+ RequestingSocket, RequestId, false,
2658
+ TEXT("sequence_set_work_range requires editor build"), nullptr,
2659
+ TEXT("EDITOR_ONLY"));
2660
+ return true;
2661
+ #endif
2662
+ }
2663
+
2664
+ SendAutomationResponse(
2665
+ RequestingSocket, RequestId, false,
2666
+ FString::Printf(TEXT("Sequence action not implemented by plugin: %s"),
2667
+ *Action),
2668
+ nullptr, TEXT("NOT_IMPLEMENTED"));
2669
+ return true;
2670
+ }