unreal-engine-mcp-server 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter-config.yml +51 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +27 -0
  19. package/.github/workflows/labeler.yml +17 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +13 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +338 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/GEMINI.md +115 -0
  31. package/Public/Plugin_setup_guide.mp4 +0 -0
  32. package/README.md +189 -128
  33. package/claude_desktop_config_example.json +7 -6
  34. package/dist/automation/bridge.d.ts +50 -0
  35. package/dist/automation/bridge.js +452 -0
  36. package/dist/automation/connection-manager.d.ts +23 -0
  37. package/dist/automation/connection-manager.js +107 -0
  38. package/dist/automation/handshake.d.ts +11 -0
  39. package/dist/automation/handshake.js +89 -0
  40. package/dist/automation/index.d.ts +3 -0
  41. package/dist/automation/index.js +3 -0
  42. package/dist/automation/message-handler.d.ts +12 -0
  43. package/dist/automation/message-handler.js +149 -0
  44. package/dist/automation/request-tracker.d.ts +25 -0
  45. package/dist/automation/request-tracker.js +98 -0
  46. package/dist/automation/types.d.ts +130 -0
  47. package/dist/automation/types.js +2 -0
  48. package/dist/cli.js +32 -5
  49. package/dist/config.d.ts +26 -0
  50. package/dist/config.js +59 -0
  51. package/dist/constants.d.ts +16 -0
  52. package/dist/constants.js +16 -0
  53. package/dist/graphql/loaders.d.ts +64 -0
  54. package/dist/graphql/loaders.js +117 -0
  55. package/dist/graphql/resolvers.d.ts +268 -0
  56. package/dist/graphql/resolvers.js +746 -0
  57. package/dist/graphql/schema.d.ts +5 -0
  58. package/dist/graphql/schema.js +437 -0
  59. package/dist/graphql/server.d.ts +26 -0
  60. package/dist/graphql/server.js +117 -0
  61. package/dist/graphql/types.d.ts +9 -0
  62. package/dist/graphql/types.js +2 -0
  63. package/dist/handlers/resource-handlers.d.ts +20 -0
  64. package/dist/handlers/resource-handlers.js +180 -0
  65. package/dist/index.d.ts +33 -18
  66. package/dist/index.js +130 -619
  67. package/dist/resources/actors.d.ts +17 -12
  68. package/dist/resources/actors.js +56 -76
  69. package/dist/resources/assets.d.ts +6 -14
  70. package/dist/resources/assets.js +115 -147
  71. package/dist/resources/levels.d.ts +13 -13
  72. package/dist/resources/levels.js +25 -34
  73. package/dist/server/resource-registry.d.ts +20 -0
  74. package/dist/server/resource-registry.js +37 -0
  75. package/dist/server/tool-registry.d.ts +23 -0
  76. package/dist/server/tool-registry.js +322 -0
  77. package/dist/server-setup.d.ts +20 -0
  78. package/dist/server-setup.js +71 -0
  79. package/dist/services/health-monitor.d.ts +34 -0
  80. package/dist/services/health-monitor.js +105 -0
  81. package/dist/services/metrics-server.d.ts +11 -0
  82. package/dist/services/metrics-server.js +105 -0
  83. package/dist/tools/actors.d.ts +163 -9
  84. package/dist/tools/actors.js +356 -311
  85. package/dist/tools/animation.d.ts +135 -4
  86. package/dist/tools/animation.js +510 -411
  87. package/dist/tools/assets.d.ts +75 -29
  88. package/dist/tools/assets.js +265 -284
  89. package/dist/tools/audio.d.ts +102 -42
  90. package/dist/tools/audio.js +272 -685
  91. package/dist/tools/base-tool.d.ts +17 -0
  92. package/dist/tools/base-tool.js +46 -0
  93. package/dist/tools/behavior-tree.d.ts +94 -0
  94. package/dist/tools/behavior-tree.js +39 -0
  95. package/dist/tools/blueprint.d.ts +208 -126
  96. package/dist/tools/blueprint.js +685 -832
  97. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  98. package/dist/tools/consolidated-tool-definitions.js +829 -496
  99. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  100. package/dist/tools/consolidated-tool-handlers.js +198 -1027
  101. package/dist/tools/debug.d.ts +143 -85
  102. package/dist/tools/debug.js +234 -180
  103. package/dist/tools/dynamic-handler-registry.d.ts +13 -0
  104. package/dist/tools/dynamic-handler-registry.js +23 -0
  105. package/dist/tools/editor.d.ts +30 -83
  106. package/dist/tools/editor.js +247 -244
  107. package/dist/tools/engine.d.ts +10 -4
  108. package/dist/tools/engine.js +13 -5
  109. package/dist/tools/environment.d.ts +30 -0
  110. package/dist/tools/environment.js +267 -0
  111. package/dist/tools/foliage.d.ts +65 -99
  112. package/dist/tools/foliage.js +221 -331
  113. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  114. package/dist/tools/handlers/actor-handlers.js +227 -0
  115. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  116. package/dist/tools/handlers/animation-handlers.js +185 -0
  117. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  118. package/dist/tools/handlers/argument-helper.js +80 -0
  119. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  120. package/dist/tools/handlers/asset-handlers.js +496 -0
  121. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  122. package/dist/tools/handlers/audio-handlers.js +166 -0
  123. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  124. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  125. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  126. package/dist/tools/handlers/common-handlers.js +56 -0
  127. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  128. package/dist/tools/handlers/editor-handlers.js +119 -0
  129. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  130. package/dist/tools/handlers/effect-handlers.js +171 -0
  131. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  132. package/dist/tools/handlers/environment-handlers.js +170 -0
  133. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  134. package/dist/tools/handlers/graph-handlers.js +90 -0
  135. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  136. package/dist/tools/handlers/input-handlers.js +21 -0
  137. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  138. package/dist/tools/handlers/inspect-handlers.js +383 -0
  139. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  140. package/dist/tools/handlers/level-handlers.js +237 -0
  141. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  142. package/dist/tools/handlers/lighting-handlers.js +144 -0
  143. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  144. package/dist/tools/handlers/performance-handlers.js +130 -0
  145. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  146. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  147. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  148. package/dist/tools/handlers/sequence-handlers.js +376 -0
  149. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  150. package/dist/tools/handlers/system-handlers.js +506 -0
  151. package/dist/tools/input.d.ts +19 -0
  152. package/dist/tools/input.js +89 -0
  153. package/dist/tools/introspection.d.ts +103 -40
  154. package/dist/tools/introspection.js +425 -568
  155. package/dist/tools/landscape.d.ts +54 -93
  156. package/dist/tools/landscape.js +284 -409
  157. package/dist/tools/level.d.ts +66 -27
  158. package/dist/tools/level.js +647 -675
  159. package/dist/tools/lighting.d.ts +77 -38
  160. package/dist/tools/lighting.js +445 -943
  161. package/dist/tools/logs.d.ts +3 -3
  162. package/dist/tools/logs.js +5 -57
  163. package/dist/tools/materials.d.ts +91 -24
  164. package/dist/tools/materials.js +194 -118
  165. package/dist/tools/niagara.d.ts +149 -39
  166. package/dist/tools/niagara.js +267 -182
  167. package/dist/tools/performance.d.ts +27 -13
  168. package/dist/tools/performance.js +203 -122
  169. package/dist/tools/physics.d.ts +32 -77
  170. package/dist/tools/physics.js +175 -582
  171. package/dist/tools/property-dictionary.d.ts +13 -0
  172. package/dist/tools/property-dictionary.js +82 -0
  173. package/dist/tools/sequence.d.ts +85 -60
  174. package/dist/tools/sequence.js +208 -747
  175. package/dist/tools/tool-definition-utils.d.ts +59 -0
  176. package/dist/tools/tool-definition-utils.js +35 -0
  177. package/dist/tools/ui.d.ts +64 -34
  178. package/dist/tools/ui.js +134 -214
  179. package/dist/types/automation-responses.d.ts +115 -0
  180. package/dist/types/automation-responses.js +2 -0
  181. package/dist/types/env.d.ts +0 -3
  182. package/dist/types/env.js +0 -7
  183. package/dist/types/responses.d.ts +249 -0
  184. package/dist/types/responses.js +2 -0
  185. package/dist/types/tool-interfaces.d.ts +898 -0
  186. package/dist/types/tool-interfaces.js +2 -0
  187. package/dist/types/tool-types.d.ts +183 -19
  188. package/dist/types/tool-types.js +0 -4
  189. package/dist/unreal-bridge.d.ts +24 -131
  190. package/dist/unreal-bridge.js +364 -1506
  191. package/dist/utils/command-validator.d.ts +9 -0
  192. package/dist/utils/command-validator.js +68 -0
  193. package/dist/utils/elicitation.d.ts +1 -1
  194. package/dist/utils/elicitation.js +12 -15
  195. package/dist/utils/error-handler.d.ts +2 -51
  196. package/dist/utils/error-handler.js +11 -87
  197. package/dist/utils/ini-reader.d.ts +3 -0
  198. package/dist/utils/ini-reader.js +69 -0
  199. package/dist/utils/logger.js +9 -6
  200. package/dist/utils/normalize.d.ts +3 -0
  201. package/dist/utils/normalize.js +56 -0
  202. package/dist/utils/path-security.d.ts +2 -0
  203. package/dist/utils/path-security.js +24 -0
  204. package/dist/utils/response-factory.d.ts +7 -0
  205. package/dist/utils/response-factory.js +27 -0
  206. package/dist/utils/response-validator.d.ts +3 -24
  207. package/dist/utils/response-validator.js +130 -81
  208. package/dist/utils/result-helpers.d.ts +4 -5
  209. package/dist/utils/result-helpers.js +15 -16
  210. package/dist/utils/safe-json.js +5 -11
  211. package/dist/utils/unreal-command-queue.d.ts +24 -0
  212. package/dist/utils/unreal-command-queue.js +120 -0
  213. package/dist/utils/validation.d.ts +0 -40
  214. package/dist/utils/validation.js +1 -78
  215. package/dist/wasm/index.d.ts +70 -0
  216. package/dist/wasm/index.js +535 -0
  217. package/docs/GraphQL-API.md +888 -0
  218. package/docs/Migration-Guide-v0.5.0.md +684 -0
  219. package/docs/Roadmap.md +53 -0
  220. package/docs/WebAssembly-Integration.md +628 -0
  221. package/docs/editor-plugin-extension.md +370 -0
  222. package/docs/handler-mapping.md +242 -0
  223. package/docs/native-automation-progress.md +128 -0
  224. package/docs/testing-guide.md +423 -0
  225. package/mcp-config-example.json +6 -6
  226. package/package.json +67 -28
  227. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  228. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  272. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  273. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  274. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  275. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  276. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  277. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  278. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  279. package/scripts/check-unreal-connection.mjs +19 -0
  280. package/scripts/clean-tmp.js +23 -0
  281. package/scripts/patch-wasm.js +26 -0
  282. package/scripts/run-all-tests.mjs +136 -0
  283. package/scripts/smoke-test.ts +94 -0
  284. package/scripts/sync-mcp-plugin.js +143 -0
  285. package/scripts/test-no-plugin-alternates.mjs +113 -0
  286. package/scripts/validate-server.js +46 -0
  287. package/scripts/verify-automation-bridge.js +200 -0
  288. package/server.json +58 -21
  289. package/src/automation/bridge.ts +558 -0
  290. package/src/automation/connection-manager.ts +130 -0
  291. package/src/automation/handshake.ts +99 -0
  292. package/src/automation/index.ts +2 -0
  293. package/src/automation/message-handler.ts +167 -0
  294. package/src/automation/request-tracker.ts +123 -0
  295. package/src/automation/types.ts +107 -0
  296. package/src/cli.ts +33 -6
  297. package/src/config.ts +73 -0
  298. package/src/constants.ts +19 -0
  299. package/src/graphql/loaders.ts +244 -0
  300. package/src/graphql/resolvers.ts +1008 -0
  301. package/src/graphql/schema.ts +452 -0
  302. package/src/graphql/server.ts +156 -0
  303. package/src/graphql/types.ts +10 -0
  304. package/src/handlers/resource-handlers.ts +186 -0
  305. package/src/index.ts +166 -664
  306. package/src/resources/actors.ts +58 -76
  307. package/src/resources/assets.ts +148 -134
  308. package/src/resources/levels.ts +28 -33
  309. package/src/server/resource-registry.ts +47 -0
  310. package/src/server/tool-registry.ts +354 -0
  311. package/src/server-setup.ts +114 -0
  312. package/src/services/health-monitor.ts +132 -0
  313. package/src/services/metrics-server.ts +142 -0
  314. package/src/tools/actors.ts +426 -323
  315. package/src/tools/animation.ts +672 -461
  316. package/src/tools/assets.ts +364 -289
  317. package/src/tools/audio.ts +323 -766
  318. package/src/tools/base-tool.ts +52 -0
  319. package/src/tools/behavior-tree.ts +45 -0
  320. package/src/tools/blueprint.ts +792 -970
  321. package/src/tools/consolidated-tool-definitions.ts +993 -515
  322. package/src/tools/consolidated-tool-handlers.ts +258 -1146
  323. package/src/tools/debug.ts +292 -187
  324. package/src/tools/dynamic-handler-registry.ts +33 -0
  325. package/src/tools/editor.ts +329 -253
  326. package/src/tools/engine.ts +14 -3
  327. package/src/tools/environment.ts +281 -0
  328. package/src/tools/foliage.ts +330 -392
  329. package/src/tools/handlers/actor-handlers.ts +265 -0
  330. package/src/tools/handlers/animation-handlers.ts +237 -0
  331. package/src/tools/handlers/argument-helper.ts +142 -0
  332. package/src/tools/handlers/asset-handlers.ts +532 -0
  333. package/src/tools/handlers/audio-handlers.ts +194 -0
  334. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  335. package/src/tools/handlers/common-handlers.ts +87 -0
  336. package/src/tools/handlers/editor-handlers.ts +123 -0
  337. package/src/tools/handlers/effect-handlers.ts +220 -0
  338. package/src/tools/handlers/environment-handlers.ts +183 -0
  339. package/src/tools/handlers/graph-handlers.ts +116 -0
  340. package/src/tools/handlers/input-handlers.ts +28 -0
  341. package/src/tools/handlers/inspect-handlers.ts +450 -0
  342. package/src/tools/handlers/level-handlers.ts +252 -0
  343. package/src/tools/handlers/lighting-handlers.ts +147 -0
  344. package/src/tools/handlers/performance-handlers.ts +132 -0
  345. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  346. package/src/tools/handlers/sequence-handlers.ts +415 -0
  347. package/src/tools/handlers/system-handlers.ts +564 -0
  348. package/src/tools/input.ts +101 -0
  349. package/src/tools/introspection.ts +493 -584
  350. package/src/tools/landscape.ts +418 -507
  351. package/src/tools/level.ts +786 -708
  352. package/src/tools/lighting.ts +588 -984
  353. package/src/tools/logs.ts +9 -57
  354. package/src/tools/materials.ts +237 -121
  355. package/src/tools/niagara.ts +335 -168
  356. package/src/tools/performance.ts +320 -169
  357. package/src/tools/physics.ts +274 -613
  358. package/src/tools/property-dictionary.ts +98 -0
  359. package/src/tools/sequence.ts +276 -820
  360. package/src/tools/tool-definition-utils.ts +35 -0
  361. package/src/tools/ui.ts +205 -283
  362. package/src/types/automation-responses.ts +119 -0
  363. package/src/types/env.ts +0 -10
  364. package/src/types/responses.ts +355 -0
  365. package/src/types/tool-interfaces.ts +250 -0
  366. package/src/types/tool-types.ts +243 -21
  367. package/src/unreal-bridge.ts +460 -1550
  368. package/src/utils/command-validator.ts +76 -0
  369. package/src/utils/elicitation.ts +10 -7
  370. package/src/utils/error-handler.ts +14 -90
  371. package/src/utils/ini-reader.ts +86 -0
  372. package/src/utils/logger.ts +8 -3
  373. package/src/utils/normalize.test.ts +162 -0
  374. package/src/utils/normalize.ts +60 -0
  375. package/src/utils/path-security.ts +43 -0
  376. package/src/utils/response-factory.ts +44 -0
  377. package/src/utils/response-validator.ts +176 -56
  378. package/src/utils/result-helpers.ts +21 -19
  379. package/src/utils/safe-json.test.ts +90 -0
  380. package/src/utils/safe-json.ts +14 -11
  381. package/src/utils/unreal-command-queue.ts +152 -0
  382. package/src/utils/validation.test.ts +184 -0
  383. package/src/utils/validation.ts +4 -1
  384. package/src/wasm/index.ts +838 -0
  385. package/test-server.mjs +100 -0
  386. package/tests/run-unreal-tool-tests.mjs +242 -14
  387. package/tests/test-animation.mjs +369 -0
  388. package/tests/test-asset-advanced.mjs +82 -0
  389. package/tests/test-asset-errors.mjs +35 -0
  390. package/tests/test-asset-graph.mjs +311 -0
  391. package/tests/test-audio.mjs +417 -0
  392. package/tests/test-automation-timeouts.mjs +98 -0
  393. package/tests/test-behavior-tree.mjs +444 -0
  394. package/tests/test-blueprint-graph.mjs +410 -0
  395. package/tests/test-blueprint.mjs +577 -0
  396. package/tests/test-client-mode.mjs +86 -0
  397. package/tests/test-console-command.mjs +56 -0
  398. package/tests/test-control-actor.mjs +425 -0
  399. package/tests/test-control-editor.mjs +112 -0
  400. package/tests/test-graphql.mjs +372 -0
  401. package/tests/test-input.mjs +349 -0
  402. package/tests/test-inspect.mjs +302 -0
  403. package/tests/test-landscape.mjs +316 -0
  404. package/tests/test-lighting.mjs +428 -0
  405. package/tests/test-manage-asset.mjs +438 -0
  406. package/tests/test-manage-level.mjs +89 -0
  407. package/tests/test-materials.mjs +356 -0
  408. package/tests/test-niagara.mjs +185 -0
  409. package/tests/test-no-inline-python.mjs +122 -0
  410. package/tests/test-performance.mjs +539 -0
  411. package/tests/test-plugin-handshake.mjs +82 -0
  412. package/tests/test-runner.mjs +933 -0
  413. package/tests/test-sequence.mjs +104 -0
  414. package/tests/test-system.mjs +96 -0
  415. package/tests/test-wasm.mjs +283 -0
  416. package/tests/test-world-partition.mjs +215 -0
  417. package/tsconfig.json +3 -3
  418. package/vitest.config.ts +35 -0
  419. package/wasm/Cargo.lock +363 -0
  420. package/wasm/Cargo.toml +42 -0
  421. package/wasm/LICENSE +21 -0
  422. package/wasm/README.md +253 -0
  423. package/wasm/src/dependency_resolver.rs +377 -0
  424. package/wasm/src/lib.rs +153 -0
  425. package/wasm/src/property_parser.rs +271 -0
  426. package/wasm/src/transform_math.rs +396 -0
  427. package/wasm/tests/integration.rs +109 -0
  428. package/.github/workflows/smithery-build.yml +0 -29
  429. package/dist/prompts/index.d.ts +0 -21
  430. package/dist/prompts/index.js +0 -217
  431. package/dist/tools/build_environment_advanced.d.ts +0 -65
  432. package/dist/tools/build_environment_advanced.js +0 -633
  433. package/dist/tools/rc.d.ts +0 -110
  434. package/dist/tools/rc.js +0 -437
  435. package/dist/tools/visual.d.ts +0 -40
  436. package/dist/tools/visual.js +0 -282
  437. package/dist/utils/http.d.ts +0 -6
  438. package/dist/utils/http.js +0 -151
  439. package/dist/utils/python-output.d.ts +0 -18
  440. package/dist/utils/python-output.js +0 -290
  441. package/dist/utils/python.d.ts +0 -2
  442. package/dist/utils/python.js +0 -4
  443. package/dist/utils/stdio-redirect.d.ts +0 -2
  444. package/dist/utils/stdio-redirect.js +0 -20
  445. package/docs/unreal-tool-test-cases.md +0 -574
  446. package/smithery.yaml +0 -29
  447. package/src/prompts/index.ts +0 -249
  448. package/src/tools/build_environment_advanced.ts +0 -732
  449. package/src/tools/rc.ts +0 -515
  450. package/src/tools/visual.ts +0 -281
  451. package/src/utils/http.ts +0 -187
  452. package/src/utils/python-output.ts +0 -351
  453. package/src/utils/python.ts +0 -3
  454. package/src/utils/stdio-redirect.ts +0 -18
@@ -0,0 +1,2614 @@
1
+ #include "Async/Async.h"
2
+ #include "Dom/JsonObject.h"
3
+ #include "GameFramework/Actor.h"
4
+ #include "McpAutomationBridgeGlobals.h"
5
+ #include "McpAutomationBridgeHelpers.h"
6
+ #include "McpAutomationBridgeSubsystem.h"
7
+ #include "Misc/ScopeExit.h"
8
+
9
+ #if WITH_EDITOR
10
+ #include "EditorAssetLibrary.h"
11
+ #include "EngineUtils.h"
12
+ #if __has_include("Subsystems/EditorActorSubsystem.h")
13
+ #include "Subsystems/EditorActorSubsystem.h"
14
+ #elif __has_include("EditorActorSubsystem.h")
15
+ #include "EditorActorSubsystem.h"
16
+ #endif
17
+ #if __has_include("Subsystems/UnrealEditorSubsystem.h")
18
+ #include "Subsystems/UnrealEditorSubsystem.h"
19
+ #define MCP_HAS_UNREALEDITOR_SUBSYSTEM 1
20
+ #elif __has_include("UnrealEditorSubsystem.h")
21
+ #include "UnrealEditorSubsystem.h"
22
+ #define MCP_HAS_UNREALEDITOR_SUBSYSTEM 1
23
+ #endif
24
+ #if __has_include("Subsystems/LevelEditorSubsystem.h")
25
+ #include "Subsystems/LevelEditorSubsystem.h"
26
+ #define MCP_HAS_LEVELEDITOR_SUBSYSTEM 1
27
+ #elif __has_include("LevelEditorSubsystem.h")
28
+ #include "LevelEditorSubsystem.h"
29
+ #define MCP_HAS_LEVELEDITOR_SUBSYSTEM 1
30
+ #endif
31
+ #if __has_include("Subsystems/AssetEditorSubsystem.h")
32
+ #include "Subsystems/AssetEditorSubsystem.h"
33
+ #elif __has_include("AssetEditorSubsystem.h")
34
+ #include "AssetEditorSubsystem.h"
35
+ #endif
36
+ // Additional editor headers for viewport control
37
+ #include "Components/LightComponent.h"
38
+ #include "Editor.h"
39
+ #include "Modules/ModuleManager.h"
40
+
41
+ #if __has_include("LevelEditor.h")
42
+ #include "LevelEditor.h"
43
+ #define MCP_HAS_LEVEL_EDITOR_MODULE 1
44
+ #else
45
+ #define MCP_HAS_LEVEL_EDITOR_MODULE 0
46
+ #endif
47
+ #if __has_include("Settings/LevelEditorPlaySettings.h")
48
+ #include "Settings/LevelEditorPlaySettings.h"
49
+ #define MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS 1
50
+ #else
51
+ #define MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS 0
52
+ #endif
53
+ #include "Components/PrimitiveComponent.h"
54
+ #include "EditorViewportClient.h"
55
+ #include "Engine/Blueprint.h"
56
+
57
+ #if __has_include("FileHelpers.h")
58
+ #include "FileHelpers.h"
59
+ #endif
60
+ #include "Animation/SkeletalMeshActor.h"
61
+ #include "Components/ActorComponent.h"
62
+ #include "Components/SceneComponent.h"
63
+ #include "Components/StaticMeshComponent.h"
64
+ #include "Engine/EngineTypes.h"
65
+ #include "Engine/SkeletalMesh.h"
66
+ #include "Engine/StaticMesh.h"
67
+ #include "Engine/StaticMeshActor.h"
68
+ #include "Engine/World.h"
69
+ #include "Exporters/Exporter.h"
70
+ #include "Misc/OutputDevice.h"
71
+
72
+ #endif
73
+
74
+ // Helper class for capturing export output
75
+ /* UE5.6: Use built-in FStringOutputDevice from UnrealString.h */
76
+
77
+ // Helper functions
78
+ // (ExtractVectorField and ExtractRotatorField moved to
79
+ // McpAutomationBridgeHelpers.h)
80
+
81
+ AActor *UMcpAutomationBridgeSubsystem::FindActorByName(const FString &Target) {
82
+ #if WITH_EDITOR
83
+ if (Target.IsEmpty() || !GEditor)
84
+ return nullptr;
85
+
86
+ // Priority: PIE World if active
87
+ if (GEditor->PlayWorld) {
88
+ for (TActorIterator<AActor> It(GEditor->PlayWorld); It; ++It) {
89
+ AActor *A = *It;
90
+ if (!A)
91
+ continue;
92
+ if (A->GetActorLabel().Equals(Target, ESearchCase::IgnoreCase) ||
93
+ A->GetName().Equals(Target, ESearchCase::IgnoreCase) ||
94
+ A->GetPathName().Equals(Target, ESearchCase::IgnoreCase)) {
95
+ return A;
96
+ }
97
+ }
98
+ // If not found in PIE, do we fall back to Editor World?
99
+ // Probably not, because interacting with Editor world during PIE is
100
+ // confusing. But for "Editor subsystems" usage, we usually want Editor
101
+ // world. Let's fallback if not found, just in case.
102
+ }
103
+
104
+ UEditorActorSubsystem *ActorSS =
105
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
106
+ if (!ActorSS)
107
+ return nullptr;
108
+
109
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
110
+ AActor *ExactMatch = nullptr;
111
+ TArray<AActor *> FuzzyMatches;
112
+
113
+ for (AActor *A : AllActors) {
114
+ if (!A)
115
+ continue;
116
+ if (A->GetActorLabel().Equals(Target, ESearchCase::IgnoreCase) ||
117
+ A->GetName().Equals(Target, ESearchCase::IgnoreCase) ||
118
+ A->GetPathName().Equals(Target, ESearchCase::IgnoreCase)) {
119
+ ExactMatch = A;
120
+ break;
121
+ }
122
+ // Collect fuzzy matches
123
+ if (A->GetActorLabel().Contains(Target, ESearchCase::IgnoreCase)) {
124
+ FuzzyMatches.Add(A);
125
+ }
126
+ }
127
+
128
+ if (ExactMatch) {
129
+ return ExactMatch;
130
+ }
131
+
132
+ // If no exact match, check fuzzy matches
133
+ if (FuzzyMatches.Num() == 1) {
134
+ return FuzzyMatches[0];
135
+ } else if (FuzzyMatches.Num() > 1) {
136
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Warning,
137
+ TEXT("FindActorByName: Ambiguous match for '%s'. Found %d matches."),
138
+ *Target, FuzzyMatches.Num());
139
+ }
140
+
141
+ // Fallback: try to load as asset if it looks like a path
142
+ if (Target.StartsWith(TEXT("/"))) {
143
+ if (UObject *Obj = UEditorAssetLibrary::LoadAsset(Target)) {
144
+ return Cast<AActor>(Obj);
145
+ }
146
+ }
147
+ #endif
148
+ return nullptr;
149
+ }
150
+
151
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSpawn(
152
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
153
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
154
+ #if WITH_EDITOR
155
+ FString ClassPath;
156
+ Payload->TryGetStringField(TEXT("classPath"), ClassPath);
157
+ FString ActorName;
158
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
159
+ FVector Location =
160
+ ExtractVectorField(Payload, TEXT("location"), FVector::ZeroVector);
161
+ FRotator Rotation =
162
+ ExtractRotatorField(Payload, TEXT("rotation"), FRotator::ZeroRotator);
163
+
164
+ UClass *ResolvedClass = nullptr;
165
+ FString MeshPath;
166
+ Payload->TryGetStringField(TEXT("meshPath"), MeshPath);
167
+ UStaticMesh *ResolvedStaticMesh = nullptr;
168
+ USkeletalMesh *ResolvedSkeletalMesh = nullptr;
169
+
170
+ // Skip LoadAsset for script classes (e.g. /Script/Engine.CameraActor) to
171
+ // avoid LogEditorAssetSubsystem errors
172
+ if ((ClassPath.StartsWith(TEXT("/")) || ClassPath.Contains(TEXT("/"))) &&
173
+ !ClassPath.StartsWith(TEXT("/Script/"))) {
174
+ if (UObject *Loaded = UEditorAssetLibrary::LoadAsset(ClassPath)) {
175
+ if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
176
+ ResolvedClass = BP->GeneratedClass;
177
+ else if (UClass *C = Cast<UClass>(Loaded))
178
+ ResolvedClass = C;
179
+ else if (UStaticMesh *Mesh = Cast<UStaticMesh>(Loaded))
180
+ ResolvedStaticMesh = Mesh;
181
+ else if (USkeletalMesh *SkelMesh = Cast<USkeletalMesh>(Loaded))
182
+ ResolvedSkeletalMesh = SkelMesh;
183
+ }
184
+ }
185
+ if (!ResolvedClass && !ResolvedStaticMesh && !ResolvedSkeletalMesh)
186
+ ResolvedClass = ResolveClassByName(ClassPath);
187
+
188
+ // If explicit mesh path provided for a general spawn request
189
+ if (!ResolvedStaticMesh && !ResolvedSkeletalMesh && !MeshPath.IsEmpty()) {
190
+ if (UObject *MeshObj = UEditorAssetLibrary::LoadAsset(MeshPath)) {
191
+ ResolvedStaticMesh = Cast<UStaticMesh>(MeshObj);
192
+ if (!ResolvedStaticMesh)
193
+ ResolvedSkeletalMesh = Cast<USkeletalMesh>(MeshObj);
194
+ }
195
+ }
196
+
197
+ // Force StaticMeshActor if we have a resolved mesh, regardless of class input
198
+ // (unless it's a specific subclass)
199
+ bool bSpawnStaticMeshActor = (ResolvedStaticMesh != nullptr);
200
+ bool bSpawnSkeletalMeshActor = (ResolvedSkeletalMesh != nullptr);
201
+
202
+ if (!bSpawnStaticMeshActor && !bSpawnSkeletalMeshActor && ResolvedClass) {
203
+ bSpawnStaticMeshActor =
204
+ ResolvedClass->IsChildOf(AStaticMeshActor::StaticClass());
205
+ if (!bSpawnStaticMeshActor)
206
+ bSpawnSkeletalMeshActor =
207
+ ResolvedClass->IsChildOf(ASkeletalMeshActor::StaticClass());
208
+ }
209
+
210
+ // Explicitly use StaticMeshActor class if we have a mesh but no class, or if
211
+ // we decided to spawn a static mesh actor
212
+ if (bSpawnStaticMeshActor && !ResolvedClass) {
213
+ ResolvedClass = AStaticMeshActor::StaticClass();
214
+ } else if (bSpawnSkeletalMeshActor && !ResolvedClass) {
215
+ ResolvedClass = ASkeletalMeshActor::StaticClass();
216
+ }
217
+
218
+ if (!ResolvedClass && !bSpawnStaticMeshActor && !bSpawnSkeletalMeshActor) {
219
+ const FString ErrorMsg =
220
+ FString::Printf(TEXT("Class not found: %s. Verify plugin is enabled if "
221
+ "using a plugin class."),
222
+ *ClassPath);
223
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("CLASS_NOT_FOUND"),
224
+ ErrorMsg);
225
+ return true;
226
+ }
227
+
228
+ UEditorActorSubsystem *ActorSS =
229
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
230
+ AActor *Spawned = nullptr;
231
+
232
+ // Support PIE spawning
233
+ UWorld *TargetWorld = (GEditor->PlayWorld) ? GEditor->PlayWorld : nullptr;
234
+
235
+ if (TargetWorld) {
236
+ // PIE Path
237
+ FActorSpawnParameters SpawnParams;
238
+ SpawnParams.SpawnCollisionHandlingOverride =
239
+ ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
240
+
241
+ UClass *ClassToSpawn =
242
+ ResolvedClass
243
+ ? ResolvedClass
244
+ : (bSpawnStaticMeshActor ? AStaticMeshActor::StaticClass()
245
+ : (bSpawnSkeletalMeshActor
246
+ ? ASkeletalMeshActor::StaticClass()
247
+ : AActor::StaticClass()));
248
+ Spawned = TargetWorld->SpawnActor(ClassToSpawn, &Location, &Rotation,
249
+ SpawnParams);
250
+
251
+ if (Spawned) {
252
+ if (bSpawnStaticMeshActor) {
253
+ if (AStaticMeshActor *StaticMeshActor =
254
+ Cast<AStaticMeshActor>(Spawned)) {
255
+ if (UStaticMeshComponent *MeshComponent =
256
+ StaticMeshActor->GetStaticMeshComponent()) {
257
+ if (ResolvedStaticMesh) {
258
+ MeshComponent->SetStaticMesh(ResolvedStaticMesh);
259
+ }
260
+ MeshComponent->SetMobility(EComponentMobility::Movable);
261
+ // PIE actors don't need MarkRenderStateDirty in the same way, but
262
+ // it doesn't hurt
263
+ }
264
+ }
265
+ } else if (bSpawnSkeletalMeshActor) {
266
+ if (ASkeletalMeshActor *SkelActor = Cast<ASkeletalMeshActor>(Spawned)) {
267
+ if (USkeletalMeshComponent *SkelComp =
268
+ SkelActor->GetSkeletalMeshComponent()) {
269
+ if (ResolvedSkeletalMesh) {
270
+ SkelComp->SetSkeletalMesh(ResolvedSkeletalMesh);
271
+ }
272
+ SkelComp->SetMobility(EComponentMobility::Movable);
273
+ }
274
+ }
275
+ }
276
+ }
277
+ } else {
278
+ // Editor Path
279
+ if (bSpawnStaticMeshActor) {
280
+ Spawned = ActorSS->SpawnActorFromClass(
281
+ ResolvedClass ? ResolvedClass : AStaticMeshActor::StaticClass(),
282
+ Location, Rotation);
283
+ if (Spawned) {
284
+ Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
285
+ ETeleportType::TeleportPhysics);
286
+ if (AStaticMeshActor *StaticMeshActor =
287
+ Cast<AStaticMeshActor>(Spawned)) {
288
+ if (UStaticMeshComponent *MeshComponent =
289
+ StaticMeshActor->GetStaticMeshComponent()) {
290
+ if (ResolvedStaticMesh) {
291
+ MeshComponent->SetStaticMesh(ResolvedStaticMesh);
292
+ }
293
+ MeshComponent->SetMobility(EComponentMobility::Movable);
294
+ MeshComponent->MarkRenderStateDirty();
295
+ }
296
+ }
297
+ }
298
+ } else if (bSpawnSkeletalMeshActor) {
299
+ Spawned = ActorSS->SpawnActorFromClass(
300
+ ResolvedClass ? ResolvedClass : ASkeletalMeshActor::StaticClass(),
301
+ Location, Rotation);
302
+ if (Spawned) {
303
+ Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
304
+ ETeleportType::TeleportPhysics);
305
+ if (ASkeletalMeshActor *SkelActor = Cast<ASkeletalMeshActor>(Spawned)) {
306
+ if (USkeletalMeshComponent *SkelComp =
307
+ SkelActor->GetSkeletalMeshComponent()) {
308
+ if (ResolvedSkeletalMesh) {
309
+ SkelComp->SetSkeletalMesh(ResolvedSkeletalMesh);
310
+ }
311
+ SkelComp->SetMobility(EComponentMobility::Movable);
312
+ SkelComp->MarkRenderStateDirty();
313
+ }
314
+ }
315
+ }
316
+ } else {
317
+ Spawned = ActorSS->SpawnActorFromClass(ResolvedClass, Location, Rotation);
318
+ if (Spawned) {
319
+ Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
320
+ ETeleportType::TeleportPhysics);
321
+ }
322
+ }
323
+ }
324
+
325
+ if (!Spawned) {
326
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("SPAWN_FAILED"),
327
+ TEXT("Failed to spawn actor"));
328
+
329
+ return true;
330
+ }
331
+
332
+ if (!ActorName.IsEmpty()) {
333
+ Spawned->SetActorLabel(ActorName);
334
+ } else {
335
+ // Auto-generate a friendly label from the mesh or class name
336
+ FString BaseName;
337
+ if (ResolvedStaticMesh) {
338
+ BaseName = ResolvedStaticMesh->GetName();
339
+ } else if (ResolvedSkeletalMesh) {
340
+ BaseName = ResolvedSkeletalMesh->GetName();
341
+ } else if (ResolvedClass) {
342
+ BaseName = ResolvedClass->GetName();
343
+ if (BaseName.EndsWith(TEXT("_C"))) {
344
+ BaseName.RemoveFromEnd(TEXT("_C"));
345
+ }
346
+ } else {
347
+ BaseName = TEXT("Actor");
348
+ }
349
+ Spawned->SetActorLabel(BaseName);
350
+ }
351
+
352
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
353
+ Data->SetStringField(TEXT("id"), Spawned->GetActorLabel());
354
+ Data->SetStringField(TEXT("name"), Spawned->GetActorLabel());
355
+ Data->SetStringField(TEXT("objectPath"), Spawned->GetPathName());
356
+ // Provide the resolved class path useful for referencing
357
+ if (ResolvedClass)
358
+ Data->SetStringField(TEXT("classPath"), ResolvedClass->GetPathName());
359
+ else
360
+ Data->SetStringField(TEXT("classPath"), ClassPath);
361
+
362
+ if (ResolvedStaticMesh)
363
+ Data->SetStringField(TEXT("meshPath"), ResolvedStaticMesh->GetPathName());
364
+ else if (ResolvedSkeletalMesh)
365
+ Data->SetStringField(TEXT("meshPath"), ResolvedSkeletalMesh->GetPathName());
366
+
367
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
368
+ TEXT("ControlActor: Spawned actor '%s'"), *Spawned->GetActorLabel());
369
+
370
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Actor spawned"), Data);
371
+ return true;
372
+
373
+ #else
374
+ return false;
375
+ #endif
376
+ }
377
+
378
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSpawnBlueprint(
379
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
380
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
381
+ #if WITH_EDITOR
382
+ FString BlueprintPath;
383
+ Payload->TryGetStringField(TEXT("blueprintPath"), BlueprintPath);
384
+ if (BlueprintPath.IsEmpty()) {
385
+ SendAutomationResponse(Socket, RequestId, false,
386
+ TEXT("Blueprint path required"), nullptr,
387
+ TEXT("INVALID_ARGUMENT"));
388
+ return true;
389
+ }
390
+
391
+ FString ActorName;
392
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
393
+ FVector Location =
394
+ ExtractVectorField(Payload, TEXT("location"), FVector::ZeroVector);
395
+ FRotator Rotation =
396
+ ExtractRotatorField(Payload, TEXT("rotation"), FRotator::ZeroRotator);
397
+
398
+ UClass *ResolvedClass = nullptr;
399
+
400
+ // Prefer the same blueprint resolution heuristics used by manage_blueprint
401
+ // so that short names and package paths behave consistently.
402
+ FString NormalizedPath;
403
+ FString LoadError;
404
+ if (!BlueprintPath.IsEmpty()) {
405
+ UBlueprint *BlueprintAsset =
406
+ LoadBlueprintAsset(BlueprintPath, NormalizedPath, LoadError);
407
+ if (BlueprintAsset && BlueprintAsset->GeneratedClass) {
408
+ ResolvedClass = BlueprintAsset->GeneratedClass;
409
+ }
410
+ }
411
+
412
+ if (!ResolvedClass && (BlueprintPath.StartsWith(TEXT("/")) ||
413
+ BlueprintPath.Contains(TEXT("/")))) {
414
+ if (UObject *Loaded = UEditorAssetLibrary::LoadAsset(BlueprintPath)) {
415
+ if (UBlueprint *BP = Cast<UBlueprint>(Loaded))
416
+ ResolvedClass = BP->GeneratedClass;
417
+ else if (UClass *C = Cast<UClass>(Loaded))
418
+ ResolvedClass = C;
419
+ }
420
+ }
421
+ if (!ResolvedClass)
422
+ ResolvedClass = ResolveClassByName(BlueprintPath);
423
+
424
+ if (!ResolvedClass) {
425
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
426
+ Resp->SetStringField(TEXT("error"), TEXT("Blueprint class not found"));
427
+ SendAutomationResponse(Socket, RequestId, false,
428
+ TEXT("Blueprint class not found"), Resp,
429
+ TEXT("CLASS_NOT_FOUND"));
430
+ return true;
431
+ }
432
+
433
+ UEditorActorSubsystem *ActorSS =
434
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
435
+
436
+ // Debug log the received location
437
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
438
+ TEXT("spawn_blueprint: Location=(%f, %f, %f) Rotation=(%f, %f, %f)"),
439
+ Location.X, Location.Y, Location.Z, Rotation.Pitch, Rotation.Yaw,
440
+ Rotation.Roll);
441
+
442
+ AActor *Spawned = nullptr;
443
+ UWorld *TargetWorld = (GEditor->PlayWorld) ? GEditor->PlayWorld : nullptr;
444
+
445
+ if (TargetWorld) {
446
+ // PIE Path
447
+ FActorSpawnParameters SpawnParams;
448
+ SpawnParams.SpawnCollisionHandlingOverride =
449
+ ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
450
+ Spawned = TargetWorld->SpawnActor(ResolvedClass, &Location, &Rotation,
451
+ SpawnParams);
452
+ // Ensure physics/teleport if needed, though SpawnActor should handle it.
453
+ } else {
454
+ // Editor Path
455
+ Spawned = ActorSS->SpawnActorFromClass(ResolvedClass, Location, Rotation);
456
+ // Explicitly set location and rotation in case SpawnActorFromClass didn't
457
+ // apply them correctly (legacy fix)
458
+ if (Spawned) {
459
+ Spawned->SetActorLocationAndRotation(Location, Rotation, false, nullptr,
460
+ ETeleportType::TeleportPhysics);
461
+ }
462
+ }
463
+
464
+ if (!Spawned) {
465
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
466
+ Resp->SetStringField(TEXT("error"), TEXT("Failed to spawn blueprint"));
467
+ SendAutomationResponse(Socket, RequestId, false,
468
+ TEXT("Failed to spawn blueprint"), Resp,
469
+ TEXT("SPAWN_FAILED"));
470
+ return true;
471
+ }
472
+
473
+ if (!ActorName.IsEmpty())
474
+ Spawned->SetActorLabel(ActorName);
475
+
476
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
477
+ Resp->SetBoolField(TEXT("success"), true);
478
+ Resp->SetStringField(TEXT("actorName"), Spawned->GetActorLabel());
479
+ Resp->SetStringField(TEXT("actorPath"), Spawned->GetPathName());
480
+ Resp->SetStringField(TEXT("classPath"), ResolvedClass->GetPathName());
481
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
482
+ TEXT("ControlActor: Spawned blueprint '%s'"),
483
+ *Spawned->GetActorLabel());
484
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Blueprint spawned"),
485
+ Resp, FString());
486
+ return true;
487
+ #else
488
+ return false;
489
+ #endif
490
+ }
491
+
492
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorDelete(
493
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
494
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
495
+ #if WITH_EDITOR
496
+ TArray<FString> Targets;
497
+ const TArray<TSharedPtr<FJsonValue>> *NamesArray = nullptr;
498
+ if (Payload->TryGetArrayField(TEXT("actorNames"), NamesArray) && NamesArray) {
499
+ for (const TSharedPtr<FJsonValue> &Entry : *NamesArray) {
500
+ if (Entry.IsValid() && Entry->Type == EJson::String) {
501
+ const FString Value = Entry->AsString().TrimStartAndEnd();
502
+ if (!Value.IsEmpty())
503
+ Targets.AddUnique(Value);
504
+ }
505
+ }
506
+ }
507
+
508
+ FString SingleName;
509
+ if (Targets.Num() == 0) {
510
+ Payload->TryGetStringField(TEXT("actorName"), SingleName);
511
+ if (!SingleName.IsEmpty())
512
+ Targets.AddUnique(SingleName);
513
+ }
514
+
515
+ if (Targets.Num() == 0) {
516
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("INVALID_ARGUMENT"),
517
+ TEXT("actorName or actorNames required"));
518
+ return true;
519
+ }
520
+
521
+ UEditorActorSubsystem *ActorSS =
522
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
523
+ TArray<FString> Deleted;
524
+ TArray<FString> Missing;
525
+
526
+ for (const FString &Name : Targets) {
527
+ AActor *Found = FindActorByName(Name);
528
+ if (!Found) {
529
+ Missing.Add(Name);
530
+ continue;
531
+ }
532
+ if (ActorSS->DestroyActor(Found)) {
533
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
534
+ TEXT("ControlActor: Deleted actor '%s'"), *Name);
535
+ Deleted.Add(Name);
536
+ } else
537
+ Missing.Add(Name);
538
+ }
539
+
540
+ const bool bAllDeleted = Missing.Num() == 0;
541
+ const bool bAnyDeleted = Deleted.Num() > 0;
542
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
543
+ Resp->SetBoolField(TEXT("success"), bAllDeleted);
544
+ Resp->SetNumberField(TEXT("deletedCount"), Deleted.Num());
545
+
546
+ TArray<TSharedPtr<FJsonValue>> DeletedArray;
547
+ for (const FString &Name : Deleted)
548
+ DeletedArray.Add(MakeShared<FJsonValueString>(Name));
549
+ Resp->SetArrayField(TEXT("deleted"), DeletedArray);
550
+
551
+ if (Missing.Num() > 0) {
552
+ TArray<TSharedPtr<FJsonValue>> MissingArray;
553
+ for (const FString &Name : Missing)
554
+ MissingArray.Add(MakeShared<FJsonValueString>(Name));
555
+ Resp->SetArrayField(TEXT("missing"), MissingArray);
556
+ }
557
+
558
+ FString Message;
559
+ FString ErrorCode;
560
+ if (!bAnyDeleted && Missing.Num() > 0) {
561
+ Message = TEXT("Actors not found");
562
+ ErrorCode = TEXT("NOT_FOUND");
563
+ } else {
564
+ Message = bAllDeleted ? TEXT("Actors deleted")
565
+ : TEXT("Some actors could not be deleted");
566
+ ErrorCode = bAllDeleted ? FString() : TEXT("DELETE_PARTIAL");
567
+ }
568
+
569
+ if (!bAllDeleted && Missing.Num() > 0 && !bAnyDeleted) {
570
+ SendStandardErrorResponse(this, Socket, RequestId, ErrorCode, Message);
571
+ } else {
572
+ SendStandardSuccessResponse(this, Socket, RequestId, Message, Resp);
573
+ }
574
+ return true;
575
+ #else
576
+ return false;
577
+ #endif
578
+ }
579
+
580
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorApplyForce(
581
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
582
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
583
+ #if WITH_EDITOR
584
+ FString TargetName;
585
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
586
+ FVector ForceVector =
587
+ ExtractVectorField(Payload, TEXT("force"), FVector::ZeroVector);
588
+
589
+ AActor *Found = FindActorByName(TargetName);
590
+ if (!Found) {
591
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
592
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
593
+ return true;
594
+ }
595
+
596
+ UPrimitiveComponent *Prim =
597
+ Found->FindComponentByClass<UPrimitiveComponent>();
598
+ if (!Prim) {
599
+ if (UStaticMeshComponent *SMC =
600
+ Found->FindComponentByClass<UStaticMeshComponent>())
601
+ Prim = SMC;
602
+ }
603
+
604
+ if (!Prim) {
605
+ SendAutomationResponse(Socket, RequestId, false,
606
+ TEXT("No component to apply force"), nullptr,
607
+ TEXT("NO_COMPONENT"));
608
+ return true;
609
+ }
610
+
611
+ if (Prim->Mobility == EComponentMobility::Static)
612
+ Prim->SetMobility(EComponentMobility::Movable);
613
+
614
+ // Ensure collision is enabled for physics
615
+ if (Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision) {
616
+ Prim->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
617
+ }
618
+
619
+ // Check if collision geometry exists (common failure for empty
620
+ // StaticMeshActors)
621
+ if (UStaticMeshComponent *SMC = Cast<UStaticMeshComponent>(Prim)) {
622
+ if (!SMC->GetStaticMesh()) {
623
+ SendStandardErrorResponse(
624
+ this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
625
+ TEXT("StaticMeshComponent has no StaticMesh assigned."), nullptr);
626
+ return true;
627
+ }
628
+ if (!SMC->GetStaticMesh()->GetBodySetup()) {
629
+ SendStandardErrorResponse(
630
+ this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
631
+ TEXT("StaticMesh has no collision geometry (BodySetup is null)."),
632
+ nullptr);
633
+ return true;
634
+ }
635
+ }
636
+
637
+ if (!Prim->IsSimulatingPhysics()) {
638
+ Prim->SetSimulatePhysics(true);
639
+ // Must recreate physics state for the body to be properly initialized in
640
+ // Editor
641
+ Prim->RecreatePhysicsState();
642
+ }
643
+
644
+ Prim->AddForce(ForceVector);
645
+ Prim->WakeAllRigidBodies();
646
+ Prim->MarkRenderStateDirty();
647
+
648
+ // Verify physics state
649
+ const bool bIsSimulating = Prim->IsSimulatingPhysics();
650
+
651
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
652
+ Data->SetBoolField(TEXT("simulating"), bIsSimulating);
653
+ TArray<TSharedPtr<FJsonValue>> Applied;
654
+ Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.X));
655
+ Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.Y));
656
+ Applied.Add(MakeShared<FJsonValueNumber>(ForceVector.Z));
657
+ Data->SetArrayField(TEXT("applied"), Applied);
658
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
659
+
660
+ if (!bIsSimulating) {
661
+ FString FailureReason = TEXT("Failed to enable physics simulation.");
662
+ if (Prim->GetCollisionEnabled() == ECollisionEnabled::NoCollision) {
663
+ FailureReason += TEXT(" Collision is disabled.");
664
+ } else if (Prim->Mobility != EComponentMobility::Movable) {
665
+ FailureReason += TEXT(" Component is not Movable.");
666
+ }
667
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("PHYSICS_FAILED"),
668
+ FailureReason, Data);
669
+ return true;
670
+ }
671
+
672
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
673
+ TEXT("ControlActor: Applied force to '%s'"), *Found->GetActorLabel());
674
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Force applied"),
675
+ Data);
676
+ return true;
677
+ #else
678
+ return false;
679
+ #endif
680
+ }
681
+
682
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSetTransform(
683
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
684
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
685
+ #if WITH_EDITOR
686
+ FString TargetName;
687
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
688
+ if (TargetName.IsEmpty()) {
689
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
690
+ nullptr, TEXT("INVALID_ARGUMENT"));
691
+ return true;
692
+ }
693
+
694
+ AActor *Found = FindActorByName(TargetName);
695
+ if (!Found) {
696
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
697
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
698
+ return true;
699
+ }
700
+
701
+ FVector Location =
702
+ ExtractVectorField(Payload, TEXT("location"), Found->GetActorLocation());
703
+ FRotator Rotation =
704
+ ExtractRotatorField(Payload, TEXT("rotation"), Found->GetActorRotation());
705
+ FVector Scale =
706
+ ExtractVectorField(Payload, TEXT("scale"), Found->GetActorScale3D());
707
+
708
+ Found->Modify();
709
+ Found->SetActorLocation(Location, false, nullptr,
710
+ ETeleportType::TeleportPhysics);
711
+ Found->SetActorRotation(Rotation, ETeleportType::TeleportPhysics);
712
+ Found->SetActorScale3D(Scale);
713
+ Found->MarkComponentsRenderStateDirty();
714
+ Found->MarkPackageDirty();
715
+
716
+ // Verify transform
717
+ const FVector NewLoc = Found->GetActorLocation();
718
+ const FRotator NewRot = Found->GetActorRotation();
719
+ const FVector NewScale = Found->GetActorScale3D();
720
+
721
+ const bool bLocMatch = NewLoc.Equals(Location, 1.0f); // 1 unit tolerance
722
+ // Rotation comparison is tricky due to normalization, skipping strict check
723
+ // for now but logging if very different
724
+ const bool bScaleMatch = NewScale.Equals(Scale, 0.01f);
725
+
726
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
727
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
728
+
729
+ auto MakeArray = [](const FVector &Vec) {
730
+ TArray<TSharedPtr<FJsonValue>> Arr;
731
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
732
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
733
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
734
+ return Arr;
735
+ };
736
+
737
+ Data->SetArrayField(TEXT("location"), MakeArray(NewLoc));
738
+ Data->SetArrayField(TEXT("scale"), MakeArray(NewScale));
739
+
740
+ if (!bLocMatch || !bScaleMatch) {
741
+ SendStandardErrorResponse(this, Socket, RequestId,
742
+ TEXT("TRANSFORM_MISMATCH"),
743
+ TEXT("Failed to set transform exactly"), Data);
744
+ return true;
745
+ }
746
+
747
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
748
+ TEXT("ControlActor: Set transform for '%s'"), *Found->GetActorLabel());
749
+ SendStandardSuccessResponse(this, Socket, RequestId,
750
+ TEXT("Actor transform updated"), Data);
751
+ return true;
752
+ #else
753
+ return false;
754
+ #endif
755
+ }
756
+
757
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorGetTransform(
758
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
759
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
760
+ #if WITH_EDITOR
761
+ FString TargetName;
762
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
763
+ if (TargetName.IsEmpty()) {
764
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("INVALID_ARGUMENT"),
765
+ TEXT("actorName required"));
766
+ return true;
767
+ }
768
+
769
+ AActor *Found = FindActorByName(TargetName);
770
+ if (!Found) {
771
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("ACTOR_NOT_FOUND"),
772
+ TEXT("Actor not found"));
773
+ return true;
774
+ }
775
+
776
+ const FTransform Current = Found->GetActorTransform();
777
+ const FVector Location = Current.GetLocation();
778
+ const FRotator Rotation = Current.GetRotation().Rotator();
779
+ const FVector Scale = Current.GetScale3D();
780
+
781
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
782
+
783
+ auto MakeArray = [](const FVector &Vec) {
784
+ TArray<TSharedPtr<FJsonValue>> Arr;
785
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
786
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
787
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
788
+ return Arr;
789
+ };
790
+
791
+ Data->SetArrayField(TEXT("location"), MakeArray(Location));
792
+ TArray<TSharedPtr<FJsonValue>> RotArray;
793
+ RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Pitch));
794
+ RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Yaw));
795
+ RotArray.Add(MakeShared<FJsonValueNumber>(Rotation.Roll));
796
+ Data->SetArrayField(TEXT("rotation"), RotArray);
797
+ Data->SetArrayField(TEXT("scale"), MakeArray(Scale));
798
+
799
+ SendStandardSuccessResponse(this, Socket, RequestId,
800
+ TEXT("Actor transform retrieved"), Data);
801
+ return true;
802
+ #else
803
+ return false;
804
+ #endif
805
+ }
806
+
807
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSetVisibility(
808
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
809
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
810
+ #if WITH_EDITOR
811
+ FString TargetName;
812
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
813
+ if (TargetName.IsEmpty()) {
814
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
815
+ nullptr, TEXT("INVALID_ARGUMENT"));
816
+ return true;
817
+ }
818
+
819
+ bool bVisible = true;
820
+ if (Payload->HasField(TEXT("visible")))
821
+ Payload->TryGetBoolField(TEXT("visible"), bVisible);
822
+
823
+ AActor *Found = FindActorByName(TargetName);
824
+ if (!Found) {
825
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
826
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
827
+ return true;
828
+ }
829
+
830
+ Found->Modify();
831
+ Found->SetActorHiddenInGame(!bVisible);
832
+ Found->SetActorEnableCollision(bVisible);
833
+
834
+ for (UActorComponent *Comp : Found->GetComponents()) {
835
+ if (!Comp)
836
+ continue;
837
+ if (UPrimitiveComponent *Prim = Cast<UPrimitiveComponent>(Comp)) {
838
+ Prim->SetVisibility(bVisible, true);
839
+ Prim->SetHiddenInGame(!bVisible);
840
+ }
841
+ }
842
+
843
+ Found->MarkComponentsRenderStateDirty();
844
+ Found->MarkPackageDirty();
845
+
846
+ // Verify visibility state
847
+ const bool bIsHidden = Found->IsHidden();
848
+ const bool bStateMatches = (bIsHidden == !bVisible);
849
+
850
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
851
+ Data->SetBoolField(TEXT("visible"), !bIsHidden);
852
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
853
+
854
+ if (!bStateMatches) {
855
+ SendStandardErrorResponse(this, Socket, RequestId,
856
+ TEXT("VISIBILITY_MISMATCH"),
857
+ TEXT("Failed to set actor visibility"), Data);
858
+ return true;
859
+ }
860
+
861
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
862
+ TEXT("ControlActor: Set visibility to %s for '%s'"),
863
+ bVisible ? TEXT("True") : TEXT("False"), *Found->GetActorLabel());
864
+ SendStandardSuccessResponse(this, Socket, RequestId,
865
+ TEXT("Actor visibility updated"), Data);
866
+ return true;
867
+ #else
868
+ return false;
869
+ #endif
870
+ }
871
+
872
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorAddComponent(
873
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
874
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
875
+ #if WITH_EDITOR
876
+ FString TargetName;
877
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
878
+ if (TargetName.IsEmpty()) {
879
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
880
+ nullptr, TEXT("INVALID_ARGUMENT"));
881
+ return true;
882
+ }
883
+
884
+ FString ComponentType;
885
+ Payload->TryGetStringField(TEXT("componentType"), ComponentType);
886
+ if (ComponentType.IsEmpty()) {
887
+ SendAutomationResponse(Socket, RequestId, false,
888
+ TEXT("componentType required"), nullptr,
889
+ TEXT("INVALID_ARGUMENT"));
890
+ return true;
891
+ }
892
+
893
+ FString ComponentName;
894
+ Payload->TryGetStringField(TEXT("componentName"), ComponentName);
895
+
896
+ AActor *Found = FindActorByName(TargetName);
897
+ if (!Found) {
898
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
899
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
900
+ return true;
901
+ }
902
+
903
+ UClass *ComponentClass = ResolveClassByName(ComponentType);
904
+ if (!ComponentClass ||
905
+ !ComponentClass->IsChildOf(UActorComponent::StaticClass())) {
906
+ SendAutomationResponse(Socket, RequestId, false,
907
+ TEXT("Component class not found"), nullptr,
908
+ TEXT("CLASS_NOT_FOUND"));
909
+ return true;
910
+ }
911
+
912
+ if (ComponentName.TrimStartAndEnd().IsEmpty())
913
+ ComponentName = FString::Printf(TEXT("%s_%d"), *ComponentClass->GetName(),
914
+ FMath::Rand());
915
+
916
+ FName DesiredName = FName(*ComponentName);
917
+ UActorComponent *NewComponent = NewObject<UActorComponent>(
918
+ Found, ComponentClass, DesiredName, RF_Transactional);
919
+ if (!NewComponent) {
920
+ SendAutomationResponse(Socket, RequestId, false,
921
+ TEXT("Failed to create component"), nullptr,
922
+ TEXT("CREATE_COMPONENT_FAILED"));
923
+ return true;
924
+ }
925
+
926
+ Found->Modify();
927
+ NewComponent->SetFlags(RF_Transactional);
928
+ Found->AddInstanceComponent(NewComponent);
929
+ NewComponent->OnComponentCreated();
930
+
931
+ if (USceneComponent *SceneComp = Cast<USceneComponent>(NewComponent)) {
932
+ if (Found->GetRootComponent() && !SceneComp->GetAttachParent()) {
933
+ SceneComp->SetupAttachment(Found->GetRootComponent());
934
+ }
935
+ }
936
+
937
+ // Force lights to be movable to ensure they work without baking (Issue #6
938
+ // fix) We check for "LightComponent" class name to avoid dependency issues if
939
+ // header is obscure, but ULightComponent is standard.
940
+ if (NewComponent->IsA(ULightComponent::StaticClass())) {
941
+ if (USceneComponent *SC = Cast<USceneComponent>(NewComponent)) {
942
+ SC->SetMobility(EComponentMobility::Movable);
943
+ }
944
+ }
945
+
946
+ // Special handling for StaticMeshComponent meshPath convenience
947
+ if (UStaticMeshComponent *SMC = Cast<UStaticMeshComponent>(NewComponent)) {
948
+ FString MeshPath;
949
+ if (Payload->TryGetStringField(TEXT("meshPath"), MeshPath) &&
950
+ !MeshPath.IsEmpty()) {
951
+ if (UObject *LoadedMesh = UEditorAssetLibrary::LoadAsset(MeshPath)) {
952
+ if (UStaticMesh *Mesh = Cast<UStaticMesh>(LoadedMesh)) {
953
+ SMC->SetStaticMesh(Mesh);
954
+ }
955
+ }
956
+ }
957
+ }
958
+
959
+ TArray<FString> AppliedProperties;
960
+ TArray<FString> PropertyWarnings;
961
+ const TSharedPtr<FJsonObject> *PropertiesPtr = nullptr;
962
+ if (Payload->TryGetObjectField(TEXT("properties"), PropertiesPtr) &&
963
+ PropertiesPtr && (*PropertiesPtr).IsValid()) {
964
+ for (const auto &Pair : (*PropertiesPtr)->Values) {
965
+ FProperty *Property = ComponentClass->FindPropertyByName(*Pair.Key);
966
+ if (!Property) {
967
+ PropertyWarnings.Add(
968
+ FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
969
+ continue;
970
+ }
971
+ FString ApplyError;
972
+ if (ApplyJsonValueToProperty(NewComponent, Property, Pair.Value,
973
+ ApplyError))
974
+ AppliedProperties.Add(Pair.Key);
975
+ else
976
+ PropertyWarnings.Add(FString::Printf(TEXT("Failed to set %s: %s"),
977
+ *Pair.Key, *ApplyError));
978
+ }
979
+ }
980
+
981
+ NewComponent->RegisterComponent();
982
+ if (USceneComponent *SceneComp = Cast<USceneComponent>(NewComponent))
983
+ SceneComp->UpdateComponentToWorld();
984
+ NewComponent->MarkPackageDirty();
985
+ Found->MarkPackageDirty();
986
+
987
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
988
+ Resp->SetBoolField(TEXT("success"), true);
989
+ Resp->SetStringField(TEXT("componentName"), NewComponent->GetName());
990
+ Resp->SetStringField(TEXT("componentPath"), NewComponent->GetPathName());
991
+ Resp->SetStringField(TEXT("componentClass"), ComponentClass->GetPathName());
992
+ if (AppliedProperties.Num() > 0) {
993
+ TArray<TSharedPtr<FJsonValue>> PropsArray;
994
+ for (const FString &PropName : AppliedProperties)
995
+ PropsArray.Add(MakeShared<FJsonValueString>(PropName));
996
+ Resp->SetArrayField(TEXT("appliedProperties"), PropsArray);
997
+ }
998
+ if (PropertyWarnings.Num() > 0) {
999
+ TArray<TSharedPtr<FJsonValue>> WarnArray;
1000
+ for (const FString &Warning : PropertyWarnings)
1001
+ WarnArray.Add(MakeShared<FJsonValueString>(Warning));
1002
+ Resp->SetArrayField(TEXT("warnings"), WarnArray);
1003
+ }
1004
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1005
+ TEXT("ControlActor: Added component '%s' to '%s'"),
1006
+ *NewComponent->GetName(), *Found->GetActorLabel());
1007
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Component added"), Resp,
1008
+ FString());
1009
+ return true;
1010
+ #else
1011
+ return false;
1012
+ #endif
1013
+ }
1014
+
1015
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSetComponentProperties(
1016
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1017
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1018
+ #if WITH_EDITOR
1019
+ FString TargetName;
1020
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1021
+ if (TargetName.IsEmpty()) {
1022
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1023
+ nullptr, TEXT("INVALID_ARGUMENT"));
1024
+ return true;
1025
+ }
1026
+
1027
+ FString ComponentName;
1028
+ Payload->TryGetStringField(TEXT("componentName"), ComponentName);
1029
+ if (ComponentName.IsEmpty()) {
1030
+ SendAutomationResponse(Socket, RequestId, false,
1031
+ TEXT("componentName required"), nullptr,
1032
+ TEXT("INVALID_ARGUMENT"));
1033
+ return true;
1034
+ }
1035
+
1036
+ const TSharedPtr<FJsonObject> *PropertiesPtr = nullptr;
1037
+ if (!(Payload->TryGetObjectField(TEXT("properties"), PropertiesPtr) &&
1038
+ PropertiesPtr && PropertiesPtr->IsValid())) {
1039
+ SendAutomationResponse(Socket, RequestId, false,
1040
+ TEXT("properties object required"), nullptr,
1041
+ TEXT("INVALID_ARGUMENT"));
1042
+ return true;
1043
+ }
1044
+
1045
+ AActor *Found = FindActorByName(TargetName);
1046
+ if (!Found) {
1047
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1048
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1049
+ return true;
1050
+ }
1051
+
1052
+ UActorComponent *TargetComponent = nullptr;
1053
+ for (UActorComponent *Comp : Found->GetComponents()) {
1054
+ if (!Comp)
1055
+ continue;
1056
+ if (Comp->GetName().Equals(ComponentName, ESearchCase::IgnoreCase)) {
1057
+ TargetComponent = Comp;
1058
+ break;
1059
+ }
1060
+ }
1061
+
1062
+ if (!TargetComponent) {
1063
+ SendAutomationResponse(Socket, RequestId, false,
1064
+ TEXT("Component not found"), nullptr,
1065
+ TEXT("COMPONENT_NOT_FOUND"));
1066
+ return true;
1067
+ }
1068
+
1069
+ TArray<FString> AppliedProperties;
1070
+ TArray<FString> PropertyWarnings;
1071
+ UClass *ComponentClass = TargetComponent->GetClass();
1072
+ TargetComponent->Modify();
1073
+
1074
+ // PRIORITY: Apply Mobility FIRST.
1075
+ // Physics simulation fails if the component is generic "Static".
1076
+ // Scan for Mobility key case-insensitively to ensure we find it regardless of
1077
+ // JSON casing
1078
+ const TSharedPtr<FJsonValue> *MobilityVal = nullptr;
1079
+ FString MobilityKey;
1080
+ for (const auto &Pair : (*PropertiesPtr)->Values) {
1081
+ if (Pair.Key.Equals(TEXT("Mobility"), ESearchCase::IgnoreCase)) {
1082
+ MobilityVal = &Pair.Value;
1083
+ MobilityKey = Pair.Key;
1084
+ break;
1085
+ }
1086
+ }
1087
+
1088
+ if (MobilityVal) {
1089
+ if (USceneComponent *SC = Cast<USceneComponent>(TargetComponent)) {
1090
+ FString EnumVal;
1091
+ if ((*MobilityVal)->TryGetString(EnumVal)) {
1092
+ // Parse enum string
1093
+ int64 Val =
1094
+ StaticEnum<EComponentMobility::Type>()->GetValueByNameString(
1095
+ EnumVal);
1096
+ if (Val != INDEX_NONE) {
1097
+ SC->SetMobility((EComponentMobility::Type)Val);
1098
+ AppliedProperties.Add(MobilityKey);
1099
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1100
+ TEXT("Explicitly set Mobility to %s"), *EnumVal);
1101
+ }
1102
+ } else {
1103
+ double Val;
1104
+ if ((*MobilityVal)->TryGetNumber(Val)) {
1105
+ SC->SetMobility((EComponentMobility::Type)(int32)Val);
1106
+ AppliedProperties.Add(MobilityKey);
1107
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1108
+ TEXT("Explicitly set Mobility to %d"), (int32)Val);
1109
+ }
1110
+ }
1111
+ }
1112
+ }
1113
+
1114
+ for (const auto &Pair : (*PropertiesPtr)->Values) {
1115
+ // Skip Mobility as we already handled it
1116
+ if (Pair.Key.Equals(TEXT("Mobility"), ESearchCase::IgnoreCase))
1117
+ continue;
1118
+
1119
+ // Special handling for SimulatePhysics
1120
+ if (Pair.Key.Equals(TEXT("SimulatePhysics"), ESearchCase::IgnoreCase) ||
1121
+ Pair.Key.Equals(TEXT("bSimulatePhysics"), ESearchCase::IgnoreCase)) {
1122
+ if (UPrimitiveComponent *Prim =
1123
+ Cast<UPrimitiveComponent>(TargetComponent)) {
1124
+ bool bVal = false;
1125
+ if (Pair.Value->TryGetBool(bVal)) {
1126
+ Prim->SetSimulatePhysics(bVal);
1127
+ AppliedProperties.Add(Pair.Key);
1128
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1129
+ TEXT("Explicitly set SimulatePhysics to %s"),
1130
+ bVal ? TEXT("True") : TEXT("False"));
1131
+ continue;
1132
+ }
1133
+ }
1134
+ }
1135
+
1136
+ FProperty *Property = ComponentClass->FindPropertyByName(*Pair.Key);
1137
+ if (!Property) {
1138
+ PropertyWarnings.Add(
1139
+ FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
1140
+ continue;
1141
+ }
1142
+ FString ApplyError;
1143
+ if (ApplyJsonValueToProperty(TargetComponent, Property, Pair.Value,
1144
+ ApplyError))
1145
+ AppliedProperties.Add(Pair.Key);
1146
+ else
1147
+ PropertyWarnings.Add(FString::Printf(TEXT("Failed to set %s: %s"),
1148
+ *Pair.Key, *ApplyError));
1149
+ }
1150
+
1151
+ if (USceneComponent *SceneComponent =
1152
+ Cast<USceneComponent>(TargetComponent)) {
1153
+ SceneComponent->MarkRenderStateDirty();
1154
+ SceneComponent->UpdateComponentToWorld();
1155
+ }
1156
+ TargetComponent->MarkPackageDirty();
1157
+
1158
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1159
+ if (AppliedProperties.Num() > 0) {
1160
+ TArray<TSharedPtr<FJsonValue>> PropsArray;
1161
+ for (const FString &PropName : AppliedProperties)
1162
+ PropsArray.Add(MakeShared<FJsonValueString>(PropName));
1163
+ Data->SetArrayField(TEXT("applied"), PropsArray);
1164
+ }
1165
+
1166
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1167
+ TEXT("ControlActor: Updated properties for component '%s' on '%s'"),
1168
+ *TargetComponent->GetName(), *Found->GetActorLabel());
1169
+
1170
+ SendStandardSuccessResponse(this, Socket, RequestId,
1171
+ TEXT("Component properties updated"), Data,
1172
+ PropertyWarnings);
1173
+ return true;
1174
+ #else
1175
+ return false;
1176
+ #endif
1177
+ }
1178
+
1179
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorGetComponents(
1180
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1181
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1182
+ #if WITH_EDITOR
1183
+ FString TargetName;
1184
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1185
+
1186
+ // Also accept "objectPath" as an alias, common in inspections
1187
+ if (TargetName.IsEmpty()) {
1188
+ Payload->TryGetStringField(TEXT("objectPath"), TargetName);
1189
+ }
1190
+
1191
+ if (TargetName.IsEmpty()) {
1192
+ SendAutomationResponse(Socket, RequestId, false,
1193
+ TEXT("actorName or objectPath required"), nullptr,
1194
+ TEXT("INVALID_ARGUMENT"));
1195
+ return true;
1196
+ }
1197
+
1198
+ AActor *Found = FindActorByName(TargetName);
1199
+ // Fallback: Check if it's a Blueprint asset to inspect CDO components
1200
+ if (!Found) {
1201
+ if (UObject *Asset = UEditorAssetLibrary::LoadAsset(TargetName)) {
1202
+ if (UBlueprint *BP = Cast<UBlueprint>(Asset)) {
1203
+ if (BP->GeneratedClass) {
1204
+ Found = Cast<AActor>(BP->GeneratedClass->GetDefaultObject());
1205
+ }
1206
+ }
1207
+ }
1208
+ }
1209
+
1210
+ if (!Found) {
1211
+ SendAutomationResponse(Socket, RequestId, false,
1212
+ TEXT("Actor or Blueprint not found"), nullptr,
1213
+ TEXT("ACTOR_NOT_FOUND"));
1214
+ return true;
1215
+ }
1216
+
1217
+ TArray<TSharedPtr<FJsonValue>> ComponentsArray;
1218
+ for (UActorComponent *Comp : Found->GetComponents()) {
1219
+ if (!Comp)
1220
+ continue;
1221
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1222
+ Entry->SetStringField(TEXT("name"), Comp->GetName());
1223
+ Entry->SetStringField(TEXT("class"), Comp->GetClass()
1224
+ ? Comp->GetClass()->GetPathName()
1225
+ : TEXT(""));
1226
+ Entry->SetStringField(TEXT("path"), Comp->GetPathName());
1227
+ if (USceneComponent *SceneComp = Cast<USceneComponent>(Comp)) {
1228
+ FVector Loc = SceneComp->GetRelativeLocation();
1229
+ FRotator Rot = SceneComp->GetRelativeRotation();
1230
+ FVector Scale = SceneComp->GetRelativeScale3D();
1231
+
1232
+ TSharedPtr<FJsonObject> LocObj = MakeShared<FJsonObject>();
1233
+ LocObj->SetNumberField(TEXT("x"), Loc.X);
1234
+ LocObj->SetNumberField(TEXT("y"), Loc.Y);
1235
+ LocObj->SetNumberField(TEXT("z"), Loc.Z);
1236
+ Entry->SetObjectField(TEXT("relativeLocation"), LocObj);
1237
+
1238
+ TSharedPtr<FJsonObject> RotObj = MakeShared<FJsonObject>();
1239
+ RotObj->SetNumberField(TEXT("pitch"), Rot.Pitch);
1240
+ RotObj->SetNumberField(TEXT("yaw"), Rot.Yaw);
1241
+ RotObj->SetNumberField(TEXT("roll"), Rot.Roll);
1242
+ Entry->SetObjectField(TEXT("relativeRotation"), RotObj);
1243
+
1244
+ TSharedPtr<FJsonObject> ScaleObj = MakeShared<FJsonObject>();
1245
+ ScaleObj->SetNumberField(TEXT("x"), Scale.X);
1246
+ ScaleObj->SetNumberField(TEXT("y"), Scale.Y);
1247
+ ScaleObj->SetNumberField(TEXT("z"), Scale.Z);
1248
+ Entry->SetObjectField(TEXT("relativeScale"), ScaleObj);
1249
+ }
1250
+ ComponentsArray.Add(MakeShared<FJsonValueObject>(Entry));
1251
+ }
1252
+
1253
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1254
+ Data->SetArrayField(TEXT("components"), ComponentsArray);
1255
+ Data->SetNumberField(TEXT("count"), ComponentsArray.Num());
1256
+ SendAutomationResponse(Socket, RequestId, true,
1257
+ TEXT("Actor components retrieved"), Data);
1258
+ return true;
1259
+ #else
1260
+ return false;
1261
+ #endif
1262
+ }
1263
+
1264
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorDuplicate(
1265
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1266
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1267
+ #if WITH_EDITOR
1268
+ FString TargetName;
1269
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1270
+ if (TargetName.IsEmpty()) {
1271
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1272
+ nullptr, TEXT("INVALID_ARGUMENT"));
1273
+ return true;
1274
+ }
1275
+
1276
+ AActor *Found = FindActorByName(TargetName);
1277
+ if (!Found) {
1278
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1279
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1280
+ return true;
1281
+ }
1282
+
1283
+ FVector Offset =
1284
+ ExtractVectorField(Payload, TEXT("offset"), FVector::ZeroVector);
1285
+ UEditorActorSubsystem *ActorSS =
1286
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1287
+ AActor *Duplicated =
1288
+ ActorSS->DuplicateActor(Found, Found->GetWorld(), Offset);
1289
+ if (!Duplicated) {
1290
+ SendAutomationResponse(Socket, RequestId, false,
1291
+ TEXT("Failed to duplicate actor"), nullptr,
1292
+ TEXT("DUPLICATE_FAILED"));
1293
+ return true;
1294
+ }
1295
+
1296
+ FString NewName;
1297
+ Payload->TryGetStringField(TEXT("newName"), NewName);
1298
+ if (!NewName.TrimStartAndEnd().IsEmpty())
1299
+ Duplicated->SetActorLabel(NewName);
1300
+
1301
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1302
+ Data->SetStringField(TEXT("source"), Found->GetActorLabel());
1303
+ Data->SetStringField(TEXT("actorName"), Duplicated->GetActorLabel());
1304
+ Data->SetStringField(TEXT("actorPath"), Duplicated->GetPathName());
1305
+
1306
+ TArray<TSharedPtr<FJsonValue>> OffsetArray;
1307
+ OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.X));
1308
+ OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.Y));
1309
+ OffsetArray.Add(MakeShared<FJsonValueNumber>(Offset.Z));
1310
+ Data->SetArrayField(TEXT("offset"), OffsetArray);
1311
+
1312
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1313
+ TEXT("ControlActor: Duplicated '%s' to '%s'"), *Found->GetActorLabel(),
1314
+ *Duplicated->GetActorLabel());
1315
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor duplicated"),
1316
+ Data);
1317
+ return true;
1318
+ #else
1319
+ return false;
1320
+ #endif
1321
+ }
1322
+
1323
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorAttach(
1324
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1325
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1326
+ #if WITH_EDITOR
1327
+ FString ChildName;
1328
+ Payload->TryGetStringField(TEXT("childActor"), ChildName);
1329
+ FString ParentName;
1330
+ Payload->TryGetStringField(TEXT("parentActor"), ParentName);
1331
+ if (ChildName.IsEmpty() || ParentName.IsEmpty()) {
1332
+ SendAutomationResponse(Socket, RequestId, false,
1333
+ TEXT("childActor and parentActor required"), nullptr,
1334
+ TEXT("INVALID_ARGUMENT"));
1335
+ return true;
1336
+ }
1337
+
1338
+ AActor *Child = FindActorByName(ChildName);
1339
+ AActor *Parent = FindActorByName(ParentName);
1340
+ if (!Child || !Parent) {
1341
+ SendAutomationResponse(Socket, RequestId, false,
1342
+ TEXT("Child or parent actor not found"), nullptr,
1343
+ TEXT("ACTOR_NOT_FOUND"));
1344
+ return true;
1345
+ }
1346
+
1347
+ if (Child == Parent) {
1348
+ SendAutomationResponse(Socket, RequestId, false,
1349
+ TEXT("Cannot attach actor to itself"), nullptr,
1350
+ TEXT("CYCLE_DETECTED"));
1351
+ return true;
1352
+ }
1353
+
1354
+ USceneComponent *ChildRoot = Child->GetRootComponent();
1355
+ USceneComponent *ParentRoot = Parent->GetRootComponent();
1356
+ if (!ChildRoot || !ParentRoot) {
1357
+ SendAutomationResponse(Socket, RequestId, false,
1358
+ TEXT("Actor missing root component"), nullptr,
1359
+ TEXT("ROOT_MISSING"));
1360
+ return true;
1361
+ }
1362
+
1363
+ Child->Modify();
1364
+ ChildRoot->Modify();
1365
+ ChildRoot->AttachToComponent(ParentRoot,
1366
+ FAttachmentTransformRules::KeepWorldTransform);
1367
+ Child->SetOwner(Parent);
1368
+ Child->MarkPackageDirty();
1369
+ Parent->MarkPackageDirty();
1370
+
1371
+ // Verify attachment
1372
+ bool bAttached = false;
1373
+ if (Child->GetRootComponent() &&
1374
+ Child->GetRootComponent()->GetAttachParent() == ParentRoot) {
1375
+ bAttached = true;
1376
+ }
1377
+
1378
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1379
+ Data->SetStringField(TEXT("child"), Child->GetActorLabel());
1380
+ Data->SetStringField(TEXT("parent"), Parent->GetActorLabel());
1381
+ Data->SetBoolField(TEXT("attached"), bAttached);
1382
+
1383
+ if (!bAttached) {
1384
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("ATTACH_FAILED"),
1385
+ TEXT("Failed to attach actor"), Data);
1386
+ return true;
1387
+ }
1388
+
1389
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1390
+ TEXT("ControlActor: Attached '%s' to '%s'"), *Child->GetActorLabel(),
1391
+ *Parent->GetActorLabel());
1392
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor attached"),
1393
+ Data);
1394
+ return true;
1395
+ #else
1396
+ return false;
1397
+ #endif
1398
+ }
1399
+
1400
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorDetach(
1401
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1402
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1403
+ #if WITH_EDITOR
1404
+ FString TargetName;
1405
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1406
+ if (TargetName.IsEmpty()) {
1407
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1408
+ nullptr, TEXT("INVALID_ARGUMENT"));
1409
+ return true;
1410
+ }
1411
+
1412
+ AActor *Found = FindActorByName(TargetName);
1413
+ if (!Found) {
1414
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1415
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1416
+ return true;
1417
+ }
1418
+
1419
+ USceneComponent *RootComp = Found->GetRootComponent();
1420
+ if (!RootComp || !RootComp->GetAttachParent()) {
1421
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1422
+ Resp->SetBoolField(TEXT("success"), true);
1423
+ Resp->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1424
+ Resp->SetStringField(TEXT("note"), TEXT("Actor was not attached"));
1425
+ SendAutomationResponse(Socket, RequestId, true,
1426
+ TEXT("Actor already detached"), Resp, FString());
1427
+ return true;
1428
+ }
1429
+
1430
+ Found->Modify();
1431
+ RootComp->Modify();
1432
+ RootComp->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
1433
+ Found->SetOwner(nullptr);
1434
+ Found->MarkPackageDirty();
1435
+
1436
+ // Verify detachment
1437
+ const bool bDetached = (RootComp->GetAttachParent() == nullptr);
1438
+
1439
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1440
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1441
+ Data->SetBoolField(TEXT("detached"), bDetached);
1442
+
1443
+ if (!bDetached) {
1444
+ SendStandardErrorResponse(this, Socket, RequestId, TEXT("DETACH_FAILED"),
1445
+ TEXT("Failed to detach actor"), Data);
1446
+ return true;
1447
+ }
1448
+
1449
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1450
+ TEXT("ControlActor: Detached '%s'"), *Found->GetActorLabel());
1451
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor detached"),
1452
+ Data);
1453
+ return true;
1454
+ #else
1455
+ return false;
1456
+ #endif
1457
+ }
1458
+
1459
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorFindByTag(
1460
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1461
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1462
+ #if WITH_EDITOR
1463
+ FString TagValue;
1464
+ Payload->TryGetStringField(TEXT("tag"), TagValue);
1465
+ if (TagValue.IsEmpty()) {
1466
+ SendAutomationResponse(Socket, RequestId, false, TEXT("tag required"),
1467
+ nullptr, TEXT("INVALID_ARGUMENT"));
1468
+ return true;
1469
+ }
1470
+
1471
+ FString MatchType;
1472
+ Payload->TryGetStringField(TEXT("matchType"), MatchType);
1473
+ MatchType = MatchType.ToLower();
1474
+ FName TagName(*TagValue);
1475
+ TArray<TSharedPtr<FJsonValue>> Matches;
1476
+
1477
+ UEditorActorSubsystem *ActorSS =
1478
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1479
+ TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
1480
+ for (AActor *Actor : AllActors) {
1481
+ if (!Actor)
1482
+ continue;
1483
+ bool bMatches = false;
1484
+ if (MatchType == TEXT("contains")) {
1485
+ for (const FName &Existing : Actor->Tags) {
1486
+ if (Existing.ToString().Contains(TagValue, ESearchCase::IgnoreCase)) {
1487
+ bMatches = true;
1488
+ break;
1489
+ }
1490
+ }
1491
+ } else {
1492
+ bMatches = Actor->ActorHasTag(TagName);
1493
+ }
1494
+
1495
+ if (bMatches) {
1496
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1497
+ Entry->SetStringField(TEXT("name"), Actor->GetActorLabel());
1498
+ Entry->SetStringField(TEXT("path"), Actor->GetPathName());
1499
+ Entry->SetStringField(TEXT("class"),
1500
+ Actor->GetClass() ? Actor->GetClass()->GetPathName()
1501
+ : TEXT(""));
1502
+ Matches.Add(MakeShared<FJsonValueObject>(Entry));
1503
+ }
1504
+ }
1505
+
1506
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1507
+ Data->SetArrayField(TEXT("actors"), Matches);
1508
+ Data->SetNumberField(TEXT("count"), Matches.Num());
1509
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actors found"),
1510
+ Data);
1511
+ return true;
1512
+ #else
1513
+ return false;
1514
+ #endif
1515
+ }
1516
+
1517
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorAddTag(
1518
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1519
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1520
+ #if WITH_EDITOR
1521
+ FString TargetName;
1522
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1523
+ FString TagValue;
1524
+ Payload->TryGetStringField(TEXT("tag"), TagValue);
1525
+ if (TargetName.IsEmpty() || TagValue.IsEmpty()) {
1526
+ SendAutomationResponse(Socket, RequestId, false,
1527
+ TEXT("actorName and tag required"), nullptr,
1528
+ TEXT("INVALID_ARGUMENT"));
1529
+ return true;
1530
+ }
1531
+
1532
+ AActor *Found = FindActorByName(TargetName);
1533
+ if (!Found) {
1534
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1535
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1536
+ return true;
1537
+ }
1538
+
1539
+ const FName TagName(*TagValue);
1540
+ const bool bAlreadyHad = Found->Tags.Contains(TagName);
1541
+
1542
+ Found->Modify();
1543
+ Found->Tags.AddUnique(TagName);
1544
+ Found->MarkPackageDirty();
1545
+
1546
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1547
+ Data->SetBoolField(TEXT("wasPresent"), bAlreadyHad);
1548
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1549
+ Data->SetStringField(TEXT("tag"), TagName.ToString());
1550
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1551
+ TEXT("ControlActor: Added tag '%s' to '%s'"), *TagName.ToString(),
1552
+ *Found->GetActorLabel());
1553
+ SendStandardSuccessResponse(this, Socket, RequestId,
1554
+ TEXT("Tag applied to actor"), Data);
1555
+ return true;
1556
+ #else
1557
+ return false;
1558
+ #endif
1559
+ }
1560
+
1561
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorFindByName(
1562
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1563
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1564
+ #if WITH_EDITOR
1565
+ FString Query;
1566
+ Payload->TryGetStringField(TEXT("name"), Query);
1567
+ if (Query.IsEmpty()) {
1568
+ SendAutomationResponse(Socket, RequestId, false, TEXT("name required"),
1569
+ nullptr, TEXT("INVALID_ARGUMENT"));
1570
+ return true;
1571
+ }
1572
+
1573
+ UEditorActorSubsystem *ActorSS =
1574
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1575
+ const TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
1576
+ TArray<TSharedPtr<FJsonValue>> Matches;
1577
+ for (AActor *Actor : AllActors) {
1578
+ if (!Actor)
1579
+ continue;
1580
+ const FString Label = Actor->GetActorLabel();
1581
+ const FString Name = Actor->GetName();
1582
+ const FString Path = Actor->GetPathName();
1583
+ const bool bMatches = Label.Contains(Query, ESearchCase::IgnoreCase) ||
1584
+ Name.Contains(Query, ESearchCase::IgnoreCase) ||
1585
+ Path.Contains(Query, ESearchCase::IgnoreCase);
1586
+ if (bMatches) {
1587
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
1588
+ Entry->SetStringField(TEXT("label"), Label);
1589
+ Entry->SetStringField(TEXT("name"), Name);
1590
+ Entry->SetStringField(TEXT("path"), Path);
1591
+ Entry->SetStringField(TEXT("class"),
1592
+ Actor->GetClass() ? Actor->GetClass()->GetPathName()
1593
+ : TEXT(""));
1594
+ Matches.Add(MakeShared<FJsonValueObject>(Entry));
1595
+ }
1596
+ }
1597
+
1598
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1599
+ Data->SetNumberField(TEXT("count"), Matches.Num());
1600
+ Data->SetArrayField(TEXT("actors"), Matches);
1601
+ Data->SetStringField(TEXT("query"), Query);
1602
+ SendStandardSuccessResponse(this, Socket, RequestId,
1603
+ TEXT("Actor query executed"), Data);
1604
+ return true;
1605
+ #else
1606
+ return false;
1607
+ #endif
1608
+ }
1609
+
1610
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorDeleteByTag(
1611
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1612
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1613
+ #if WITH_EDITOR
1614
+ FString TagValue;
1615
+ Payload->TryGetStringField(TEXT("tag"), TagValue);
1616
+ if (TagValue.IsEmpty()) {
1617
+ SendAutomationResponse(Socket, RequestId, false, TEXT("tag required"),
1618
+ nullptr, TEXT("INVALID_ARGUMENT"));
1619
+ return true;
1620
+ }
1621
+
1622
+ const FName TagName(*TagValue);
1623
+ UEditorActorSubsystem *ActorSS =
1624
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
1625
+ const TArray<AActor *> AllActors = ActorSS->GetAllLevelActors();
1626
+ TArray<FString> Deleted;
1627
+
1628
+ for (AActor *Actor : AllActors) {
1629
+ if (!Actor)
1630
+ continue;
1631
+ if (Actor->ActorHasTag(TagName)) {
1632
+ const FString Label = Actor->GetActorLabel();
1633
+ if (ActorSS->DestroyActor(Actor))
1634
+ Deleted.Add(Label);
1635
+ }
1636
+ }
1637
+
1638
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1639
+ Data->SetStringField(TEXT("tag"), TagName.ToString());
1640
+ Data->SetNumberField(TEXT("deletedCount"), Deleted.Num());
1641
+ TArray<TSharedPtr<FJsonValue>> DeletedArray;
1642
+ for (const FString &Name : Deleted)
1643
+ DeletedArray.Add(MakeShared<FJsonValueString>(Name));
1644
+ Data->SetArrayField(TEXT("deleted"), DeletedArray);
1645
+ SendStandardSuccessResponse(this, Socket, RequestId,
1646
+ TEXT("Actors deleted by tag"), Data);
1647
+ return true;
1648
+ #else
1649
+ return false;
1650
+ #endif
1651
+ }
1652
+
1653
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorSetBlueprintVariables(
1654
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1655
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1656
+ #if WITH_EDITOR
1657
+ FString TargetName;
1658
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1659
+ if (TargetName.IsEmpty()) {
1660
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1661
+ nullptr, TEXT("INVALID_ARGUMENT"));
1662
+ return true;
1663
+ }
1664
+
1665
+ const TSharedPtr<FJsonObject> *VariablesPtr = nullptr;
1666
+ if (!(Payload->TryGetObjectField(TEXT("variables"), VariablesPtr) &&
1667
+ VariablesPtr && VariablesPtr->IsValid())) {
1668
+ SendAutomationResponse(Socket, RequestId, false,
1669
+ TEXT("variables object required"), nullptr,
1670
+ TEXT("INVALID_ARGUMENT"));
1671
+ return true;
1672
+ }
1673
+
1674
+ AActor *Found = FindActorByName(TargetName);
1675
+ if (!Found) {
1676
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1677
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1678
+ return true;
1679
+ }
1680
+
1681
+ UClass *ActorClass = Found->GetClass();
1682
+ Found->Modify();
1683
+ TArray<FString> Applied;
1684
+ TArray<FString> Warnings;
1685
+
1686
+ for (const auto &Pair : (*VariablesPtr)->Values) {
1687
+ FProperty *Property = ActorClass->FindPropertyByName(*Pair.Key);
1688
+ if (!Property) {
1689
+ Warnings.Add(FString::Printf(TEXT("Property not found: %s"), *Pair.Key));
1690
+ continue;
1691
+ }
1692
+
1693
+ FString ApplyError;
1694
+ if (ApplyJsonValueToProperty(Found, Property, Pair.Value, ApplyError))
1695
+ Applied.Add(Pair.Key);
1696
+ else
1697
+ Warnings.Add(FString::Printf(TEXT("Failed to set %s: %s"), *Pair.Key,
1698
+ *ApplyError));
1699
+ }
1700
+
1701
+ Found->MarkComponentsRenderStateDirty();
1702
+ Found->MarkPackageDirty();
1703
+
1704
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1705
+ if (Applied.Num() > 0) {
1706
+ TArray<TSharedPtr<FJsonValue>> AppliedArray;
1707
+ for (const FString &Name : Applied)
1708
+ AppliedArray.Add(MakeShared<FJsonValueString>(Name));
1709
+ Data->SetArrayField(TEXT("updated"), AppliedArray);
1710
+ }
1711
+
1712
+ SendStandardSuccessResponse(this, Socket, RequestId,
1713
+ TEXT("Variables updated"), Data, Warnings);
1714
+ return true;
1715
+ #else
1716
+ return false;
1717
+ #endif
1718
+ }
1719
+
1720
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorCreateSnapshot(
1721
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1722
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1723
+ #if WITH_EDITOR
1724
+ FString TargetName;
1725
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1726
+ if (TargetName.IsEmpty()) {
1727
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1728
+ nullptr, TEXT("INVALID_ARGUMENT"));
1729
+ return true;
1730
+ }
1731
+
1732
+ FString SnapshotName;
1733
+ Payload->TryGetStringField(TEXT("snapshotName"), SnapshotName);
1734
+ if (SnapshotName.IsEmpty()) {
1735
+ SendAutomationResponse(Socket, RequestId, false,
1736
+ TEXT("snapshotName required"), nullptr,
1737
+ TEXT("INVALID_ARGUMENT"));
1738
+ return true;
1739
+ }
1740
+
1741
+ AActor *Found = FindActorByName(TargetName);
1742
+ if (!Found) {
1743
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1744
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1745
+ return true;
1746
+ }
1747
+
1748
+ const FString SnapshotKey =
1749
+ FString::Printf(TEXT("%s::%s"), *Found->GetPathName(), *SnapshotName);
1750
+ CachedActorSnapshots.Add(SnapshotKey, Found->GetActorTransform());
1751
+
1752
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1753
+ Data->SetStringField(TEXT("snapshotName"), SnapshotName);
1754
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1755
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Snapshot created"),
1756
+ Data);
1757
+ return true;
1758
+ #else
1759
+ return false;
1760
+ #endif
1761
+ }
1762
+
1763
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorRestoreSnapshot(
1764
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1765
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1766
+ #if WITH_EDITOR
1767
+ FString TargetName;
1768
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1769
+ if (TargetName.IsEmpty()) {
1770
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1771
+ nullptr, TEXT("INVALID_ARGUMENT"));
1772
+ return true;
1773
+ }
1774
+
1775
+ FString SnapshotName;
1776
+ Payload->TryGetStringField(TEXT("snapshotName"), SnapshotName);
1777
+ if (SnapshotName.IsEmpty()) {
1778
+ SendAutomationResponse(Socket, RequestId, false,
1779
+ TEXT("snapshotName required"), nullptr,
1780
+ TEXT("INVALID_ARGUMENT"));
1781
+ return true;
1782
+ }
1783
+
1784
+ AActor *Found = FindActorByName(TargetName);
1785
+ if (!Found) {
1786
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1787
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1788
+ return true;
1789
+ }
1790
+
1791
+ const FString SnapshotKey =
1792
+ FString::Printf(TEXT("%s::%s"), *Found->GetPathName(), *SnapshotName);
1793
+ if (!CachedActorSnapshots.Contains(SnapshotKey)) {
1794
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Snapshot not found"),
1795
+ nullptr, TEXT("SNAPSHOT_NOT_FOUND"));
1796
+ return true;
1797
+ }
1798
+
1799
+ const FTransform &SavedTransform = CachedActorSnapshots[SnapshotKey];
1800
+ Found->Modify();
1801
+ Found->SetActorTransform(SavedTransform);
1802
+ Found->MarkComponentsRenderStateDirty();
1803
+ Found->MarkPackageDirty();
1804
+
1805
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1806
+ Data->SetStringField(TEXT("snapshotName"), SnapshotName);
1807
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1808
+ SendStandardSuccessResponse(this, Socket, RequestId,
1809
+ TEXT("Snapshot restored"), Data);
1810
+ return true;
1811
+ #else
1812
+ return false;
1813
+ #endif
1814
+ }
1815
+
1816
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorExport(
1817
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1818
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1819
+ #if WITH_EDITOR
1820
+ FString TargetName;
1821
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1822
+ if (TargetName.IsEmpty()) {
1823
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1824
+ nullptr, TEXT("INVALID_ARGUMENT"));
1825
+ return true;
1826
+ }
1827
+
1828
+ AActor *Found = FindActorByName(TargetName);
1829
+ if (!Found) {
1830
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1831
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1832
+ return true;
1833
+ }
1834
+
1835
+ FMcpOutputCapture OutputCapture;
1836
+ UExporter::ExportToOutputDevice(nullptr, Found, nullptr, OutputCapture,
1837
+ TEXT("T3D"), 0, 0, false);
1838
+ FString OutputString = FString::Join(OutputCapture.Consume(), TEXT("\n"));
1839
+
1840
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1841
+ Data->SetStringField(TEXT("t3d"), OutputString);
1842
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1843
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor exported"),
1844
+ Data);
1845
+ return true;
1846
+ #else
1847
+ return false;
1848
+ #endif
1849
+ }
1850
+
1851
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorGetBoundingBox(
1852
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1853
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1854
+ #if WITH_EDITOR
1855
+ FString TargetName;
1856
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1857
+ if (TargetName.IsEmpty()) {
1858
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1859
+ nullptr, TEXT("INVALID_ARGUMENT"));
1860
+ return true;
1861
+ }
1862
+
1863
+ AActor *Found = FindActorByName(TargetName);
1864
+ if (!Found) {
1865
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1866
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1867
+ return true;
1868
+ }
1869
+
1870
+ FVector Origin, BoxExtent;
1871
+ Found->GetActorBounds(false, Origin, BoxExtent);
1872
+
1873
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1874
+
1875
+ auto MakeArray = [](const FVector &Vec) {
1876
+ TArray<TSharedPtr<FJsonValue>> Arr;
1877
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
1878
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
1879
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
1880
+ return Arr;
1881
+ };
1882
+
1883
+ Data->SetArrayField(TEXT("origin"), MakeArray(Origin));
1884
+ Data->SetArrayField(TEXT("extent"), MakeArray(BoxExtent));
1885
+ SendStandardSuccessResponse(this, Socket, RequestId,
1886
+ TEXT("Bounding box retrieved"), Data);
1887
+ return true;
1888
+ #else
1889
+ return false;
1890
+ #endif
1891
+ }
1892
+
1893
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorGetMetadata(
1894
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1895
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1896
+ #if WITH_EDITOR
1897
+ FString TargetName;
1898
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1899
+ if (TargetName.IsEmpty()) {
1900
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
1901
+ nullptr, TEXT("INVALID_ARGUMENT"));
1902
+ return true;
1903
+ }
1904
+
1905
+ AActor *Found = FindActorByName(TargetName);
1906
+ if (!Found) {
1907
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1908
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1909
+ return true;
1910
+ }
1911
+
1912
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1913
+ Data->SetStringField(TEXT("name"), Found->GetName());
1914
+ Data->SetStringField(TEXT("label"), Found->GetActorLabel());
1915
+ Data->SetStringField(TEXT("path"), Found->GetPathName());
1916
+ Data->SetStringField(TEXT("class"), Found->GetClass()
1917
+ ? Found->GetClass()->GetPathName()
1918
+ : TEXT(""));
1919
+
1920
+ TArray<TSharedPtr<FJsonValue>> TagsArray;
1921
+ for (const FName &Tag : Found->Tags) {
1922
+ TagsArray.Add(MakeShared<FJsonValueString>(Tag.ToString()));
1923
+ }
1924
+ Data->SetArrayField(TEXT("tags"), TagsArray);
1925
+
1926
+ const FTransform Current = Found->GetActorTransform();
1927
+ auto MakeArray = [](const FVector &Vec) {
1928
+ TArray<TSharedPtr<FJsonValue>> Arr;
1929
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
1930
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
1931
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
1932
+ return Arr;
1933
+ };
1934
+ Data->SetArrayField(TEXT("location"), MakeArray(Current.GetLocation()));
1935
+
1936
+ SendStandardSuccessResponse(this, Socket, RequestId,
1937
+ TEXT("Metadata retrieved"), Data);
1938
+ return true;
1939
+ #else
1940
+ return false;
1941
+ #endif
1942
+ }
1943
+
1944
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorRemoveTag(
1945
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
1946
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
1947
+ #if WITH_EDITOR
1948
+ FString TargetName;
1949
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
1950
+ FString TagValue;
1951
+ Payload->TryGetStringField(TEXT("tag"), TagValue);
1952
+ if (TargetName.IsEmpty() || TagValue.IsEmpty()) {
1953
+ SendAutomationResponse(Socket, RequestId, false,
1954
+ TEXT("actorName and tag required"), nullptr,
1955
+ TEXT("INVALID_ARGUMENT"));
1956
+ return true;
1957
+ }
1958
+
1959
+ AActor *Found = FindActorByName(TargetName);
1960
+ if (!Found) {
1961
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
1962
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
1963
+ return true;
1964
+ }
1965
+
1966
+ const FName TagName(*TagValue);
1967
+ if (!Found->Tags.Contains(TagName)) {
1968
+ // Idempotent success
1969
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
1970
+ Resp->SetBoolField(TEXT("success"), true);
1971
+ Resp->SetBoolField(TEXT("wasPresent"), false);
1972
+ Resp->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1973
+ Resp->SetStringField(TEXT("tag"), TagValue);
1974
+ SendAutomationResponse(Socket, RequestId, true,
1975
+ TEXT("Tag not present (idempotent)"), Resp,
1976
+ FString());
1977
+ return true;
1978
+ }
1979
+
1980
+ Found->Modify();
1981
+ Found->Tags.Remove(TagName);
1982
+ Found->MarkPackageDirty();
1983
+
1984
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
1985
+ Data->SetBoolField(TEXT("wasPresent"), true);
1986
+ Data->SetStringField(TEXT("actorName"), Found->GetActorLabel());
1987
+ Data->SetStringField(TEXT("tag"), TagValue);
1988
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
1989
+ TEXT("ControlActor: Removed tag '%s' from '%s'"), *TagValue,
1990
+ *Found->GetActorLabel());
1991
+ SendStandardSuccessResponse(this, Socket, RequestId,
1992
+ TEXT("Tag removed from actor"), Data);
1993
+ return true;
1994
+ #else
1995
+ return false;
1996
+ #endif
1997
+ }
1998
+
1999
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorAction(
2000
+ const FString &RequestId, const FString &Action,
2001
+ const TSharedPtr<FJsonObject> &Payload,
2002
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
2003
+ const FString Lower = Action.ToLower();
2004
+ if (!Lower.Equals(TEXT("control_actor"), ESearchCase::IgnoreCase) &&
2005
+ !Lower.StartsWith(TEXT("control_actor")))
2006
+ return false;
2007
+ if (!Payload.IsValid()) {
2008
+ SendAutomationError(RequestingSocket, RequestId,
2009
+ TEXT("control_actor payload missing."),
2010
+ TEXT("INVALID_PAYLOAD"));
2011
+ return true;
2012
+ }
2013
+
2014
+ FString SubAction;
2015
+ Payload->TryGetStringField(TEXT("action"), SubAction);
2016
+ const FString LowerSub = SubAction.ToLower();
2017
+
2018
+ UE_LOG(LogMcpAutomationBridgeSubsystem, Display,
2019
+ TEXT("HandleControlActorAction: %s RequestId=%s"), *LowerSub,
2020
+ *RequestId);
2021
+
2022
+ #if WITH_EDITOR
2023
+ if (!GEditor) {
2024
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2025
+ TEXT("Editor not available"), nullptr,
2026
+ TEXT("EDITOR_NOT_AVAILABLE"));
2027
+ return true;
2028
+ }
2029
+ if (!GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
2030
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2031
+ TEXT("EditorActorSubsystem not available"), nullptr,
2032
+ TEXT("EDITOR_ACTOR_SUBSYSTEM_MISSING"));
2033
+ return true;
2034
+ }
2035
+
2036
+ if (LowerSub == TEXT("spawn"))
2037
+ return HandleControlActorSpawn(RequestId, Payload, RequestingSocket);
2038
+ if (LowerSub == TEXT("spawn_blueprint"))
2039
+ return HandleControlActorSpawnBlueprint(RequestId, Payload,
2040
+ RequestingSocket);
2041
+ if (LowerSub == TEXT("delete") || LowerSub == TEXT("remove"))
2042
+ return HandleControlActorDelete(RequestId, Payload, RequestingSocket);
2043
+ if (LowerSub == TEXT("apply_force") ||
2044
+ LowerSub == TEXT("apply_force_to_actor"))
2045
+ return HandleControlActorApplyForce(RequestId, Payload, RequestingSocket);
2046
+ if (LowerSub == TEXT("set_transform") ||
2047
+ LowerSub == TEXT("set_actor_transform"))
2048
+ return HandleControlActorSetTransform(RequestId, Payload, RequestingSocket);
2049
+ if (LowerSub == TEXT("get_transform") ||
2050
+ LowerSub == TEXT("get_actor_transform"))
2051
+ return HandleControlActorGetTransform(RequestId, Payload, RequestingSocket);
2052
+ if (LowerSub == TEXT("set_visibility") ||
2053
+ LowerSub == TEXT("set_actor_visibility"))
2054
+ return HandleControlActorSetVisibility(RequestId, Payload,
2055
+ RequestingSocket);
2056
+ if (LowerSub == TEXT("add_component"))
2057
+ return HandleControlActorAddComponent(RequestId, Payload, RequestingSocket);
2058
+ if (LowerSub == TEXT("set_component_properties"))
2059
+ return HandleControlActorSetComponentProperties(RequestId, Payload,
2060
+ RequestingSocket);
2061
+ if (LowerSub == TEXT("get_components"))
2062
+ return HandleControlActorGetComponents(RequestId, Payload,
2063
+ RequestingSocket);
2064
+ if (LowerSub == TEXT("duplicate"))
2065
+ return HandleControlActorDuplicate(RequestId, Payload, RequestingSocket);
2066
+ if (LowerSub == TEXT("attach"))
2067
+ return HandleControlActorAttach(RequestId, Payload, RequestingSocket);
2068
+ if (LowerSub == TEXT("detach"))
2069
+ return HandleControlActorDetach(RequestId, Payload, RequestingSocket);
2070
+ if (LowerSub == TEXT("find_by_tag"))
2071
+ return HandleControlActorFindByTag(RequestId, Payload, RequestingSocket);
2072
+ if (LowerSub == TEXT("add_tag"))
2073
+ return HandleControlActorAddTag(RequestId, Payload, RequestingSocket);
2074
+ if (LowerSub == TEXT("remove_tag"))
2075
+ return HandleControlActorRemoveTag(RequestId, Payload, RequestingSocket);
2076
+ if (LowerSub == TEXT("find_by_name"))
2077
+ return HandleControlActorFindByName(RequestId, Payload, RequestingSocket);
2078
+ if (LowerSub == TEXT("delete_by_tag"))
2079
+ return HandleControlActorDeleteByTag(RequestId, Payload, RequestingSocket);
2080
+ if (LowerSub == TEXT("set_blueprint_variables"))
2081
+ return HandleControlActorSetBlueprintVariables(RequestId, Payload,
2082
+ RequestingSocket);
2083
+ if (LowerSub == TEXT("create_snapshot"))
2084
+ return HandleControlActorCreateSnapshot(RequestId, Payload,
2085
+ RequestingSocket);
2086
+ if (LowerSub == TEXT("restore_snapshot"))
2087
+ return HandleControlActorRestoreSnapshot(RequestId, Payload,
2088
+ RequestingSocket);
2089
+ if (LowerSub == TEXT("export"))
2090
+ return HandleControlActorExport(RequestId, Payload, RequestingSocket);
2091
+ if (LowerSub == TEXT("get_bounding_box"))
2092
+ return HandleControlActorGetBoundingBox(RequestId, Payload,
2093
+ RequestingSocket);
2094
+ if (LowerSub == TEXT("get_metadata"))
2095
+ return HandleControlActorGetMetadata(RequestId, Payload, RequestingSocket);
2096
+ if (LowerSub == TEXT("list") || LowerSub == TEXT("list_actors"))
2097
+ return HandleControlActorList(RequestId, Payload, RequestingSocket);
2098
+ if (LowerSub == TEXT("get") || LowerSub == TEXT("get_actor") ||
2099
+ LowerSub == TEXT("get_actor_by_name"))
2100
+ return HandleControlActorGet(RequestId, Payload, RequestingSocket);
2101
+
2102
+ SendAutomationResponse(
2103
+ RequestingSocket, RequestId, false,
2104
+ FString::Printf(TEXT("Unknown actor control action: %s"), *LowerSub),
2105
+ nullptr, TEXT("UNKNOWN_ACTION"));
2106
+ return true;
2107
+ #else
2108
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2109
+ TEXT("Actor control requires editor build."), nullptr,
2110
+ TEXT("NOT_IMPLEMENTED"));
2111
+ return true;
2112
+ #endif
2113
+ }
2114
+
2115
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorPlay(
2116
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2117
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2118
+ #if WITH_EDITOR
2119
+ if (GEditor->PlayWorld) {
2120
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2121
+ Resp->SetBoolField(TEXT("success"), true);
2122
+ Resp->SetBoolField(TEXT("alreadyPlaying"), true);
2123
+ SendAutomationResponse(Socket, RequestId, true,
2124
+ TEXT("Play session already active"), Resp,
2125
+ FString());
2126
+ return true;
2127
+ }
2128
+
2129
+ FRequestPlaySessionParams PlayParams;
2130
+ PlayParams.WorldType = EPlaySessionWorldType::PlayInEditor;
2131
+ #if MCP_HAS_LEVEL_EDITOR_PLAY_SETTINGS
2132
+ PlayParams.EditorPlaySettings = GetMutableDefault<ULevelEditorPlaySettings>();
2133
+ #endif
2134
+ #if MCP_HAS_LEVEL_EDITOR_MODULE
2135
+ if (FLevelEditorModule *LevelEditorModule =
2136
+ FModuleManager::GetModulePtr<FLevelEditorModule>(
2137
+ TEXT("LevelEditor"))) {
2138
+ TSharedPtr<IAssetViewport> DestinationViewport =
2139
+ LevelEditorModule->GetFirstActiveViewport();
2140
+ if (DestinationViewport.IsValid())
2141
+ PlayParams.DestinationSlateViewport = DestinationViewport;
2142
+ }
2143
+ #endif
2144
+
2145
+ GEditor->RequestPlaySession(PlayParams);
2146
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2147
+ Resp->SetBoolField(TEXT("success"), true);
2148
+ SendAutomationResponse(Socket, RequestId, true,
2149
+ TEXT("Play in Editor started"), Resp, FString());
2150
+ return true;
2151
+ #else
2152
+ return false;
2153
+ #endif
2154
+ }
2155
+
2156
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorStop(
2157
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2158
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2159
+ #if WITH_EDITOR
2160
+ if (!GEditor->PlayWorld) {
2161
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2162
+ Resp->SetBoolField(TEXT("success"), true);
2163
+ Resp->SetBoolField(TEXT("alreadyStopped"), true);
2164
+ SendAutomationResponse(Socket, RequestId, true,
2165
+ TEXT("Play session not active"), Resp, FString());
2166
+ return true;
2167
+ }
2168
+
2169
+ GEditor->RequestEndPlayMap();
2170
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2171
+ Resp->SetBoolField(TEXT("success"), true);
2172
+ SendAutomationResponse(Socket, RequestId, true,
2173
+ TEXT("Play in Editor stopped"), Resp, FString());
2174
+ return true;
2175
+ #else
2176
+ return false;
2177
+ #endif
2178
+ }
2179
+
2180
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorEject(
2181
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2182
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2183
+ #if WITH_EDITOR
2184
+ if (!GEditor->PlayWorld) {
2185
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2186
+ Resp->SetBoolField(TEXT("success"), true);
2187
+ Resp->SetBoolField(TEXT("alreadyStopped"), true);
2188
+ SendAutomationResponse(Socket, RequestId, true,
2189
+ TEXT("Play session not active"), Resp, FString());
2190
+ return true;
2191
+ }
2192
+
2193
+ GEditor->RequestEndPlayMap();
2194
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2195
+ Resp->SetBoolField(TEXT("success"), true);
2196
+ SendAutomationResponse(Socket, RequestId, true,
2197
+ TEXT("Play in Editor ejected"), Resp, FString());
2198
+ return true;
2199
+ #else
2200
+ return false;
2201
+ #endif
2202
+ }
2203
+
2204
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorPossess(
2205
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2206
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2207
+ #if WITH_EDITOR
2208
+ FString ActorName;
2209
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
2210
+
2211
+ // Also try "objectPath" as fallback since schema might use that
2212
+ if (ActorName.IsEmpty())
2213
+ Payload->TryGetStringField(TEXT("objectPath"), ActorName);
2214
+
2215
+ if (ActorName.IsEmpty()) {
2216
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
2217
+ nullptr, TEXT("INVALID_ARGUMENT"));
2218
+ return true;
2219
+ }
2220
+
2221
+ AActor *Found = FindActorByName(ActorName);
2222
+ if (!Found) {
2223
+ SendAutomationResponse(
2224
+ Socket, RequestId, false,
2225
+ FString::Printf(TEXT("Actor not found: %s"), *ActorName), nullptr,
2226
+ TEXT("ACTOR_NOT_FOUND"));
2227
+ return true;
2228
+ }
2229
+
2230
+ if (GEditor) {
2231
+ GEditor->SelectNone(true, true, false);
2232
+ GEditor->SelectActor(Found, true, true, true);
2233
+ // 'POSSESS' command works on selected actor in PIE
2234
+ if (GEditor->PlayWorld) {
2235
+ GEditor->Exec(GEditor->PlayWorld, TEXT("POSSESS"));
2236
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Possessed actor"),
2237
+ nullptr);
2238
+ } else {
2239
+ // If not in PIE, we can't possess
2240
+ SendAutomationResponse(Socket, RequestId, false,
2241
+ TEXT("Cannot possess actor while not in PIE"),
2242
+ nullptr, TEXT("NOT_IN_PIE"));
2243
+ }
2244
+ return true;
2245
+ }
2246
+
2247
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Editor not available"),
2248
+ nullptr, TEXT("EDITOR_NOT_AVAILABLE"));
2249
+ return true;
2250
+ #else
2251
+ return false;
2252
+ #endif
2253
+ }
2254
+
2255
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorFocusActor(
2256
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2257
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2258
+ #if WITH_EDITOR
2259
+ FString ActorName;
2260
+ Payload->TryGetStringField(TEXT("actorName"), ActorName);
2261
+ if (ActorName.IsEmpty()) {
2262
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
2263
+ nullptr, TEXT("INVALID_ARGUMENT"));
2264
+ return true;
2265
+ }
2266
+
2267
+ if (UEditorActorSubsystem *ActorSS =
2268
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>()) {
2269
+ TArray<AActor *> Actors = ActorSS->GetAllLevelActors();
2270
+ for (AActor *Actor : Actors) {
2271
+ if (!Actor)
2272
+ continue;
2273
+ if (Actor->GetActorLabel().Equals(ActorName, ESearchCase::IgnoreCase)) {
2274
+ GEditor->SelectNone(true, true, false);
2275
+ GEditor->SelectActor(Actor, true, true, true);
2276
+ GEditor->Exec(nullptr, TEXT("EDITORTEMPVIEWPORT"));
2277
+ GEditor->MoveViewportCamerasToActor(*Actor, false);
2278
+ SendAutomationResponse(Socket, RequestId, true,
2279
+ TEXT("Viewport focused on actor"), nullptr,
2280
+ FString());
2281
+ return true;
2282
+ }
2283
+ }
2284
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
2285
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
2286
+ return true;
2287
+ }
2288
+ return false;
2289
+ #else
2290
+ return false;
2291
+ #endif
2292
+ }
2293
+
2294
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorSetCamera(
2295
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2296
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2297
+ #if WITH_EDITOR
2298
+ const TSharedPtr<FJsonObject> *Loc = nullptr;
2299
+ FVector Location(0, 0, 0);
2300
+ FRotator Rotation(0, 0, 0);
2301
+ if (Payload->TryGetObjectField(TEXT("location"), Loc) && Loc &&
2302
+ (*Loc).IsValid())
2303
+ ReadVectorField(*Loc, TEXT(""), Location, Location);
2304
+ if (Payload->TryGetObjectField(TEXT("rotation"), Loc) && Loc &&
2305
+ (*Loc).IsValid())
2306
+ ReadRotatorField(*Loc, TEXT(""), Rotation, Rotation);
2307
+
2308
+ #if defined(MCP_HAS_UNREALEDITOR_SUBSYSTEM)
2309
+ if (UUnrealEditorSubsystem *UES =
2310
+ GEditor->GetEditorSubsystem<UUnrealEditorSubsystem>()) {
2311
+ UES->SetLevelViewportCameraInfo(Location, Rotation);
2312
+ #if defined(MCP_HAS_LEVELEDITOR_SUBSYSTEM)
2313
+ if (ULevelEditorSubsystem *LES =
2314
+ GEditor->GetEditorSubsystem<ULevelEditorSubsystem>())
2315
+ LES->EditorInvalidateViewports();
2316
+ #endif
2317
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2318
+ Resp->SetBoolField(TEXT("success"), true);
2319
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Camera set"), Resp,
2320
+ FString());
2321
+ return true;
2322
+ }
2323
+ #endif
2324
+ if (FEditorViewportClient *ViewportClient =
2325
+ GEditor->GetActiveViewport()
2326
+ ? (FEditorViewportClient *)GEditor->GetActiveViewport()
2327
+ ->GetClient()
2328
+ : nullptr) {
2329
+ ViewportClient->SetViewLocation(Location);
2330
+ ViewportClient->SetViewRotation(Rotation);
2331
+ ViewportClient->Invalidate();
2332
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2333
+ Resp->SetBoolField(TEXT("success"), true);
2334
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Camera set"), Resp,
2335
+ FString());
2336
+ return true;
2337
+ }
2338
+ return false;
2339
+ #else
2340
+ return false;
2341
+ #endif
2342
+ }
2343
+
2344
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorSetViewMode(
2345
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2346
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2347
+ #if WITH_EDITOR
2348
+ FString Mode;
2349
+ Payload->TryGetStringField(TEXT("viewMode"), Mode);
2350
+ FString LowerMode = Mode.ToLower();
2351
+ FString Chosen;
2352
+ if (LowerMode == TEXT("lit"))
2353
+ Chosen = TEXT("Lit");
2354
+ else if (LowerMode == TEXT("unlit"))
2355
+ Chosen = TEXT("Unlit");
2356
+ else if (LowerMode == TEXT("wireframe"))
2357
+ Chosen = TEXT("Wireframe");
2358
+ else if (LowerMode == TEXT("detaillighting"))
2359
+ Chosen = TEXT("DetailLighting");
2360
+ else if (LowerMode == TEXT("lightingonly"))
2361
+ Chosen = TEXT("LightingOnly");
2362
+ else if (LowerMode == TEXT("lightcomplexity"))
2363
+ Chosen = TEXT("LightComplexity");
2364
+ else if (LowerMode == TEXT("shadercomplexity"))
2365
+ Chosen = TEXT("ShaderComplexity");
2366
+ else if (LowerMode == TEXT("lightmapdensity"))
2367
+ Chosen = TEXT("LightmapDensity");
2368
+ else if (LowerMode == TEXT("stationarylightoverlap"))
2369
+ Chosen = TEXT("StationaryLightOverlap");
2370
+ else if (LowerMode == TEXT("reflectionoverride"))
2371
+ Chosen = TEXT("ReflectionOverride");
2372
+ else
2373
+ Chosen = Mode;
2374
+
2375
+ const FString Cmd = FString::Printf(TEXT("viewmode %s"), *Chosen);
2376
+ if (GEditor->Exec(nullptr, *Cmd)) {
2377
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2378
+ Resp->SetBoolField(TEXT("success"), true);
2379
+ Resp->SetStringField(TEXT("viewMode"), Chosen);
2380
+ SendAutomationResponse(Socket, RequestId, true, TEXT("View mode set"), Resp,
2381
+ FString());
2382
+ return true;
2383
+ }
2384
+ SendAutomationResponse(Socket, RequestId, false,
2385
+ TEXT("View mode command failed"), nullptr,
2386
+ TEXT("EXEC_FAILED"));
2387
+ return true;
2388
+ #else
2389
+ return false;
2390
+ #endif
2391
+ }
2392
+
2393
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorAction(
2394
+ const FString &RequestId, const FString &Action,
2395
+ const TSharedPtr<FJsonObject> &Payload,
2396
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
2397
+ const FString Lower = Action.ToLower();
2398
+ if (!Lower.Equals(TEXT("control_editor"), ESearchCase::IgnoreCase) &&
2399
+ !Lower.StartsWith(TEXT("control_editor")))
2400
+ return false;
2401
+ if (!Payload.IsValid()) {
2402
+ SendAutomationError(RequestingSocket, RequestId,
2403
+ TEXT("control_editor payload missing."),
2404
+ TEXT("INVALID_PAYLOAD"));
2405
+ return true;
2406
+ }
2407
+
2408
+ FString SubAction;
2409
+ Payload->TryGetStringField(TEXT("action"), SubAction);
2410
+ const FString LowerSub = SubAction.ToLower();
2411
+
2412
+ #if WITH_EDITOR
2413
+ if (!GEditor) {
2414
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2415
+ TEXT("Editor not available"), nullptr,
2416
+ TEXT("EDITOR_NOT_AVAILABLE"));
2417
+ return true;
2418
+ }
2419
+
2420
+ if (LowerSub == TEXT("play"))
2421
+ return HandleControlEditorPlay(RequestId, Payload, RequestingSocket);
2422
+ if (LowerSub == TEXT("stop"))
2423
+ return HandleControlEditorStop(RequestId, Payload, RequestingSocket);
2424
+ if (LowerSub == TEXT("eject"))
2425
+ return HandleControlEditorEject(RequestId, Payload, RequestingSocket);
2426
+ if (LowerSub == TEXT("possess"))
2427
+ return HandleControlEditorPossess(RequestId, Payload, RequestingSocket);
2428
+ if (LowerSub == TEXT("focus_actor"))
2429
+ return HandleControlEditorFocusActor(RequestId, Payload, RequestingSocket);
2430
+ if (LowerSub == TEXT("set_camera") ||
2431
+ LowerSub == TEXT("set_camera_position") ||
2432
+ LowerSub == TEXT("set_viewport_camera"))
2433
+ return HandleControlEditorSetCamera(RequestId, Payload, RequestingSocket);
2434
+ if (LowerSub == TEXT("set_view_mode"))
2435
+ return HandleControlEditorSetViewMode(RequestId, Payload, RequestingSocket);
2436
+ if (LowerSub == TEXT("open_asset"))
2437
+ return HandleControlEditorOpenAsset(RequestId, Payload, RequestingSocket);
2438
+
2439
+ SendAutomationResponse(
2440
+ RequestingSocket, RequestId, false,
2441
+ FString::Printf(TEXT("Unknown editor control action: %s"), *LowerSub),
2442
+ nullptr, TEXT("UNKNOWN_ACTION"));
2443
+ return true;
2444
+ #else
2445
+ SendAutomationResponse(RequestingSocket, RequestId, false,
2446
+ TEXT("Editor control requires editor build."), nullptr,
2447
+ TEXT("NOT_IMPLEMENTED"));
2448
+ return true;
2449
+ #endif
2450
+ }
2451
+
2452
+ bool UMcpAutomationBridgeSubsystem::HandleControlEditorOpenAsset(
2453
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2454
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2455
+ #if WITH_EDITOR
2456
+ FString AssetPath;
2457
+ Payload->TryGetStringField(TEXT("assetPath"), AssetPath);
2458
+ if (AssetPath.IsEmpty()) {
2459
+ SendAutomationResponse(Socket, RequestId, false, TEXT("assetPath required"),
2460
+ nullptr, TEXT("INVALID_ARGUMENT"));
2461
+ return true;
2462
+ }
2463
+
2464
+ if (!GEditor) {
2465
+ SendAutomationResponse(Socket, RequestId, false,
2466
+ TEXT("Editor not available"), nullptr,
2467
+ TEXT("EDITOR_NOT_AVAILABLE"));
2468
+ return true;
2469
+ }
2470
+
2471
+ UAssetEditorSubsystem *AssetEditorSS =
2472
+ GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
2473
+ if (!AssetEditorSS) {
2474
+ SendAutomationResponse(Socket, RequestId, false,
2475
+ TEXT("AssetEditorSubsystem not available"), nullptr,
2476
+ TEXT("SUBSYSTEM_MISSING"));
2477
+ return true;
2478
+ }
2479
+
2480
+ if (!UEditorAssetLibrary::DoesAssetExist(AssetPath)) {
2481
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Asset not found"),
2482
+ nullptr, TEXT("ASSET_NOT_FOUND"));
2483
+ return true;
2484
+ }
2485
+
2486
+ UObject *Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
2487
+ if (!Asset) {
2488
+ SendAutomationResponse(Socket, RequestId, false,
2489
+ TEXT("Failed to load asset"), nullptr,
2490
+ TEXT("LOAD_FAILED"));
2491
+ return true;
2492
+ }
2493
+
2494
+ const bool bOpened = AssetEditorSS->OpenEditorForAsset(Asset);
2495
+
2496
+ TSharedPtr<FJsonObject> Resp = MakeShared<FJsonObject>();
2497
+ Resp->SetBoolField(TEXT("success"), bOpened);
2498
+ Resp->SetStringField(TEXT("assetPath"), AssetPath);
2499
+
2500
+ if (bOpened) {
2501
+ SendAutomationResponse(Socket, RequestId, true, TEXT("Asset opened"), Resp,
2502
+ FString());
2503
+ } else {
2504
+ SendAutomationResponse(Socket, RequestId, false,
2505
+ TEXT("Failed to open asset editor"), Resp,
2506
+ TEXT("OPEN_FAILED"));
2507
+ }
2508
+ return true;
2509
+ #else
2510
+ return false;
2511
+ #endif
2512
+ }
2513
+
2514
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorList(
2515
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2516
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2517
+ #if WITH_EDITOR
2518
+ FString Filter;
2519
+ Payload->TryGetStringField(TEXT("filter"), Filter);
2520
+
2521
+ UEditorActorSubsystem *ActorSS =
2522
+ GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
2523
+ if (!ActorSS) {
2524
+ SendAutomationResponse(Socket, RequestId, false,
2525
+ TEXT("EditorActorSubsystem unavailable"), nullptr,
2526
+ TEXT("SUBSYSTEM_MISSING"));
2527
+ return true;
2528
+ }
2529
+
2530
+ const TArray<AActor *> &AllActors = ActorSS->GetAllLevelActors();
2531
+ TArray<TSharedPtr<FJsonValue>> ActorsArray;
2532
+
2533
+ for (AActor *Actor : AllActors) {
2534
+ if (!Actor)
2535
+ continue;
2536
+ const FString Label = Actor->GetActorLabel();
2537
+ const FString Name = Actor->GetName();
2538
+ if (!Filter.IsEmpty() && !Label.Contains(Filter) && !Name.Contains(Filter))
2539
+ continue;
2540
+
2541
+ TSharedPtr<FJsonObject> Entry = MakeShared<FJsonObject>();
2542
+ Entry->SetStringField(TEXT("label"), Label);
2543
+ Entry->SetStringField(TEXT("name"), Name);
2544
+ Entry->SetStringField(TEXT("path"), Actor->GetPathName());
2545
+ Entry->SetStringField(TEXT("class"), Actor->GetClass()
2546
+ ? Actor->GetClass()->GetPathName()
2547
+ : TEXT(""));
2548
+ ActorsArray.Add(MakeShared<FJsonValueObject>(Entry));
2549
+ }
2550
+
2551
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
2552
+ Data->SetArrayField(TEXT("actors"), ActorsArray);
2553
+ Data->SetNumberField(TEXT("count"), ActorsArray.Num());
2554
+ if (!Filter.IsEmpty())
2555
+ Data->SetStringField(TEXT("filter"), Filter);
2556
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actors listed"),
2557
+ Data);
2558
+ return true;
2559
+ #else
2560
+ return false;
2561
+ #endif
2562
+ }
2563
+
2564
+ bool UMcpAutomationBridgeSubsystem::HandleControlActorGet(
2565
+ const FString &RequestId, const TSharedPtr<FJsonObject> &Payload,
2566
+ TSharedPtr<FMcpBridgeWebSocket> Socket) {
2567
+ #if WITH_EDITOR
2568
+ FString TargetName;
2569
+ Payload->TryGetStringField(TEXT("actorName"), TargetName);
2570
+ if (TargetName.IsEmpty()) {
2571
+ SendAutomationResponse(Socket, RequestId, false, TEXT("actorName required"),
2572
+ nullptr, TEXT("INVALID_ARGUMENT"));
2573
+ return true;
2574
+ }
2575
+
2576
+ AActor *Found = FindActorByName(TargetName);
2577
+ if (!Found) {
2578
+ SendAutomationResponse(Socket, RequestId, false, TEXT("Actor not found"),
2579
+ nullptr, TEXT("ACTOR_NOT_FOUND"));
2580
+ return true;
2581
+ }
2582
+
2583
+ const FTransform Current = Found->GetActorTransform();
2584
+ TSharedPtr<FJsonObject> Data = MakeShared<FJsonObject>();
2585
+ Data->SetStringField(TEXT("name"), Found->GetName());
2586
+ Data->SetStringField(TEXT("label"), Found->GetActorLabel());
2587
+ Data->SetStringField(TEXT("path"), Found->GetPathName());
2588
+ Data->SetStringField(TEXT("class"), Found->GetClass()
2589
+ ? Found->GetClass()->GetPathName()
2590
+ : TEXT(""));
2591
+
2592
+ TArray<TSharedPtr<FJsonValue>> TagsArray;
2593
+ for (const FName &Tag : Found->Tags) {
2594
+ TagsArray.Add(MakeShared<FJsonValueString>(Tag.ToString()));
2595
+ }
2596
+ Data->SetArrayField(TEXT("tags"), TagsArray);
2597
+
2598
+ auto MakeArray = [](const FVector &Vec) -> TArray<TSharedPtr<FJsonValue>> {
2599
+ TArray<TSharedPtr<FJsonValue>> Arr;
2600
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.X));
2601
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Y));
2602
+ Arr.Add(MakeShared<FJsonValueNumber>(Vec.Z));
2603
+ return Arr;
2604
+ };
2605
+ Data->SetArrayField(TEXT("location"), MakeArray(Current.GetLocation()));
2606
+ Data->SetArrayField(TEXT("scale"), MakeArray(Current.GetScale3D()));
2607
+
2608
+ SendStandardSuccessResponse(this, Socket, RequestId, TEXT("Actor retrieved"),
2609
+ Data);
2610
+ return true;
2611
+ #else
2612
+ return false;
2613
+ #endif
2614
+ }