unreal-engine-mcp-server 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter.yml +148 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +23 -0
  19. package/.github/workflows/labeler.yml +16 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +12 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +269 -22
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -72
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -604
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5475 -1627
  97. package/dist/tools/consolidated-tool-definitions.js +829 -482
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1009
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +45 -0
  161. package/dist/tools/logs.js +210 -0
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +195 -11
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -649
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -500
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1122
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +219 -0
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +250 -13
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -572
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. package/src/utils/stdio-redirect.ts +0 -18
@@ -0,0 +1,1184 @@
1
+ #include "McpAutomationBridgeGlobals.h"
2
+ #include "McpAutomationBridgeHelpers.h"
3
+ #include "McpAutomationBridgeSubsystem.h"
4
+ #include "Misc/ScopeExit.h"
5
+
6
+ #if WITH_EDITOR
7
+ #include "EdGraph/EdGraph.h"
8
+ #include "EdGraph/EdGraphNode.h"
9
+ #include "EdGraph/EdGraphPin.h"
10
+ #include "EdGraph/EdGraphSchema.h"
11
+ #include "EdGraphNode_Comment.h"
12
+ #include "Engine/Blueprint.h"
13
+ #include "K2Node_BreakStruct.h"
14
+ #include "K2Node_CallFunction.h"
15
+ #include "K2Node_CommutativeAssociativeBinaryOperator.h"
16
+ #include "K2Node_CustomEvent.h"
17
+ #include "K2Node_DynamicCast.h"
18
+ #include "K2Node_Event.h"
19
+ #include "K2Node_ExecutionSequence.h"
20
+ #include "K2Node_FunctionEntry.h"
21
+ #include "K2Node_FunctionResult.h"
22
+ #include "K2Node_IfThenElse.h"
23
+ #include "K2Node_InputAxisEvent.h"
24
+ #include "K2Node_Knot.h"
25
+ #include "K2Node_Literal.h"
26
+ #include "K2Node_MakeArray.h"
27
+ #include "K2Node_MakeStruct.h"
28
+ #include "K2Node_PromotableOperator.h"
29
+ #include "K2Node_Select.h"
30
+ #include "K2Node_Self.h"
31
+ #include "K2Node_Timeline.h"
32
+ #include "K2Node_VariableGet.h"
33
+ #include "K2Node_VariableSet.h"
34
+ #include "Kismet/GameplayStatics.h"
35
+ #include "Kismet/KismetMathLibrary.h"
36
+ #include "Kismet/KismetSystemLibrary.h"
37
+ #include "Kismet2/BlueprintEditorUtils.h"
38
+ #include "Kismet2/KismetEditorUtilities.h"
39
+
40
+ #endif
41
+
42
+ /**
43
+ * Process a "manage_blueprint_graph" automation request to inspect or modify a Blueprint graph.
44
+ *
45
+ * The Payload JSON controls the specific operation via the "subAction" field (examples: create_node,
46
+ * connect_pins, get_nodes, break_pin_links, delete_node, create_reroute_node, set_node_property,
47
+ * get_node_details, get_graph_details, get_pin_details). In editor builds this function performs
48
+ * graph/blueprint lookups and edits; in non-editor builds it reports an editor-only error.
49
+ *
50
+ * @param RequestId Unique identifier for the automation request (used in responses).
51
+ * @param Action The requested action name; this handler only processes "manage_blueprint_graph".
52
+ * @param Payload JSON object containing action options such as "assetPath"/"blueprintPath", "graphName",
53
+ * "subAction" and subaction-specific fields (nodeType, nodeId, pin names, positions, etc.).
54
+ * @param RequestingSocket WebSocket used to send responses and errors back to the requester.
55
+ * @return `true` if the request was handled by this function (Action == "manage_blueprint_graph"), `false` otherwise.
56
+ */
57
+ bool UMcpAutomationBridgeSubsystem::HandleBlueprintGraphAction(
58
+ const FString &RequestId, const FString &Action,
59
+ const TSharedPtr<FJsonObject> &Payload,
60
+ TSharedPtr<FMcpBridgeWebSocket> RequestingSocket) {
61
+ if (Action != TEXT("manage_blueprint_graph")) {
62
+ return false;
63
+ }
64
+
65
+ #if WITH_EDITOR
66
+ if (!Payload.IsValid()) {
67
+ SendAutomationError(RequestingSocket, RequestId,
68
+ TEXT("Missing payload for blueprint graph action."),
69
+ TEXT("INVALID_PAYLOAD"));
70
+ return true;
71
+ }
72
+
73
+ FString AssetPath;
74
+ if (!Payload->TryGetStringField(TEXT("assetPath"), AssetPath) ||
75
+ AssetPath.IsEmpty()) {
76
+ // Allow callers to use "blueprintPath" (as exposed by the consolidated
77
+ // tool schema) as an alias for assetPath so tests and tools do not need
78
+ // to duplicate the same value under two keys.
79
+ FString BlueprintPath;
80
+ Payload->TryGetStringField(TEXT("blueprintPath"), BlueprintPath);
81
+ if (!BlueprintPath.IsEmpty()) {
82
+ AssetPath = BlueprintPath;
83
+ }
84
+ }
85
+
86
+ if (AssetPath.IsEmpty()) {
87
+ SendAutomationError(
88
+ RequestingSocket, RequestId,
89
+ TEXT("Missing 'assetPath' or 'blueprintPath' in payload."),
90
+ TEXT("INVALID_ARGUMENT"));
91
+ return true;
92
+ }
93
+
94
+ UBlueprint *Blueprint = LoadObject<UBlueprint>(nullptr, *AssetPath);
95
+ if (!Blueprint) {
96
+ SendAutomationError(
97
+ RequestingSocket, RequestId,
98
+ FString::Printf(TEXT("Could not load blueprint at path: %s"),
99
+ *AssetPath),
100
+ TEXT("ASSET_NOT_FOUND"));
101
+ return true;
102
+ }
103
+
104
+ FString GraphName;
105
+ Payload->TryGetStringField(TEXT("graphName"), GraphName);
106
+ UEdGraph *TargetGraph = nullptr;
107
+
108
+ // Find the target graph
109
+ if (GraphName.IsEmpty() ||
110
+ GraphName.Equals(TEXT("EventGraph"), ESearchCase::IgnoreCase)) {
111
+ // Default to the main ubergraph/event graph
112
+ if (Blueprint->UbergraphPages.Num() > 0) {
113
+ TargetGraph = Blueprint->UbergraphPages[0];
114
+ }
115
+ } else {
116
+ // Search in FunctionGraphs and UbergraphPages
117
+ for (UEdGraph *Graph : Blueprint->FunctionGraphs) {
118
+ if (Graph->GetName() == GraphName) {
119
+ TargetGraph = Graph;
120
+ break;
121
+ }
122
+ }
123
+ if (!TargetGraph) {
124
+ for (UEdGraph *Graph : Blueprint->UbergraphPages) {
125
+ if (Graph->GetName() == GraphName) {
126
+ TargetGraph = Graph;
127
+ break;
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (!TargetGraph) {
134
+ // Fallback: try finding by name in all graphs
135
+ TArray<UEdGraph *> AllGraphs;
136
+ Blueprint->GetAllGraphs(AllGraphs);
137
+ for (UEdGraph *Graph : AllGraphs) {
138
+ if (Graph->GetName() == GraphName) {
139
+ TargetGraph = Graph;
140
+ break;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (!TargetGraph) {
146
+ SendAutomationError(
147
+ RequestingSocket, RequestId,
148
+ FString::Printf(TEXT("Could not find graph '%s' in blueprint."),
149
+ *GraphName),
150
+ TEXT("GRAPH_NOT_FOUND"));
151
+ return true;
152
+ }
153
+
154
+ const FString SubAction = Payload->GetStringField(TEXT("subAction"));
155
+
156
+ // Node identifier interoperability:
157
+ // - Prefer NodeGuid strings for stable references.
158
+ // - Accept node UObject names (e.g. "K2Node_Event_0") for clients that
159
+ // mistakenly pass nodeName where nodeId is expected.
160
+ auto FindNodeByIdOrName = [&](const FString &Id) -> UEdGraphNode * {
161
+ if (Id.IsEmpty()) {
162
+ return nullptr;
163
+ }
164
+
165
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
166
+ if (!Node) {
167
+ continue;
168
+ }
169
+
170
+ if (Node->NodeGuid.ToString().Equals(Id, ESearchCase::IgnoreCase) ||
171
+ Node->GetName().Equals(Id, ESearchCase::IgnoreCase)) {
172
+ return Node;
173
+ }
174
+ }
175
+
176
+ return nullptr;
177
+ };
178
+
179
+ if (SubAction == TEXT("create_node")) {
180
+ const FScopedTransaction Transaction(
181
+ FText::FromString(TEXT("Create Blueprint Node")));
182
+ Blueprint->Modify();
183
+ TargetGraph->Modify();
184
+
185
+ FString NodeType;
186
+ Payload->TryGetStringField(TEXT("nodeType"), NodeType);
187
+ float X = 0.0f;
188
+ float Y = 0.0f;
189
+ Payload->TryGetNumberField(TEXT("x"), X);
190
+ Payload->TryGetNumberField(TEXT("y"), Y);
191
+
192
+ // Helper to finalize and report
193
+ auto FinalizeAndReport = [&](auto &NodeCreator, UEdGraphNode *NewNode) {
194
+ if (NewNode) {
195
+ // Set position BEFORE finalization per FGraphNodeCreator pattern
196
+ NewNode->NodePosX = X;
197
+ NewNode->NodePosY = Y;
198
+
199
+ // Finalize() calls CreateNewGuid(), PostPlacedNewNode(), and
200
+ // AllocateDefaultPins() if pins are empty. Do NOT call
201
+ // AllocateDefaultPins() again after this!
202
+ NodeCreator.Finalize();
203
+
204
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
205
+
206
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
207
+ Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
208
+ Result->SetStringField(TEXT("nodeName"), NewNode->GetName());
209
+ SendAutomationResponse(RequestingSocket, RequestId, true,
210
+ TEXT("Node created."), Result);
211
+ } else {
212
+ SendAutomationError(
213
+ RequestingSocket, RequestId,
214
+ TEXT("Failed to create node (unsupported type or internal error)."),
215
+ TEXT("CREATE_FAILED"));
216
+ }
217
+ };
218
+
219
+ // Map common Blueprint node names to their CallFunction equivalents
220
+ // This allows users to use nodeType="PrintString" instead of CallFunction
221
+ static TMap<FString, TTuple<FString, FString>> CommonFunctionNodes = {
222
+ {TEXT("PrintString"),
223
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintString"))},
224
+ {TEXT("Print"),
225
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintString"))},
226
+ {TEXT("PrintText"),
227
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("PrintText"))},
228
+ {TEXT("SetActorLocation"),
229
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorLocation"))},
230
+ {TEXT("GetActorLocation"),
231
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorLocation"))},
232
+ {TEXT("SetActorRotation"),
233
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorRotation"))},
234
+ {TEXT("GetActorRotation"),
235
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorRotation"))},
236
+ {TEXT("SetActorTransform"),
237
+ MakeTuple(TEXT("AActor"), TEXT("K2_SetActorTransform"))},
238
+ {TEXT("GetActorTransform"),
239
+ MakeTuple(TEXT("AActor"), TEXT("K2_GetActorTransform"))},
240
+ {TEXT("AddActorLocalOffset"),
241
+ MakeTuple(TEXT("AActor"), TEXT("K2_AddActorLocalOffset"))},
242
+ {TEXT("Delay"), MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("Delay"))},
243
+ {TEXT("DestroyActor"),
244
+ MakeTuple(TEXT("AActor"), TEXT("K2_DestroyActor"))},
245
+ {TEXT("SpawnActor"),
246
+ MakeTuple(TEXT("UGameplayStatics"),
247
+ TEXT("BeginDeferredActorSpawnFromClass"))},
248
+ {TEXT("GetPlayerPawn"),
249
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetPlayerPawn"))},
250
+ {TEXT("GetPlayerController"),
251
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetPlayerController"))},
252
+ {TEXT("PlaySound"),
253
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySound2D"))},
254
+ {TEXT("PlaySound2D"),
255
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySound2D"))},
256
+ {TEXT("PlaySoundAtLocation"),
257
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("PlaySoundAtLocation"))},
258
+ {TEXT("GetWorldDeltaSeconds"),
259
+ MakeTuple(TEXT("UGameplayStatics"), TEXT("GetWorldDeltaSeconds"))},
260
+ {TEXT("SetTimerByFunctionName"),
261
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_SetTimer"))},
262
+ {TEXT("ClearTimer"),
263
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_ClearTimer"))},
264
+ {TEXT("IsValid"),
265
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("IsValid"))},
266
+ {TEXT("IsValidClass"),
267
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("IsValidClass"))},
268
+ // Math Nodes
269
+ {TEXT("Add_IntInt"),
270
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Add_IntInt"))},
271
+ {TEXT("Subtract_IntInt"),
272
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Subtract_IntInt"))},
273
+ {TEXT("Multiply_IntInt"),
274
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Multiply_IntInt"))},
275
+ {TEXT("Divide_IntInt"),
276
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Divide_IntInt"))},
277
+ {TEXT("Add_DoubleDouble"),
278
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Add_DoubleDouble"))},
279
+ {TEXT("Subtract_DoubleDouble"),
280
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Subtract_DoubleDouble"))},
281
+ {TEXT("Multiply_DoubleDouble"),
282
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Multiply_DoubleDouble"))},
283
+ {TEXT("Divide_DoubleDouble"),
284
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("Divide_DoubleDouble"))},
285
+ {TEXT("FTrunc"), MakeTuple(TEXT("UKismetMathLibrary"), TEXT("FTrunc"))},
286
+ // Vector Ops
287
+ {TEXT("MakeVector"),
288
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("MakeVector"))},
289
+ {TEXT("BreakVector"),
290
+ MakeTuple(TEXT("UKismetMathLibrary"), TEXT("BreakVector"))},
291
+ // Actor/Component Ops
292
+ {TEXT("GetComponentByClass"),
293
+ MakeTuple(TEXT("AActor"), TEXT("GetComponentByClass"))},
294
+ // Timer
295
+ {TEXT("GetWorldTimerManager"),
296
+ MakeTuple(TEXT("UKismetSystemLibrary"), TEXT("K2_GetTimerManager"))}};
297
+
298
+ // Check if this is a common function node shortcut
299
+ if (const auto *FuncInfo = CommonFunctionNodes.Find(NodeType)) {
300
+ FString ClassName = FuncInfo->Get<0>();
301
+ FString FuncName = FuncInfo->Get<1>();
302
+
303
+ // Find the class and function BEFORE creating NodeCreator
304
+ // (FGraphNodeCreator asserts in destructor if not finalized)
305
+ UClass *Class = nullptr;
306
+ if (ClassName == TEXT("UKismetSystemLibrary")) {
307
+ Class = UKismetSystemLibrary::StaticClass();
308
+ } else if (ClassName == TEXT("UGameplayStatics")) {
309
+ Class = UGameplayStatics::StaticClass();
310
+ } else if (ClassName == TEXT("AActor")) {
311
+ Class = AActor::StaticClass();
312
+ } else if (ClassName == TEXT("UKismetMathLibrary")) {
313
+ Class = UKismetMathLibrary::StaticClass();
314
+ } else {
315
+ Class = ResolveUClass(ClassName);
316
+ }
317
+
318
+ UFunction *Func = nullptr;
319
+ if (Class) {
320
+ Func = Class->FindFunctionByName(*FuncName);
321
+ }
322
+
323
+ // Return early with error if function not found (before NodeCreator)
324
+ if (!Func) {
325
+ SendAutomationError(
326
+ RequestingSocket, RequestId,
327
+ FString::Printf(
328
+ TEXT("Could not find function '%s::%s' for node type '%s'"),
329
+ *ClassName, *FuncName, *NodeType),
330
+ TEXT("FUNCTION_NOT_FOUND"));
331
+ return true;
332
+ }
333
+
334
+ // Now safe to create NodeCreator since we know we'll finalize it
335
+ FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
336
+ UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
337
+ CallFuncNode->SetFromFunction(Func);
338
+ FinalizeAndReport(NodeCreator, CallFuncNode);
339
+ return true;
340
+ }
341
+
342
+ // Basic node creation logic - this can be expanded significantly
343
+ if (NodeType == TEXT("InputAxisEvent")) {
344
+ FString InputAxisName;
345
+ Payload->TryGetStringField(TEXT("inputAxisName"), InputAxisName);
346
+
347
+ if (InputAxisName.IsEmpty()) {
348
+ SendAutomationError(RequestingSocket, RequestId,
349
+ TEXT("inputAxisName required"),
350
+ TEXT("INVALID_ARGUMENT"));
351
+ return true;
352
+ }
353
+
354
+ FGraphNodeCreator<UK2Node_InputAxisEvent> NodeCreator(*TargetGraph);
355
+ UK2Node_InputAxisEvent *InputNode = NodeCreator.CreateNode(false);
356
+ InputNode->InputAxisName = FName(*InputAxisName);
357
+
358
+ FinalizeAndReport(NodeCreator, InputNode);
359
+ } else if (NodeType == TEXT("CallFunction") ||
360
+ NodeType == TEXT("K2Node_CallFunction") ||
361
+ NodeType == TEXT("FunctionCall")) {
362
+ FString MemberName;
363
+ Payload->TryGetStringField(TEXT("memberName"), MemberName);
364
+ FString MemberClass;
365
+ Payload->TryGetStringField(TEXT("memberClass"),
366
+ MemberClass); // Optional, for static functions
367
+
368
+ UFunction *Func = nullptr;
369
+ if (!MemberClass.IsEmpty()) {
370
+ UClass *Class = ResolveUClass(MemberClass);
371
+ if (Class) {
372
+ Func = Class->FindFunctionByName(*MemberName);
373
+ }
374
+ } else {
375
+ // Try to find in blueprint context
376
+ Func = Blueprint->GeneratedClass->FindFunctionByName(*MemberName);
377
+ if (!Func) {
378
+ // Try global search if simple name, or check common libraries
379
+ Func = FindObject<UFunction>(nullptr, *MemberName);
380
+ if (!Func) {
381
+ // Fallback: Check common libraries
382
+ if (UClass *KSL = UKismetSystemLibrary::StaticClass())
383
+ Func = KSL->FindFunctionByName(*MemberName);
384
+ if (!Func)
385
+ if (UClass *GPS = UGameplayStatics::StaticClass())
386
+ Func = GPS->FindFunctionByName(*MemberName);
387
+ if (!Func)
388
+ if (UClass *KML = UKismetMathLibrary::StaticClass())
389
+ Func = KML->FindFunctionByName(*MemberName);
390
+ }
391
+ }
392
+ }
393
+
394
+ if (Func) {
395
+ FGraphNodeCreator<UK2Node_CallFunction> NodeCreator(*TargetGraph);
396
+ UK2Node_CallFunction *CallFuncNode = NodeCreator.CreateNode(false);
397
+ CallFuncNode->SetFromFunction(Func);
398
+ FinalizeAndReport(NodeCreator, CallFuncNode);
399
+ } else {
400
+ SendAutomationError(
401
+ RequestingSocket, RequestId,
402
+ FString::Printf(TEXT("Could not find function '%s'"), *MemberName),
403
+ TEXT("FUNCTION_NOT_FOUND"));
404
+ return true;
405
+ }
406
+ } else if (NodeType == TEXT("VariableGet")) {
407
+ FString VarName;
408
+ Payload->TryGetStringField(TEXT("variableName"), VarName);
409
+ FName VarFName(*VarName);
410
+
411
+ // Validation BEFORE creation
412
+ bool bFound = false;
413
+ for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
414
+ if (VarDesc.VarName == VarFName) {
415
+ bFound = true;
416
+ break;
417
+ }
418
+ }
419
+ if (!bFound && Blueprint->GeneratedClass &&
420
+ Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
421
+ bFound = true;
422
+ }
423
+
424
+ if (!bFound) {
425
+ SendAutomationError(
426
+ RequestingSocket, RequestId,
427
+ FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
428
+ TEXT("VARIABLE_NOT_FOUND"));
429
+ return true;
430
+ }
431
+
432
+ FGraphNodeCreator<UK2Node_VariableGet> NodeCreator(*TargetGraph);
433
+ UK2Node_VariableGet *VarGet = NodeCreator.CreateNode(false);
434
+ VarGet->VariableReference.SetSelfMember(VarFName);
435
+ FinalizeAndReport(NodeCreator, VarGet);
436
+ } else if (NodeType == TEXT("VariableSet")) {
437
+ FString VarName;
438
+ Payload->TryGetStringField(TEXT("variableName"), VarName);
439
+ FName VarFName(*VarName);
440
+
441
+ // Validation BEFORE creation
442
+ bool bFound = false;
443
+ for (const FBPVariableDescription &VarDesc : Blueprint->NewVariables) {
444
+ if (VarDesc.VarName == VarFName) {
445
+ bFound = true;
446
+ break;
447
+ }
448
+ }
449
+ if (!bFound && Blueprint->GeneratedClass &&
450
+ Blueprint->GeneratedClass->FindPropertyByName(VarFName)) {
451
+ bFound = true;
452
+ }
453
+
454
+ if (!bFound) {
455
+ SendAutomationError(
456
+ RequestingSocket, RequestId,
457
+ FString::Printf(TEXT("Could not find variable '%s'"), *VarName),
458
+ TEXT("VARIABLE_NOT_FOUND"));
459
+ return true;
460
+ }
461
+
462
+ FGraphNodeCreator<UK2Node_VariableSet> NodeCreator(*TargetGraph);
463
+ UK2Node_VariableSet *VarSet = NodeCreator.CreateNode(false);
464
+ VarSet->VariableReference.SetSelfMember(VarFName);
465
+ FinalizeAndReport(NodeCreator, VarSet);
466
+ } else if (NodeType == TEXT("CustomEvent")) {
467
+ FString EventName;
468
+ Payload->TryGetStringField(TEXT("eventName"), EventName);
469
+
470
+ FGraphNodeCreator<UK2Node_CustomEvent> NodeCreator(*TargetGraph);
471
+ UK2Node_CustomEvent *EventNode = NodeCreator.CreateNode(false);
472
+
473
+ EventNode->CustomFunctionName = FName(*EventName);
474
+ FinalizeAndReport(NodeCreator, EventNode);
475
+ } else if (NodeType == TEXT("Event") || NodeType == TEXT("K2Node_Event")) {
476
+ FString EventName;
477
+ Payload->TryGetStringField(
478
+ TEXT("eventName"),
479
+ EventName); // e.g., "ReceiveBeginPlay", "ReceiveTick"
480
+ FString MemberClass;
481
+ Payload->TryGetStringField(TEXT("memberClass"),
482
+ MemberClass); // Optional class override
483
+
484
+ if (EventName.IsEmpty()) {
485
+ SendAutomationError(RequestingSocket, RequestId,
486
+ TEXT("eventName required for Event node"),
487
+ TEXT("INVALID_ARGUMENT"));
488
+ return true;
489
+ }
490
+
491
+ // Map common event name aliases to their actual function names
492
+ static TMap<FString, FString> EventNameAliases = {
493
+ {TEXT("BeginPlay"), TEXT("ReceiveBeginPlay")},
494
+ {TEXT("Tick"), TEXT("ReceiveTick")},
495
+ {TEXT("EndPlay"), TEXT("ReceiveEndPlay")},
496
+ {TEXT("ActorBeginOverlap"), TEXT("ReceiveActorBeginOverlap")},
497
+ {TEXT("ActorEndOverlap"), TEXT("ReceiveActorEndOverlap")},
498
+ {TEXT("Hit"), TEXT("ReceiveHit")},
499
+ {TEXT("BeginCursorOver"), TEXT("ReceiveBeginCursorOver")},
500
+ {TEXT("EndCursorOver"), TEXT("ReceiveEndCursorOver")},
501
+ {TEXT("Clicked"), TEXT("ReceiveClicked")},
502
+ {TEXT("Released"), TEXT("ReceiveReleased")},
503
+ {TEXT("Destroyed"), TEXT("ReceiveDestroyed")},
504
+ };
505
+
506
+ if (const FString *Alias = EventNameAliases.Find(EventName)) {
507
+ EventName = *Alias;
508
+ }
509
+
510
+ // Determine target class: use explicit MemberClass or search hierarchy
511
+ UClass *TargetClass = nullptr;
512
+ UFunction *EventFunc = nullptr;
513
+
514
+ if (!MemberClass.IsEmpty()) {
515
+ // Explicit class specified
516
+ TargetClass = ResolveUClass(MemberClass);
517
+ if (TargetClass) {
518
+ EventFunc = TargetClass->FindFunctionByName(*EventName);
519
+ }
520
+ } else {
521
+ // Search up the class hierarchy starting from the Blueprint's parent
522
+ // class. Events like ReceiveBeginPlay are defined in AActor, not in
523
+ // the generated Blueprint class.
524
+ UClass *SearchClass = Blueprint->ParentClass;
525
+ while (SearchClass && !EventFunc) {
526
+ EventFunc = SearchClass->FindFunctionByName(
527
+ *EventName, EIncludeSuperFlag::ExcludeSuper);
528
+ if (EventFunc) {
529
+ TargetClass = SearchClass;
530
+ break;
531
+ }
532
+ SearchClass = SearchClass->GetSuperClass();
533
+ }
534
+
535
+ // If not found in hierarchy, try the generated class
536
+ if (!EventFunc && Blueprint->GeneratedClass) {
537
+ EventFunc = Blueprint->GeneratedClass->FindFunctionByName(*EventName);
538
+ if (EventFunc) {
539
+ TargetClass = Blueprint->GeneratedClass;
540
+ }
541
+ }
542
+ }
543
+
544
+ if (EventFunc && TargetClass) {
545
+ FGraphNodeCreator<UK2Node_Event> NodeCreator(*TargetGraph);
546
+ UK2Node_Event *EventNode = NodeCreator.CreateNode(false);
547
+ EventNode->EventReference.SetFromField<UFunction>(EventFunc, false);
548
+ EventNode->bOverrideFunction = true;
549
+ FinalizeAndReport(NodeCreator, EventNode);
550
+ } else {
551
+ // Provide helpful error message
552
+ FString SearchedClasses;
553
+ UClass *C = Blueprint->ParentClass;
554
+ int ClassCount = 0;
555
+ while (C && ClassCount < 5) {
556
+ if (!SearchedClasses.IsEmpty())
557
+ SearchedClasses += TEXT(", ");
558
+ SearchedClasses += C->GetName();
559
+ C = C->GetSuperClass();
560
+ ClassCount++;
561
+ }
562
+ SendAutomationError(
563
+ RequestingSocket, RequestId,
564
+ FString::Printf(TEXT("Could not find event '%s'. Searched classes: "
565
+ "%s. Try using the full name like "
566
+ "'ReceiveBeginPlay' instead of 'BeginPlay'."),
567
+ *EventName, *SearchedClasses),
568
+ TEXT("EVENT_NOT_FOUND"));
569
+ }
570
+
571
+ } else if (NodeType == TEXT("Cast") ||
572
+ NodeType.StartsWith(TEXT("CastTo"))) {
573
+ FString TargetClassName;
574
+ Payload->TryGetStringField(TEXT("targetClass"), TargetClassName);
575
+
576
+ // If targetClass not specified, try to infer from nodeType
577
+ // "CastTo<ClassName>"
578
+ if (TargetClassName.IsEmpty() && NodeType.StartsWith(TEXT("CastTo"))) {
579
+ TargetClassName = NodeType.Mid(6); // Remove "CastTo" prefix
580
+ }
581
+
582
+ UClass *TargetClass = ResolveUClass(TargetClassName);
583
+ if (!TargetClass) {
584
+ SendAutomationError(
585
+ RequestingSocket, RequestId,
586
+ FString::Printf(
587
+ TEXT("Could not resolve target class '%s' for Cast node"),
588
+ *TargetClassName),
589
+ TEXT("CLASS_NOT_FOUND"));
590
+ return true;
591
+ }
592
+
593
+ FGraphNodeCreator<UK2Node_DynamicCast> NodeCreator(*TargetGraph);
594
+ UK2Node_DynamicCast *CastNode = NodeCreator.CreateNode(false);
595
+ CastNode->TargetType = TargetClass;
596
+ FinalizeAndReport(NodeCreator, CastNode);
597
+ } else if (NodeType == TEXT("Sequence")) {
598
+ FGraphNodeCreator<UK2Node_ExecutionSequence> NodeCreator(*TargetGraph);
599
+ UK2Node_ExecutionSequence *NewNode = NodeCreator.CreateNode(false);
600
+ FinalizeAndReport(NodeCreator, NewNode);
601
+ } else if (NodeType == TEXT("Branch") || NodeType == TEXT("IfThenElse") ||
602
+ NodeType == TEXT("K2Node_IfThenElse")) {
603
+ FGraphNodeCreator<UK2Node_IfThenElse> NodeCreator(*TargetGraph);
604
+ UK2Node_IfThenElse *BranchNode = NodeCreator.CreateNode(false);
605
+ FinalizeAndReport(NodeCreator, BranchNode);
606
+ } else if (NodeType == TEXT("Literal")) {
607
+ // Create a literal node that can hold an object reference. This is a
608
+ // fully functional K2 literal node that returns the referenced asset
609
+ // or object when executed in the graph.
610
+ FString LiteralType;
611
+ Payload->TryGetStringField(TEXT("literalType"), LiteralType);
612
+ LiteralType.TrimStartAndEndInline();
613
+ const FString LiteralTypeLower =
614
+ LiteralType.IsEmpty() ? TEXT("object") : LiteralType.ToLower();
615
+
616
+ if (LiteralTypeLower == TEXT("object") ||
617
+ LiteralTypeLower == TEXT("asset")) {
618
+ FString ObjectPath;
619
+ Payload->TryGetStringField(TEXT("objectPath"), ObjectPath);
620
+ if (ObjectPath.IsEmpty()) {
621
+ // As a convenience, allow callers to use assetPath as the
622
+ // literal source when objectPath is omitted.
623
+ Payload->TryGetStringField(TEXT("assetPath"), ObjectPath);
624
+ }
625
+
626
+ if (ObjectPath.IsEmpty()) {
627
+ SendAutomationError(RequestingSocket, RequestId,
628
+ TEXT("Literal object creation requires "
629
+ "'objectPath' or 'assetPath'."),
630
+ TEXT("INVALID_LITERAL"));
631
+ return true;
632
+ }
633
+
634
+ UObject *LoadedObject = LoadObject<UObject>(nullptr, *ObjectPath);
635
+ if (!LoadedObject) {
636
+ SendAutomationError(
637
+ RequestingSocket, RequestId,
638
+ FString::Printf(TEXT("Literal object not found at path '%s'"),
639
+ *ObjectPath),
640
+ TEXT("OBJECT_NOT_FOUND"));
641
+ return true;
642
+ }
643
+
644
+ // Create the node only after successful validation
645
+ FGraphNodeCreator<UK2Node_Literal> NodeCreator(*TargetGraph);
646
+ UK2Node_Literal *LiteralNode = NodeCreator.CreateNode(false);
647
+ if (!LiteralNode) {
648
+ SendAutomationError(RequestingSocket, RequestId,
649
+ TEXT("Failed to allocate Literal node."),
650
+ TEXT("CREATE_FAILED"));
651
+ return true;
652
+ }
653
+
654
+ // UK2Node_Literal stores the referenced UObject in a private
655
+ // member; use its public setter rather than touching the
656
+ // field directly so we respect engine encapsulation.
657
+ LiteralNode->SetObjectRef(LoadedObject);
658
+ FinalizeAndReport(NodeCreator, LiteralNode);
659
+ } else {
660
+ // Primitive literal support (float/int/bool/strings) can be
661
+ // added later by wiring value pins. For now, fail fast rather
662
+ // than pretending success.
663
+ SendAutomationError(
664
+ RequestingSocket, RequestId,
665
+ FString::Printf(TEXT("Unsupported literalType '%s' (only "
666
+ "'object'/'asset' supported)."),
667
+ *LiteralType),
668
+ TEXT("UNSUPPORTED_LITERAL_TYPE"));
669
+ return true;
670
+ }
671
+ } else if (NodeType == TEXT("Comment")) {
672
+ FGraphNodeCreator<UEdGraphNode_Comment> NodeCreator(*TargetGraph);
673
+ UEdGraphNode_Comment *CommentNode = NodeCreator.CreateNode(false);
674
+
675
+ FString CommentText;
676
+ if (Payload->TryGetStringField(TEXT("comment"), CommentText) &&
677
+ !CommentText.IsEmpty()) {
678
+ CommentNode->NodeComment = CommentText;
679
+ } else {
680
+ CommentNode->NodeComment = TEXT("Comment");
681
+ }
682
+
683
+ CommentNode->NodeWidth = 400;
684
+ CommentNode->NodeHeight = 100;
685
+
686
+ FinalizeAndReport(NodeCreator, CommentNode);
687
+ } else if (NodeType == TEXT("MakeArray")) {
688
+ FGraphNodeCreator<UK2Node_MakeArray> NodeCreator(*TargetGraph);
689
+ UK2Node_MakeArray *MakeArrayNode = NodeCreator.CreateNode(false);
690
+ FinalizeAndReport(NodeCreator, MakeArrayNode);
691
+ } else if (NodeType == TEXT("Return")) {
692
+ FGraphNodeCreator<UK2Node_FunctionResult> NodeCreator(*TargetGraph);
693
+ UK2Node_FunctionResult *ReturnNode = NodeCreator.CreateNode(false);
694
+ FinalizeAndReport(NodeCreator, ReturnNode);
695
+ } else if (NodeType == TEXT("Self")) {
696
+ FGraphNodeCreator<UK2Node_Self> NodeCreator(*TargetGraph);
697
+ UK2Node_Self *SelfNode = NodeCreator.CreateNode(false);
698
+ FinalizeAndReport(NodeCreator, SelfNode);
699
+ } else if (NodeType == TEXT("Select")) {
700
+ FGraphNodeCreator<UK2Node_Select> NodeCreator(*TargetGraph);
701
+ UK2Node_Select *SelectNode = NodeCreator.CreateNode(false);
702
+ FinalizeAndReport(NodeCreator, SelectNode);
703
+ } else if (NodeType == TEXT("Timeline")) {
704
+ FGraphNodeCreator<UK2Node_Timeline> NodeCreator(*TargetGraph);
705
+ UK2Node_Timeline *TimelineNode = NodeCreator.CreateNode(false);
706
+
707
+ FString TimelineName;
708
+ if (Payload->TryGetStringField(TEXT("timelineName"), TimelineName) &&
709
+ !TimelineName.IsEmpty()) {
710
+ TimelineNode->TimelineName = FName(*TimelineName);
711
+ }
712
+
713
+ FinalizeAndReport(NodeCreator, TimelineNode);
714
+ } else if (NodeType == TEXT("MakeStruct")) {
715
+ FString StructName;
716
+ Payload->TryGetStringField(TEXT("structName"), StructName);
717
+ if (StructName.IsEmpty()) {
718
+ SendAutomationError(RequestingSocket, RequestId,
719
+ TEXT("structName required for MakeStruct"),
720
+ TEXT("INVALID_ARGUMENT"));
721
+ return true;
722
+ }
723
+ UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
724
+ if (!Struct) {
725
+ SendAutomationError(RequestingSocket, RequestId,
726
+ TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
727
+ return true;
728
+ }
729
+
730
+ FGraphNodeCreator<UK2Node_MakeStruct> NodeCreator(*TargetGraph);
731
+ UK2Node_MakeStruct *MakeStructNode = NodeCreator.CreateNode(false);
732
+ MakeStructNode->StructType = Struct;
733
+ FinalizeAndReport(NodeCreator, MakeStructNode);
734
+ } else if (NodeType == TEXT("BreakStruct")) {
735
+ FString StructName;
736
+ Payload->TryGetStringField(TEXT("structName"), StructName);
737
+ if (StructName.IsEmpty()) {
738
+ SendAutomationError(RequestingSocket, RequestId,
739
+ TEXT("structName required for BreakStruct"),
740
+ TEXT("INVALID_ARGUMENT"));
741
+ return true;
742
+ }
743
+ UScriptStruct *Struct = FindObject<UScriptStruct>(nullptr, *StructName);
744
+ if (!Struct) {
745
+ SendAutomationError(RequestingSocket, RequestId,
746
+ TEXT("Struct not found"), TEXT("STRUCT_NOT_FOUND"));
747
+ return true;
748
+ }
749
+
750
+ FGraphNodeCreator<UK2Node_BreakStruct> NodeCreator(*TargetGraph);
751
+ UK2Node_BreakStruct *BreakStructNode = NodeCreator.CreateNode(false);
752
+ BreakStructNode->StructType = Struct;
753
+ FinalizeAndReport(NodeCreator, BreakStructNode);
754
+ } else {
755
+ SendAutomationError(
756
+ RequestingSocket, RequestId,
757
+ TEXT("Failed to create node (unsupported type or internal error)."),
758
+ TEXT("CREATE_FAILED"));
759
+ }
760
+ return true;
761
+ } else if (SubAction == TEXT("connect_pins")) {
762
+ const FScopedTransaction Transaction(
763
+ FText::FromString(TEXT("Connect Blueprint Pins")));
764
+ Blueprint->Modify();
765
+ TargetGraph->Modify();
766
+
767
+ FString FromNodeId, FromPinName, ToNodeId, ToPinName;
768
+ Payload->TryGetStringField(TEXT("fromNodeId"), FromNodeId);
769
+ Payload->TryGetStringField(TEXT("fromPinName"), FromPinName);
770
+ Payload->TryGetStringField(TEXT("toNodeId"), ToNodeId);
771
+ Payload->TryGetStringField(TEXT("toPinName"), ToPinName);
772
+
773
+ UEdGraphNode *FromNode = FindNodeByIdOrName(FromNodeId);
774
+ UEdGraphNode *ToNode = FindNodeByIdOrName(ToNodeId);
775
+
776
+ if (!FromNode || !ToNode) {
777
+ SendAutomationError(RequestingSocket, RequestId,
778
+ TEXT("Could not find source or target node."),
779
+ TEXT("NODE_NOT_FOUND"));
780
+ return true;
781
+ }
782
+
783
+ // Handle PinName in format "NodeName.PinName"
784
+ FString FromPinClean = FromPinName;
785
+ if (FromPinName.Contains(TEXT("."))) {
786
+ FromPinName.Split(TEXT("."), nullptr, &FromPinClean);
787
+ }
788
+ FString ToPinClean = ToPinName;
789
+ if (ToPinName.Contains(TEXT("."))) {
790
+ ToPinName.Split(TEXT("."), nullptr, &ToPinClean);
791
+ }
792
+
793
+ UEdGraphPin *FromPin = FromNode->FindPin(*FromPinClean);
794
+ UEdGraphPin *ToPin = ToNode->FindPin(*ToPinClean);
795
+
796
+ if (!FromPin || !ToPin) {
797
+ SendAutomationError(RequestingSocket, RequestId,
798
+ TEXT("Could not find source or target pin."),
799
+ TEXT("PIN_NOT_FOUND"));
800
+ return true;
801
+ }
802
+
803
+ FromNode->Modify();
804
+ ToNode->Modify();
805
+
806
+ if (TargetGraph->GetSchema()->TryCreateConnection(FromPin, ToPin)) {
807
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
808
+ SendAutomationResponse(RequestingSocket, RequestId, true,
809
+ TEXT("Pins connected."));
810
+ } else {
811
+ SendAutomationError(RequestingSocket, RequestId,
812
+ TEXT("Failed to connect pins (schema rejection)."),
813
+ TEXT("CONNECTION_FAILED"));
814
+ }
815
+ return true;
816
+ } else if (SubAction == TEXT("get_nodes")) {
817
+ TArray<TSharedPtr<FJsonValue>> NodesArray;
818
+
819
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
820
+ if (!Node)
821
+ continue;
822
+
823
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
824
+ NodeObj->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
825
+ NodeObj->SetStringField(TEXT("nodeName"), Node->GetName());
826
+ NodeObj->SetStringField(TEXT("nodeType"), Node->GetClass()->GetName());
827
+ NodeObj->SetStringField(
828
+ TEXT("nodeTitle"),
829
+ Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
830
+ NodeObj->SetStringField(TEXT("comment"), Node->NodeComment);
831
+ NodeObj->SetNumberField(TEXT("x"), Node->NodePosX);
832
+ NodeObj->SetNumberField(TEXT("y"), Node->NodePosY);
833
+
834
+ TArray<TSharedPtr<FJsonValue>> PinsArray;
835
+ for (UEdGraphPin *Pin : Node->Pins) {
836
+ if (!Pin)
837
+ continue;
838
+
839
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
840
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
841
+ PinObj->SetStringField(TEXT("pinType"),
842
+ Pin->PinType.PinCategory.ToString());
843
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
844
+ ? TEXT("Input")
845
+ : TEXT("Output"));
846
+
847
+ // Add pin sub-category object type if applicable
848
+ if (Pin->PinType.PinCategory == TEXT("object") ||
849
+ Pin->PinType.PinCategory == TEXT("class") ||
850
+ Pin->PinType.PinCategory == TEXT("struct")) {
851
+ if (Pin->PinType.PinSubCategoryObject.IsValid()) {
852
+ PinObj->SetStringField(
853
+ TEXT("pinSubType"),
854
+ Pin->PinType.PinSubCategoryObject->GetName());
855
+ }
856
+ }
857
+
858
+ TArray<TSharedPtr<FJsonValue>> LinkedToFileArray;
859
+ for (UEdGraphPin *LinkedPin : Pin->LinkedTo) {
860
+ if (LinkedPin && LinkedPin->GetOwningNode()) {
861
+ TSharedPtr<FJsonObject> LinkObj = MakeShared<FJsonObject>();
862
+ LinkObj->SetStringField(
863
+ TEXT("nodeId"),
864
+ LinkedPin->GetOwningNode()->NodeGuid.ToString());
865
+ LinkObj->SetStringField(TEXT("pinName"),
866
+ LinkedPin->PinName.ToString());
867
+ LinkedToFileArray.Add(MakeShared<FJsonValueObject>(LinkObj));
868
+ }
869
+ }
870
+ PinObj->SetArrayField(TEXT("linkedTo"), LinkedToFileArray);
871
+ PinsArray.Add(MakeShared<FJsonValueObject>(PinObj));
872
+ }
873
+ NodeObj->SetArrayField(TEXT("pins"), PinsArray);
874
+
875
+ NodesArray.Add(MakeShared<FJsonValueObject>(NodeObj));
876
+ }
877
+
878
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
879
+ Result->SetArrayField(TEXT("nodes"), NodesArray);
880
+ Result->SetStringField(TEXT("graphName"), TargetGraph->GetName());
881
+
882
+ SendAutomationResponse(RequestingSocket, RequestId, true,
883
+ TEXT("Nodes retrieved."), Result);
884
+ return true;
885
+ } else if (SubAction == TEXT("break_pin_links")) {
886
+ const FScopedTransaction Transaction(
887
+ FText::FromString(TEXT("Break Blueprint Pin Links")));
888
+ Blueprint->Modify();
889
+ TargetGraph->Modify();
890
+
891
+ FString NodeId, PinName;
892
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
893
+ Payload->TryGetStringField(TEXT("pinName"), PinName);
894
+
895
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
896
+
897
+ if (!TargetNode) {
898
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
899
+ TEXT("NODE_NOT_FOUND"));
900
+ return true;
901
+ }
902
+
903
+ UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
904
+ if (!Pin) {
905
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
906
+ TEXT("PIN_NOT_FOUND"));
907
+ return true;
908
+ }
909
+
910
+ TargetNode->Modify();
911
+ TargetGraph->GetSchema()->BreakPinLinks(*Pin, true);
912
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
913
+ SendAutomationResponse(RequestingSocket, RequestId, true,
914
+ TEXT("Pin links broken."));
915
+ return true;
916
+ }
917
+
918
+ else if (SubAction == TEXT("delete_node")) {
919
+ const FScopedTransaction Transaction(
920
+ FText::FromString(TEXT("Delete Blueprint Node")));
921
+ Blueprint->Modify();
922
+ TargetGraph->Modify();
923
+
924
+ FString NodeId;
925
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
926
+
927
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
928
+
929
+ if (TargetNode) {
930
+ FBlueprintEditorUtils::RemoveNode(Blueprint, TargetNode, true);
931
+ SendAutomationResponse(RequestingSocket, RequestId, true,
932
+ TEXT("Node deleted."));
933
+ } else {
934
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
935
+ TEXT("NODE_NOT_FOUND"));
936
+ }
937
+ return true;
938
+ } else if (SubAction == TEXT("create_reroute_node")) {
939
+ const FScopedTransaction Transaction(
940
+ FText::FromString(TEXT("Create Reroute Node")));
941
+ Blueprint->Modify();
942
+ TargetGraph->Modify();
943
+
944
+ float X = 0.0f;
945
+ float Y = 0.0f;
946
+ Payload->TryGetNumberField(TEXT("x"), X);
947
+ Payload->TryGetNumberField(TEXT("y"), Y);
948
+
949
+ FGraphNodeCreator<UK2Node_Knot> NodeCreator(*TargetGraph);
950
+ UK2Node_Knot *RerouteNode = NodeCreator.CreateNode(false);
951
+
952
+ RerouteNode->NodePosX = X;
953
+ RerouteNode->NodePosY = Y;
954
+
955
+ NodeCreator.Finalize();
956
+
957
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
958
+
959
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
960
+ Result->SetStringField(TEXT("nodeId"), RerouteNode->NodeGuid.ToString());
961
+ SendAutomationResponse(RequestingSocket, RequestId, true,
962
+ TEXT("Reroute node created."), Result);
963
+ return true;
964
+ } else if (SubAction == TEXT("set_node_property")) {
965
+ const FScopedTransaction Transaction(
966
+ FText::FromString(TEXT("Set Blueprint Node Property")));
967
+ Blueprint->Modify();
968
+ TargetGraph->Modify();
969
+
970
+ // Generic property setter for common node properties used by tools.
971
+ FString NodeId;
972
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
973
+ FString PropertyName;
974
+ Payload->TryGetStringField(TEXT("propertyName"), PropertyName);
975
+ FString Value;
976
+ Payload->TryGetStringField(TEXT("value"), Value);
977
+
978
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
979
+
980
+ if (TargetNode) {
981
+ TargetNode->Modify();
982
+ bool bHandled = false;
983
+
984
+ if (PropertyName.Equals(TEXT("Comment"), ESearchCase::IgnoreCase) ||
985
+ PropertyName.Equals(TEXT("NodeComment"), ESearchCase::IgnoreCase)) {
986
+ TargetNode->NodeComment = Value;
987
+ bHandled = true;
988
+ } else if (PropertyName.Equals(TEXT("X"), ESearchCase::IgnoreCase) ||
989
+ PropertyName.Equals(TEXT("NodePosX"),
990
+ ESearchCase::IgnoreCase)) {
991
+ double NumValue = 0.0;
992
+ if (!Payload->TryGetNumberField(TEXT("value"), NumValue)) {
993
+ NumValue = FCString::Atod(*Value);
994
+ }
995
+ TargetNode->NodePosX = static_cast<float>(NumValue);
996
+ bHandled = true;
997
+ } else if (PropertyName.Equals(TEXT("Y"), ESearchCase::IgnoreCase) ||
998
+ PropertyName.Equals(TEXT("NodePosY"),
999
+ ESearchCase::IgnoreCase)) {
1000
+ double NumValue = 0.0;
1001
+ if (!Payload->TryGetNumberField(TEXT("value"), NumValue)) {
1002
+ NumValue = FCString::Atod(*Value);
1003
+ }
1004
+ TargetNode->NodePosY = static_cast<float>(NumValue);
1005
+ bHandled = true;
1006
+ } else if (PropertyName.Equals(TEXT("bCommentBubbleVisible"),
1007
+ ESearchCase::IgnoreCase)) {
1008
+ TargetNode->bCommentBubbleVisible = Value.ToBool();
1009
+ bHandled = true;
1010
+ } else if (PropertyName.Equals(TEXT("bCommentBubblePinned"),
1011
+ ESearchCase::IgnoreCase)) {
1012
+ TargetNode->bCommentBubblePinned = Value.ToBool();
1013
+ bHandled = true;
1014
+ }
1015
+
1016
+ if (bHandled) {
1017
+ TargetGraph->NotifyGraphChanged();
1018
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint);
1019
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1020
+ TEXT("Node property updated."));
1021
+ } else {
1022
+ SendAutomationError(
1023
+ RequestingSocket, RequestId,
1024
+ FString::Printf(TEXT("Unsupported node property '%s'"),
1025
+ *PropertyName),
1026
+ TEXT("PROPERTY_NOT_SUPPORTED"));
1027
+ }
1028
+ } else {
1029
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1030
+ TEXT("NODE_NOT_FOUND"));
1031
+ }
1032
+ return true;
1033
+ } else if (SubAction == TEXT("get_node_details")) {
1034
+ FString NodeId;
1035
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
1036
+
1037
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
1038
+
1039
+ if (TargetNode) {
1040
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1041
+ Result->SetStringField(TEXT("nodeName"), TargetNode->GetName());
1042
+ Result->SetStringField(
1043
+ TEXT("nodeTitle"),
1044
+ TargetNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
1045
+ Result->SetStringField(TEXT("nodeComment"), TargetNode->NodeComment);
1046
+ Result->SetNumberField(TEXT("x"), TargetNode->NodePosX);
1047
+ Result->SetNumberField(TEXT("y"), TargetNode->NodePosY);
1048
+
1049
+ TArray<TSharedPtr<FJsonValue>> Pins;
1050
+ for (UEdGraphPin *Pin : TargetNode->Pins) {
1051
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
1052
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
1053
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
1054
+ ? TEXT("Input")
1055
+ : TEXT("Output"));
1056
+ PinObj->SetStringField(TEXT("pinType"),
1057
+ Pin->PinType.PinCategory.ToString());
1058
+ Pins.Add(MakeShared<FJsonValueObject>(PinObj));
1059
+ }
1060
+ Result->SetArrayField(TEXT("pins"), Pins);
1061
+
1062
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1063
+ TEXT("Node details retrieved."), Result);
1064
+ } else {
1065
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1066
+ TEXT("NODE_NOT_FOUND"));
1067
+ }
1068
+ return true;
1069
+ } else if (SubAction == TEXT("get_graph_details")) {
1070
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1071
+ Result->SetStringField(TEXT("graphName"), TargetGraph->GetName());
1072
+ Result->SetNumberField(TEXT("nodeCount"), TargetGraph->Nodes.Num());
1073
+
1074
+ TArray<TSharedPtr<FJsonValue>> Nodes;
1075
+ for (UEdGraphNode *Node : TargetGraph->Nodes) {
1076
+ TSharedPtr<FJsonObject> NodeObj = MakeShared<FJsonObject>();
1077
+ NodeObj->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
1078
+ NodeObj->SetStringField(TEXT("nodeName"), Node->GetName());
1079
+ NodeObj->SetStringField(
1080
+ TEXT("nodeTitle"),
1081
+ Node->GetNodeTitle(ENodeTitleType::ListView).ToString());
1082
+ Nodes.Add(MakeShared<FJsonValueObject>(NodeObj));
1083
+ }
1084
+ Result->SetArrayField(TEXT("nodes"), Nodes);
1085
+
1086
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1087
+ TEXT("Graph details retrieved."), Result);
1088
+ return true;
1089
+ } else if (SubAction == TEXT("get_pin_details")) {
1090
+ FString NodeId;
1091
+ Payload->TryGetStringField(TEXT("nodeId"), NodeId);
1092
+ FString PinName;
1093
+ Payload->TryGetStringField(TEXT("pinName"), PinName);
1094
+
1095
+ UEdGraphNode *TargetNode = FindNodeByIdOrName(NodeId);
1096
+
1097
+ if (!TargetNode) {
1098
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Node not found."),
1099
+ TEXT("NODE_NOT_FOUND"));
1100
+ return true;
1101
+ }
1102
+
1103
+ TArray<UEdGraphPin *> PinsToReport;
1104
+ if (!PinName.IsEmpty()) {
1105
+ UEdGraphPin *Pin = TargetNode->FindPin(*PinName);
1106
+ if (!Pin) {
1107
+ SendAutomationError(RequestingSocket, RequestId, TEXT("Pin not found."),
1108
+ TEXT("PIN_NOT_FOUND"));
1109
+ return true;
1110
+ }
1111
+ PinsToReport.Add(Pin);
1112
+ } else {
1113
+ PinsToReport = TargetNode->Pins;
1114
+ }
1115
+
1116
+ TSharedPtr<FJsonObject> Result = MakeShared<FJsonObject>();
1117
+ Result->SetStringField(TEXT("nodeId"), NodeId);
1118
+
1119
+ TArray<TSharedPtr<FJsonValue>> PinsJson;
1120
+ for (UEdGraphPin *Pin : PinsToReport) {
1121
+ if (!Pin) {
1122
+ continue;
1123
+ }
1124
+
1125
+ TSharedPtr<FJsonObject> PinObj = MakeShared<FJsonObject>();
1126
+ PinObj->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
1127
+ PinObj->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input
1128
+ ? TEXT("Input")
1129
+ : TEXT("Output"));
1130
+ PinObj->SetStringField(TEXT("pinType"),
1131
+ Pin->PinType.PinCategory.ToString());
1132
+
1133
+ if (Pin->LinkedTo.Num() > 0) {
1134
+ TArray<TSharedPtr<FJsonValue>> LinkedArray;
1135
+ for (UEdGraphPin *LinkedPin : Pin->LinkedTo) {
1136
+ if (!LinkedPin) {
1137
+ continue;
1138
+ }
1139
+ FString LinkedNodeId =
1140
+ LinkedPin->GetOwningNode()
1141
+ ? LinkedPin->GetOwningNode()->NodeGuid.ToString()
1142
+ : FString();
1143
+ const FString LinkedLabel =
1144
+ LinkedNodeId.IsEmpty()
1145
+ ? LinkedPin->PinName.ToString()
1146
+ : FString::Printf(TEXT("%s:%s"), *LinkedNodeId,
1147
+ *LinkedPin->PinName.ToString());
1148
+ LinkedArray.Add(MakeShared<FJsonValueString>(LinkedLabel));
1149
+ }
1150
+ PinObj->SetArrayField(TEXT("linkedTo"), LinkedArray);
1151
+ }
1152
+
1153
+ if (!Pin->DefaultValue.IsEmpty()) {
1154
+ PinObj->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
1155
+ } else if (!Pin->DefaultTextValue.IsEmptyOrWhitespace()) {
1156
+ PinObj->SetStringField(TEXT("defaultTextValue"),
1157
+ Pin->DefaultTextValue.ToString());
1158
+ } else if (Pin->DefaultObject) {
1159
+ PinObj->SetStringField(TEXT("defaultObjectPath"),
1160
+ Pin->DefaultObject->GetPathName());
1161
+ }
1162
+
1163
+ PinsJson.Add(MakeShared<FJsonValueObject>(PinObj));
1164
+ }
1165
+
1166
+ Result->SetArrayField(TEXT("pins"), PinsJson);
1167
+
1168
+ SendAutomationResponse(RequestingSocket, RequestId, true,
1169
+ TEXT("Pin details retrieved."), Result);
1170
+ return true;
1171
+ }
1172
+
1173
+ SendAutomationError(
1174
+ RequestingSocket, RequestId,
1175
+ FString::Printf(TEXT("Unknown subAction: %s"), *SubAction),
1176
+ TEXT("INVALID_SUBACTION"));
1177
+ return true;
1178
+ #else
1179
+ SendAutomationError(RequestingSocket, RequestId,
1180
+ TEXT("Blueprint graph actions are editor-only."),
1181
+ TEXT("EDITOR_ONLY"));
1182
+ return true;
1183
+ #endif
1184
+ }