unreal-engine-mcp-server 0.4.7 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (438) hide show
  1. package/.env.example +26 -0
  2. package/.env.production +38 -7
  3. package/.eslintrc.json +0 -54
  4. package/.eslintrc.override.json +8 -0
  5. package/.github/ISSUE_TEMPLATE/bug_report.yml +94 -0
  6. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  7. package/.github/ISSUE_TEMPLATE/feature_request.yml +56 -0
  8. package/.github/copilot-instructions.md +478 -45
  9. package/.github/dependabot.yml +19 -0
  10. package/.github/labeler.yml +24 -0
  11. package/.github/labels.yml +70 -0
  12. package/.github/pull_request_template.md +42 -0
  13. package/.github/release-drafter.yml +148 -0
  14. package/.github/workflows/auto-merge.yml +38 -0
  15. package/.github/workflows/ci.yml +38 -0
  16. package/.github/workflows/dependency-review.yml +17 -0
  17. package/.github/workflows/gemini-issue-triage.yml +172 -0
  18. package/.github/workflows/greetings.yml +23 -0
  19. package/.github/workflows/labeler.yml +16 -0
  20. package/.github/workflows/links.yml +80 -0
  21. package/.github/workflows/pr-size-labeler.yml +137 -0
  22. package/.github/workflows/publish-mcp.yml +12 -7
  23. package/.github/workflows/release-drafter.yml +23 -0
  24. package/.github/workflows/release.yml +112 -0
  25. package/.github/workflows/semantic-pull-request.yml +35 -0
  26. package/.github/workflows/smoke-test.yml +36 -0
  27. package/.github/workflows/stale.yml +28 -0
  28. package/CHANGELOG.md +267 -31
  29. package/CONTRIBUTING.md +140 -0
  30. package/README.md +166 -71
  31. package/claude_desktop_config_example.json +7 -6
  32. package/dist/automation/bridge.d.ts +50 -0
  33. package/dist/automation/bridge.js +452 -0
  34. package/dist/automation/connection-manager.d.ts +23 -0
  35. package/dist/automation/connection-manager.js +107 -0
  36. package/dist/automation/handshake.d.ts +11 -0
  37. package/dist/automation/handshake.js +89 -0
  38. package/dist/automation/index.d.ts +3 -0
  39. package/dist/automation/index.js +3 -0
  40. package/dist/automation/message-handler.d.ts +12 -0
  41. package/dist/automation/message-handler.js +149 -0
  42. package/dist/automation/request-tracker.d.ts +25 -0
  43. package/dist/automation/request-tracker.js +98 -0
  44. package/dist/automation/types.d.ts +130 -0
  45. package/dist/automation/types.js +2 -0
  46. package/dist/cli.js +32 -5
  47. package/dist/config.d.ts +27 -0
  48. package/dist/config.js +60 -0
  49. package/dist/constants.d.ts +12 -0
  50. package/dist/constants.js +12 -0
  51. package/dist/graphql/resolvers.d.ts +268 -0
  52. package/dist/graphql/resolvers.js +743 -0
  53. package/dist/graphql/schema.d.ts +5 -0
  54. package/dist/graphql/schema.js +437 -0
  55. package/dist/graphql/server.d.ts +26 -0
  56. package/dist/graphql/server.js +115 -0
  57. package/dist/graphql/types.d.ts +7 -0
  58. package/dist/graphql/types.js +2 -0
  59. package/dist/handlers/resource-handlers.d.ts +20 -0
  60. package/dist/handlers/resource-handlers.js +180 -0
  61. package/dist/index.d.ts +31 -18
  62. package/dist/index.js +119 -619
  63. package/dist/prompts/index.js +4 -4
  64. package/dist/resources/actors.d.ts +17 -12
  65. package/dist/resources/actors.js +56 -76
  66. package/dist/resources/assets.d.ts +6 -14
  67. package/dist/resources/assets.js +115 -147
  68. package/dist/resources/levels.d.ts +13 -13
  69. package/dist/resources/levels.js +25 -34
  70. package/dist/server/resource-registry.d.ts +20 -0
  71. package/dist/server/resource-registry.js +37 -0
  72. package/dist/server/tool-registry.d.ts +23 -0
  73. package/dist/server/tool-registry.js +322 -0
  74. package/dist/server-setup.d.ts +21 -0
  75. package/dist/server-setup.js +111 -0
  76. package/dist/services/health-monitor.d.ts +34 -0
  77. package/dist/services/health-monitor.js +105 -0
  78. package/dist/services/metrics-server.d.ts +11 -0
  79. package/dist/services/metrics-server.js +105 -0
  80. package/dist/tools/actors.d.ts +147 -9
  81. package/dist/tools/actors.js +350 -311
  82. package/dist/tools/animation.d.ts +135 -4
  83. package/dist/tools/animation.js +510 -411
  84. package/dist/tools/assets.d.ts +117 -19
  85. package/dist/tools/assets.js +259 -284
  86. package/dist/tools/audio.d.ts +102 -42
  87. package/dist/tools/audio.js +272 -685
  88. package/dist/tools/base-tool.d.ts +17 -0
  89. package/dist/tools/base-tool.js +46 -0
  90. package/dist/tools/behavior-tree.d.ts +94 -0
  91. package/dist/tools/behavior-tree.js +39 -0
  92. package/dist/tools/blueprint/helpers.d.ts +29 -0
  93. package/dist/tools/blueprint/helpers.js +182 -0
  94. package/dist/tools/blueprint.d.ts +228 -118
  95. package/dist/tools/blueprint.js +685 -832
  96. package/dist/tools/consolidated-tool-definitions.d.ts +5462 -1781
  97. package/dist/tools/consolidated-tool-definitions.js +829 -496
  98. package/dist/tools/consolidated-tool-handlers.d.ts +2 -1
  99. package/dist/tools/consolidated-tool-handlers.js +211 -1026
  100. package/dist/tools/debug.d.ts +143 -85
  101. package/dist/tools/debug.js +234 -180
  102. package/dist/tools/dynamic-handler-registry.d.ts +11 -0
  103. package/dist/tools/dynamic-handler-registry.js +101 -0
  104. package/dist/tools/editor.d.ts +139 -18
  105. package/dist/tools/editor.js +239 -244
  106. package/dist/tools/engine.d.ts +10 -4
  107. package/dist/tools/engine.js +13 -5
  108. package/dist/tools/environment.d.ts +36 -0
  109. package/dist/tools/environment.js +267 -0
  110. package/dist/tools/foliage.d.ts +105 -14
  111. package/dist/tools/foliage.js +219 -331
  112. package/dist/tools/handlers/actor-handlers.d.ts +3 -0
  113. package/dist/tools/handlers/actor-handlers.js +232 -0
  114. package/dist/tools/handlers/animation-handlers.d.ts +3 -0
  115. package/dist/tools/handlers/animation-handlers.js +185 -0
  116. package/dist/tools/handlers/argument-helper.d.ts +16 -0
  117. package/dist/tools/handlers/argument-helper.js +80 -0
  118. package/dist/tools/handlers/asset-handlers.d.ts +3 -0
  119. package/dist/tools/handlers/asset-handlers.js +496 -0
  120. package/dist/tools/handlers/audio-handlers.d.ts +3 -0
  121. package/dist/tools/handlers/audio-handlers.js +166 -0
  122. package/dist/tools/handlers/blueprint-handlers.d.ts +4 -0
  123. package/dist/tools/handlers/blueprint-handlers.js +358 -0
  124. package/dist/tools/handlers/common-handlers.d.ts +14 -0
  125. package/dist/tools/handlers/common-handlers.js +56 -0
  126. package/dist/tools/handlers/editor-handlers.d.ts +3 -0
  127. package/dist/tools/handlers/editor-handlers.js +119 -0
  128. package/dist/tools/handlers/effect-handlers.d.ts +3 -0
  129. package/dist/tools/handlers/effect-handlers.js +171 -0
  130. package/dist/tools/handlers/environment-handlers.d.ts +3 -0
  131. package/dist/tools/handlers/environment-handlers.js +170 -0
  132. package/dist/tools/handlers/graph-handlers.d.ts +3 -0
  133. package/dist/tools/handlers/graph-handlers.js +90 -0
  134. package/dist/tools/handlers/input-handlers.d.ts +3 -0
  135. package/dist/tools/handlers/input-handlers.js +21 -0
  136. package/dist/tools/handlers/inspect-handlers.d.ts +3 -0
  137. package/dist/tools/handlers/inspect-handlers.js +383 -0
  138. package/dist/tools/handlers/level-handlers.d.ts +3 -0
  139. package/dist/tools/handlers/level-handlers.js +237 -0
  140. package/dist/tools/handlers/lighting-handlers.d.ts +3 -0
  141. package/dist/tools/handlers/lighting-handlers.js +144 -0
  142. package/dist/tools/handlers/performance-handlers.d.ts +3 -0
  143. package/dist/tools/handlers/performance-handlers.js +130 -0
  144. package/dist/tools/handlers/pipeline-handlers.d.ts +3 -0
  145. package/dist/tools/handlers/pipeline-handlers.js +110 -0
  146. package/dist/tools/handlers/sequence-handlers.d.ts +3 -0
  147. package/dist/tools/handlers/sequence-handlers.js +376 -0
  148. package/dist/tools/handlers/system-handlers.d.ts +4 -0
  149. package/dist/tools/handlers/system-handlers.js +506 -0
  150. package/dist/tools/input.d.ts +19 -0
  151. package/dist/tools/input.js +89 -0
  152. package/dist/tools/introspection.d.ts +103 -40
  153. package/dist/tools/introspection.js +425 -568
  154. package/dist/tools/landscape.d.ts +97 -36
  155. package/dist/tools/landscape.js +280 -409
  156. package/dist/tools/level.d.ts +130 -10
  157. package/dist/tools/level.js +639 -675
  158. package/dist/tools/lighting.d.ts +77 -38
  159. package/dist/tools/lighting.js +441 -943
  160. package/dist/tools/logs.d.ts +3 -3
  161. package/dist/tools/logs.js +5 -57
  162. package/dist/tools/materials.d.ts +91 -24
  163. package/dist/tools/materials.js +190 -118
  164. package/dist/tools/niagara.d.ts +149 -39
  165. package/dist/tools/niagara.js +232 -182
  166. package/dist/tools/performance.d.ts +27 -12
  167. package/dist/tools/performance.js +204 -122
  168. package/dist/tools/physics.d.ts +32 -77
  169. package/dist/tools/physics.js +171 -582
  170. package/dist/tools/property-dictionary.d.ts +13 -0
  171. package/dist/tools/property-dictionary.js +82 -0
  172. package/dist/tools/sequence.d.ts +73 -48
  173. package/dist/tools/sequence.js +196 -748
  174. package/dist/tools/tool-definition-utils.d.ts +59 -0
  175. package/dist/tools/tool-definition-utils.js +35 -0
  176. package/dist/tools/ui.d.ts +66 -34
  177. package/dist/tools/ui.js +134 -214
  178. package/dist/types/env.d.ts +0 -3
  179. package/dist/types/env.js +0 -7
  180. package/dist/types/tool-interfaces.d.ts +898 -0
  181. package/dist/types/tool-interfaces.js +2 -0
  182. package/dist/types/tool-types.d.ts +183 -19
  183. package/dist/types/tool-types.js +0 -4
  184. package/dist/unreal-bridge.d.ts +24 -131
  185. package/dist/unreal-bridge.js +364 -1506
  186. package/dist/utils/command-validator.d.ts +9 -0
  187. package/dist/utils/command-validator.js +67 -0
  188. package/dist/utils/elicitation.d.ts +1 -1
  189. package/dist/utils/elicitation.js +12 -15
  190. package/dist/utils/error-handler.d.ts +2 -51
  191. package/dist/utils/error-handler.js +11 -87
  192. package/dist/utils/ini-reader.d.ts +3 -0
  193. package/dist/utils/ini-reader.js +69 -0
  194. package/dist/utils/logger.js +9 -6
  195. package/dist/utils/normalize.d.ts +3 -0
  196. package/dist/utils/normalize.js +56 -0
  197. package/dist/utils/response-factory.d.ts +7 -0
  198. package/dist/utils/response-factory.js +33 -0
  199. package/dist/utils/response-validator.d.ts +3 -24
  200. package/dist/utils/response-validator.js +130 -81
  201. package/dist/utils/result-helpers.d.ts +4 -5
  202. package/dist/utils/result-helpers.js +15 -16
  203. package/dist/utils/safe-json.js +5 -11
  204. package/dist/utils/unreal-command-queue.d.ts +24 -0
  205. package/dist/utils/unreal-command-queue.js +120 -0
  206. package/dist/utils/validation.d.ts +0 -40
  207. package/dist/utils/validation.js +1 -78
  208. package/dist/wasm/index.d.ts +70 -0
  209. package/dist/wasm/index.js +535 -0
  210. package/docs/GraphQL-API.md +888 -0
  211. package/docs/Migration-Guide-v0.5.0.md +692 -0
  212. package/docs/Roadmap.md +53 -0
  213. package/docs/WebAssembly-Integration.md +628 -0
  214. package/docs/editor-plugin-extension.md +370 -0
  215. package/docs/handler-mapping.md +242 -0
  216. package/docs/native-automation-progress.md +128 -0
  217. package/docs/testing-guide.md +423 -0
  218. package/mcp-config-example.json +6 -6
  219. package/package.json +60 -27
  220. package/plugins/McpAutomationBridge/Config/FilterPlugin.ini +8 -0
  221. package/plugins/McpAutomationBridge/McpAutomationBridge.uplugin +64 -0
  222. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/McpAutomationBridge.Build.cs +189 -0
  223. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.cpp +22 -0
  224. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeGlobals.h +30 -0
  225. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeHelpers.h +1983 -0
  226. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeModule.cpp +72 -0
  227. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSettings.cpp +46 -0
  228. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridgeSubsystem.cpp +581 -0
  229. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AnimationHandlers.cpp +2394 -0
  230. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetQueryHandlers.cpp +300 -0
  231. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AssetWorkflowHandlers.cpp +2807 -0
  232. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_AudioHandlers.cpp +1087 -0
  233. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BehaviorTreeHandlers.cpp +488 -0
  234. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.cpp +643 -0
  235. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintCreationHandlers.h +31 -0
  236. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintGraphHandlers.cpp +1184 -0
  237. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers.cpp +5652 -0
  238. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_BlueprintHandlers_List.cpp +152 -0
  239. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ControlHandlers.cpp +2614 -0
  240. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_DebugHandlers.cpp +42 -0
  241. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EditorFunctionHandlers.cpp +1237 -0
  242. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EffectHandlers.cpp +1701 -0
  243. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_EnvironmentHandlers.cpp +2145 -0
  244. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_FoliageHandlers.cpp +954 -0
  245. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InputHandlers.cpp +209 -0
  246. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_InsightsHandlers.cpp +41 -0
  247. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LandscapeHandlers.cpp +1164 -0
  248. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LevelHandlers.cpp +762 -0
  249. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LightingHandlers.cpp +634 -0
  250. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_LogHandlers.cpp +136 -0
  251. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_MaterialGraphHandlers.cpp +494 -0
  252. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraGraphHandlers.cpp +278 -0
  253. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_NiagaraHandlers.cpp +625 -0
  254. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PerformanceHandlers.cpp +401 -0
  255. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PipelineHandlers.cpp +67 -0
  256. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_ProcessRequest.cpp +735 -0
  257. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_PropertyHandlers.cpp +2634 -0
  258. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_RenderHandlers.cpp +189 -0
  259. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.cpp +917 -0
  260. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SCSHandlers.h +39 -0
  261. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequenceHandlers.cpp +2670 -0
  262. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_SequencerHandlers.cpp +519 -0
  263. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_TestHandlers.cpp +38 -0
  264. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_UiHandlers.cpp +668 -0
  265. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpAutomationBridge_WorldPartitionHandlers.cpp +346 -0
  266. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.cpp +1330 -0
  267. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpBridgeWebSocket.h +149 -0
  268. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Private/McpConnectionManager.cpp +783 -0
  269. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSettings.h +115 -0
  270. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpAutomationBridgeSubsystem.h +796 -0
  271. package/plugins/McpAutomationBridge/Source/McpAutomationBridge/Public/McpConnectionManager.h +117 -0
  272. package/scripts/check-unreal-connection.mjs +19 -0
  273. package/scripts/clean-tmp.js +23 -0
  274. package/scripts/patch-wasm.js +26 -0
  275. package/scripts/run-all-tests.mjs +131 -0
  276. package/scripts/smoke-test.ts +94 -0
  277. package/scripts/sync-mcp-plugin.js +143 -0
  278. package/scripts/test-no-plugin-alternates.mjs +113 -0
  279. package/scripts/validate-server.js +46 -0
  280. package/scripts/verify-automation-bridge.js +200 -0
  281. package/server.json +57 -21
  282. package/src/automation/bridge.ts +558 -0
  283. package/src/automation/connection-manager.ts +130 -0
  284. package/src/automation/handshake.ts +99 -0
  285. package/src/automation/index.ts +2 -0
  286. package/src/automation/message-handler.ts +167 -0
  287. package/src/automation/request-tracker.ts +123 -0
  288. package/src/automation/types.ts +107 -0
  289. package/src/cli.ts +33 -6
  290. package/src/config.ts +73 -0
  291. package/src/constants.ts +12 -0
  292. package/src/graphql/resolvers.ts +1010 -0
  293. package/src/graphql/schema.ts +452 -0
  294. package/src/graphql/server.ts +154 -0
  295. package/src/graphql/types.ts +7 -0
  296. package/src/handlers/resource-handlers.ts +186 -0
  297. package/src/index.ts +152 -663
  298. package/src/prompts/index.ts +4 -4
  299. package/src/resources/actors.ts +58 -76
  300. package/src/resources/assets.ts +147 -134
  301. package/src/resources/levels.ts +28 -33
  302. package/src/server/resource-registry.ts +47 -0
  303. package/src/server/tool-registry.ts +354 -0
  304. package/src/server-setup.ts +148 -0
  305. package/src/services/health-monitor.ts +132 -0
  306. package/src/services/metrics-server.ts +142 -0
  307. package/src/tools/actors.ts +417 -322
  308. package/src/tools/animation.ts +671 -461
  309. package/src/tools/assets.ts +353 -289
  310. package/src/tools/audio.ts +323 -766
  311. package/src/tools/base-tool.ts +52 -0
  312. package/src/tools/behavior-tree.ts +45 -0
  313. package/src/tools/blueprint/helpers.ts +189 -0
  314. package/src/tools/blueprint.ts +787 -965
  315. package/src/tools/consolidated-tool-definitions.ts +993 -515
  316. package/src/tools/consolidated-tool-handlers.ts +272 -1139
  317. package/src/tools/debug.ts +292 -187
  318. package/src/tools/dynamic-handler-registry.ts +151 -0
  319. package/src/tools/editor.ts +309 -246
  320. package/src/tools/engine.ts +14 -3
  321. package/src/tools/environment.ts +287 -0
  322. package/src/tools/foliage.ts +314 -379
  323. package/src/tools/handlers/actor-handlers.ts +271 -0
  324. package/src/tools/handlers/animation-handlers.ts +237 -0
  325. package/src/tools/handlers/argument-helper.ts +142 -0
  326. package/src/tools/handlers/asset-handlers.ts +532 -0
  327. package/src/tools/handlers/audio-handlers.ts +194 -0
  328. package/src/tools/handlers/blueprint-handlers.ts +380 -0
  329. package/src/tools/handlers/common-handlers.ts +87 -0
  330. package/src/tools/handlers/editor-handlers.ts +123 -0
  331. package/src/tools/handlers/effect-handlers.ts +220 -0
  332. package/src/tools/handlers/environment-handlers.ts +183 -0
  333. package/src/tools/handlers/graph-handlers.ts +116 -0
  334. package/src/tools/handlers/input-handlers.ts +28 -0
  335. package/src/tools/handlers/inspect-handlers.ts +450 -0
  336. package/src/tools/handlers/level-handlers.ts +252 -0
  337. package/src/tools/handlers/lighting-handlers.ts +147 -0
  338. package/src/tools/handlers/performance-handlers.ts +132 -0
  339. package/src/tools/handlers/pipeline-handlers.ts +127 -0
  340. package/src/tools/handlers/sequence-handlers.ts +415 -0
  341. package/src/tools/handlers/system-handlers.ts +564 -0
  342. package/src/tools/input.ts +101 -0
  343. package/src/tools/introspection.ts +493 -584
  344. package/src/tools/landscape.ts +394 -489
  345. package/src/tools/level.ts +752 -694
  346. package/src/tools/lighting.ts +583 -984
  347. package/src/tools/logs.ts +9 -57
  348. package/src/tools/materials.ts +231 -121
  349. package/src/tools/niagara.ts +293 -168
  350. package/src/tools/performance.ts +320 -168
  351. package/src/tools/physics.ts +268 -613
  352. package/src/tools/property-dictionary.ts +98 -0
  353. package/src/tools/sequence.ts +255 -815
  354. package/src/tools/tool-definition-utils.ts +35 -0
  355. package/src/tools/ui.ts +207 -283
  356. package/src/types/env.ts +0 -10
  357. package/src/types/tool-interfaces.ts +250 -0
  358. package/src/types/tool-types.ts +243 -21
  359. package/src/unreal-bridge.ts +460 -1550
  360. package/src/utils/command-validator.ts +75 -0
  361. package/src/utils/elicitation.ts +10 -7
  362. package/src/utils/error-handler.ts +14 -90
  363. package/src/utils/ini-reader.ts +86 -0
  364. package/src/utils/logger.ts +8 -3
  365. package/src/utils/normalize.ts +60 -0
  366. package/src/utils/response-factory.ts +39 -0
  367. package/src/utils/response-validator.ts +176 -56
  368. package/src/utils/result-helpers.ts +21 -19
  369. package/src/utils/safe-json.ts +14 -11
  370. package/src/utils/unreal-command-queue.ts +152 -0
  371. package/src/utils/validation.ts +4 -1
  372. package/src/wasm/index.ts +838 -0
  373. package/test-server.mjs +100 -0
  374. package/tests/run-unreal-tool-tests.mjs +242 -14
  375. package/tests/test-animation.mjs +44 -0
  376. package/tests/test-asset-advanced.mjs +82 -0
  377. package/tests/test-asset-errors.mjs +35 -0
  378. package/tests/test-audio.mjs +219 -0
  379. package/tests/test-automation-timeouts.mjs +98 -0
  380. package/tests/test-behavior-tree.mjs +261 -0
  381. package/tests/test-blueprint-events.mjs +35 -0
  382. package/tests/test-blueprint-graph.mjs +79 -0
  383. package/tests/test-blueprint.mjs +577 -0
  384. package/tests/test-client-mode.mjs +86 -0
  385. package/tests/test-console-command.mjs +56 -0
  386. package/tests/test-control-actor.mjs +425 -0
  387. package/tests/test-control-editor.mjs +80 -0
  388. package/tests/test-extra-tools.mjs +38 -0
  389. package/tests/test-graphql.mjs +322 -0
  390. package/tests/test-inspect.mjs +72 -0
  391. package/tests/test-landscape.mjs +60 -0
  392. package/tests/test-manage-asset.mjs +438 -0
  393. package/tests/test-manage-level.mjs +70 -0
  394. package/tests/test-materials.mjs +356 -0
  395. package/tests/test-niagara.mjs +185 -0
  396. package/tests/test-no-inline-python.mjs +122 -0
  397. package/tests/test-plugin-handshake.mjs +82 -0
  398. package/tests/test-render.mjs +33 -0
  399. package/tests/test-runner.mjs +933 -0
  400. package/tests/test-search-assets.mjs +66 -0
  401. package/tests/test-sequence.mjs +68 -0
  402. package/tests/test-system.mjs +57 -0
  403. package/tests/test-wasm.mjs +193 -0
  404. package/tests/test-world-partition.mjs +215 -0
  405. package/tsconfig.json +3 -3
  406. package/wasm/Cargo.lock +363 -0
  407. package/wasm/Cargo.toml +42 -0
  408. package/wasm/LICENSE +21 -0
  409. package/wasm/README.md +253 -0
  410. package/wasm/src/dependency_resolver.rs +377 -0
  411. package/wasm/src/lib.rs +153 -0
  412. package/wasm/src/property_parser.rs +271 -0
  413. package/wasm/src/transform_math.rs +396 -0
  414. package/wasm/tests/integration.rs +109 -0
  415. package/.github/workflows/smithery-build.yml +0 -29
  416. package/dist/tools/build_environment_advanced.d.ts +0 -65
  417. package/dist/tools/build_environment_advanced.js +0 -633
  418. package/dist/tools/rc.d.ts +0 -110
  419. package/dist/tools/rc.js +0 -437
  420. package/dist/tools/visual.d.ts +0 -40
  421. package/dist/tools/visual.js +0 -282
  422. package/dist/utils/http.d.ts +0 -6
  423. package/dist/utils/http.js +0 -151
  424. package/dist/utils/python-output.d.ts +0 -18
  425. package/dist/utils/python-output.js +0 -290
  426. package/dist/utils/python.d.ts +0 -2
  427. package/dist/utils/python.js +0 -4
  428. package/dist/utils/stdio-redirect.d.ts +0 -2
  429. package/dist/utils/stdio-redirect.js +0 -20
  430. package/docs/unreal-tool-test-cases.md +0 -574
  431. package/smithery.yaml +0 -29
  432. package/src/tools/build_environment_advanced.ts +0 -732
  433. package/src/tools/rc.ts +0 -515
  434. package/src/tools/visual.ts +0 -281
  435. package/src/utils/http.ts +0 -187
  436. package/src/utils/python-output.ts +0 -351
  437. package/src/utils/python.ts +0 -3
  438. package/src/utils/stdio-redirect.ts +0 -18
@@ -1,12 +1,14 @@
1
1
  import { UnrealBridge } from '../unreal-bridge.js';
2
2
  import { ensureRotation, ensureVector3 } from '../utils/validation.js';
3
- import { coerceString, coerceVector3, interpretStandardResult } from '../utils/result-helpers.js';
4
- import { escapePythonString } from '../utils/python.js';
3
+ import { BaseTool } from './base-tool.js';
4
+ import { IActorTools, StandardActionResponse } from '../types/tool-interfaces.js';
5
5
 
6
- export class ActorTools {
7
- constructor(private bridge: UnrealBridge) {}
6
+ export class ActorTools extends BaseTool implements IActorTools {
7
+ constructor(bridge: UnrealBridge) {
8
+ super(bridge);
9
+ }
8
10
 
9
- async spawn(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number }; actorName?: string }) {
11
+ async spawn(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number }; actorName?: string; meshPath?: string; timeoutMs?: number }) {
10
12
  if (!params.classPath || typeof params.classPath !== 'string' || params.classPath.trim().length === 0) {
11
13
  throw new Error(`Invalid classPath: ${params.classPath}`);
12
14
  }
@@ -16,7 +18,7 @@ export class ActorTools {
16
18
  if (params.actorName !== undefined && (!requestedActorName || requestedActorName.length === 0)) {
17
19
  throw new Error(`Invalid actorName: ${params.actorName}`);
18
20
  }
19
- const sanitizedActorName = requestedActorName?.replace(/[^A-Za-z0-9_-]/g, '_');
21
+ const sanitizedActorName = requestedActorName?.replace(/[^A-Za-z0-9_-]/g, '_');
20
22
  const lowerName = className.toLowerCase();
21
23
 
22
24
  const shapeMapping: Record<string, string> = {
@@ -39,315 +41,152 @@ export class ActorTools {
39
41
  'actor rotation'
40
42
  );
41
43
 
42
- const escapedResolvedClassPath = escapePythonString(mappedClassPath);
43
- const escapedRequestedPath = escapePythonString(className);
44
- const escapedRequestedActorName = sanitizedActorName ? escapePythonString(sanitizedActorName) : '';
45
-
46
- const pythonCmd = `
47
- import unreal
48
- import json
49
- import time
50
-
51
- result = {
52
- "success": False,
53
- "message": "",
54
- "error": "",
55
- "actorName": "",
56
- "requestedClass": "${escapedRequestedPath}",
57
- "resolvedClass": "${escapedResolvedClassPath}",
58
- "location": [${locX}, ${locY}, ${locZ}],
59
- "rotation": [${rotPitch}, ${rotYaw}, ${rotRoll}],
60
- "requestedActorName": "${escapedRequestedActorName}",
61
- "warnings": [],
62
- "details": []
63
- }
64
-
65
- ${this.getPythonSpawnHelper()}
66
-
67
- abstract_classes = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor', 'Pawn', 'Character']
68
-
69
- def finalize():
70
- data = dict(result)
71
- if data.get("success"):
72
- if not data.get("message"):
73
- data["message"] = "Actor spawned successfully"
74
- data.pop("error", None)
75
- else:
76
- if not data.get("error"):
77
- data["error"] = data.get("message") or "Failed to spawn actor"
78
- if not data.get("message"):
79
- data["message"] = data["error"]
80
- if not data.get("warnings"):
81
- data.pop("warnings", None)
82
- if not data.get("details"):
83
- data.pop("details", None)
84
- return data
85
-
86
- try:
87
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
88
- if les and les.is_in_play_in_editor():
89
- result["message"] = "Cannot spawn actors while in Play In Editor mode. Please stop PIE first."
90
- result["error"] = result["message"]
91
- result["details"].append("Play In Editor mode detected")
92
- print('RESULT:' + json.dumps(finalize()))
93
- raise SystemExit(0)
94
- except SystemExit:
95
- raise
96
- except Exception:
97
- result["warnings"].append("Unable to determine Play In Editor state")
98
-
99
- if result["requestedClass"] in abstract_classes:
100
- result["message"] = f"Cannot spawn {result['requestedClass']}: class is abstract and cannot be instantiated"
101
- result["error"] = result["message"]
102
- else:
103
- try:
104
- class_path = result["resolvedClass"]
105
- requested_path = result["requestedClass"]
106
- location = unreal.Vector(${locX}, ${locY}, ${locZ})
107
- rotation = unreal.Rotator(${rotPitch}, ${rotYaw}, ${rotRoll})
108
- actor = None
109
-
110
- simple_name = requested_path.split('/')[-1] if '/' in requested_path else requested_path
111
- if '.' in simple_name:
112
- simple_name = simple_name.split('.')[-1]
113
- simple_name_lower = simple_name.lower()
114
- class_lookup_name = class_path.split('.')[-1] if '.' in class_path else simple_name
115
-
116
- result["details"].append(f"Attempting spawn using class path: {class_path}")
117
-
118
- if class_path.startswith('/Game') or class_path.startswith('/Engine'):
119
- try:
120
- asset = unreal.EditorAssetLibrary.load_asset(class_path)
121
- except Exception as asset_error:
122
- asset = None
123
- result["warnings"].append(f"Failed to load asset for {class_path}: {asset_error}")
124
- if asset:
125
- if isinstance(asset, unreal.Blueprint):
126
- try:
127
- actor_class = asset.generated_class()
128
- except Exception as blueprint_error:
129
- actor_class = None
130
- result["warnings"].append(f"Failed to resolve blueprint class: {blueprint_error}")
131
- if actor_class:
132
- actor = spawn_actor_from_class(actor_class, location, rotation)
133
- if actor:
134
- result["details"].append("Spawned using Blueprint generated class")
135
- elif isinstance(asset, unreal.StaticMesh):
136
- actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
137
- if actor:
138
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
139
- if mesh_component:
140
- mesh_component.set_static_mesh(asset)
141
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
142
- result["details"].append("Applied static mesh to spawned StaticMeshActor")
143
-
144
- if not actor:
145
- shape_map = {
146
- 'cube': '/Engine/BasicShapes/Cube',
147
- 'sphere': '/Engine/BasicShapes/Sphere',
148
- 'cylinder': '/Engine/BasicShapes/Cylinder',
149
- 'cone': '/Engine/BasicShapes/Cone',
150
- 'plane': '/Engine/BasicShapes/Plane',
151
- 'torus': '/Engine/BasicShapes/Torus'
152
- }
153
- mesh_path = shape_map.get(simple_name_lower)
154
- if not mesh_path and class_path.startswith('/Engine/BasicShapes'):
155
- mesh_path = class_path
156
- if mesh_path:
157
- try:
158
- shape_mesh = unreal.EditorAssetLibrary.load_asset(mesh_path)
159
- except Exception as shape_error:
160
- shape_mesh = None
161
- result["warnings"].append(f"Failed to load shape mesh {mesh_path}: {shape_error}")
162
- if shape_mesh:
163
- actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
164
- if actor:
165
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
166
- if mesh_component:
167
- mesh_component.set_static_mesh(shape_mesh)
168
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
169
- result["details"].append(f"Spawned StaticMeshActor with mesh {mesh_path}")
170
-
171
- if not actor:
172
- if class_lookup_name == "StaticMeshActor":
173
- actor = spawn_actor_from_class(unreal.StaticMeshActor, location, rotation)
174
- if actor:
175
- try:
176
- cube_mesh = unreal.EditorAssetLibrary.load_asset('/Engine/BasicShapes/Cube')
177
- except Exception as cube_error:
178
- cube_mesh = None
179
- result["warnings"].append(f"Failed to load default cube mesh: {cube_error}")
180
- if cube_mesh:
181
- mesh_component = actor.get_component_by_class(unreal.StaticMeshComponent)
182
- if mesh_component:
183
- mesh_component.set_static_mesh(cube_mesh)
184
- mesh_component.set_editor_property('mobility', unreal.ComponentMobility.MOVABLE)
185
- result["details"].append("Applied default cube mesh to StaticMeshActor")
186
- elif class_lookup_name == "CameraActor":
187
- actor = spawn_actor_from_class(unreal.CameraActor, location, rotation)
188
- if actor:
189
- result["details"].append("Spawned CameraActor via reflected class lookup")
190
- else:
191
- actor_class = getattr(unreal, class_lookup_name, None)
192
- if actor_class:
193
- actor = spawn_actor_from_class(actor_class, location, rotation)
194
- if actor:
195
- result["details"].append(f"Spawned {class_lookup_name} via reflected class lookup")
196
-
197
- if actor:
198
- desired_name = (result.get("requestedActorName") or "").strip()
199
- actor_name = ""
200
- if desired_name:
201
- try:
202
- try:
203
- actor.set_actor_label(desired_name, True)
204
- except TypeError:
205
- actor.set_actor_label(desired_name)
206
- actor_name = actor.get_actor_label() or desired_name
207
- except Exception as label_error:
208
- result["warnings"].append(f"Failed to honor requested actor name '{desired_name}': {label_error}")
209
- if not actor_name:
210
- timestamp = int(time.time() * 1000) % 10000
211
- base_name = simple_name or class_lookup_name or class_path.split('/')[-1]
212
- fallback_name = f"{base_name}_{timestamp}"
213
- try:
214
- actor.set_actor_label(fallback_name)
215
- except Exception as label_error:
216
- result["warnings"].append(f"Failed to set actor label: {label_error}")
217
- actor_name = actor.get_actor_label() or fallback_name
218
- result["success"] = True
219
- result["actorName"] = actor_name
220
- if not result["message"]:
221
- result["message"] = f"Spawned {actor_name} at ({location.x}, {location.y}, {location.z})"
222
- else:
223
- result["message"] = f"Failed to spawn actor from: {class_path}. Try using /Engine/BasicShapes/Cube or StaticMeshActor"
224
- result["error"] = result["message"]
225
- except Exception as spawn_error:
226
- result["error"] = f"Error spawning actor: {spawn_error}"
227
- if not result["message"]:
228
- result["message"] = result["error"]
229
-
230
- print('RESULT:' + json.dumps(finalize()))
231
- `.trim();
232
-
233
44
  try {
234
- const response = await this.bridge.executePython(pythonCmd);
235
- const interpreted = interpretStandardResult(response, {
236
- successMessage: `Spawned actor ${className}`,
237
- failureMessage: `Failed to spawn actor ${className}`
238
- });
45
+ const bridge = this.getAutomationBridge();
46
+ const timeoutMs = typeof params.timeoutMs === 'number' && params.timeoutMs > 0 ? params.timeoutMs : undefined;
47
+ const response = await bridge.sendAutomationRequest(
48
+ 'control_actor',
49
+ {
50
+ action: 'spawn',
51
+ classPath: mappedClassPath,
52
+ location: { x: locX, y: locY, z: locZ },
53
+ rotation: { pitch: rotPitch, yaw: rotYaw, roll: rotRoll },
54
+ actorName: sanitizedActorName,
55
+ meshPath: params.meshPath
56
+ },
57
+ timeoutMs ? { timeoutMs } : undefined
58
+ );
239
59
 
240
- if (!interpreted.success) {
241
- throw new Error(interpreted.error || interpreted.message);
60
+ if (!response || !response.success) {
61
+ throw new Error(response?.error || response?.message || 'Failed to spawn actor');
242
62
  }
243
63
 
244
- const actorName = coerceString(interpreted.payload.actorName);
245
- const resolvedClass = coerceString(interpreted.payload.resolvedClass) ?? mappedClassPath;
246
- const requestedClass = coerceString(interpreted.payload.requestedClass) ?? className;
247
- const locationVector = coerceVector3(interpreted.payload.location) ?? [locX, locY, locZ];
248
- const rotationVector = coerceVector3(interpreted.payload.rotation) ?? [rotPitch, rotYaw, rotRoll];
249
-
250
- const result: Record<string, unknown> = {
64
+ const data = (response as any).data || {};
65
+ const result: StandardActionResponse = {
251
66
  success: true,
252
- message: interpreted.message,
253
- actorName: actorName ?? undefined,
254
- resolvedClass,
255
- requestedClass,
256
- location: { x: locationVector[0], y: locationVector[1], z: locationVector[2] },
257
- rotation: { pitch: rotationVector[0], yaw: rotationVector[1], roll: rotationVector[2] }
67
+ message: response.message || `Spawned actor ${className}`,
68
+ actorName: data.name || (response as any).actorName,
69
+ actorPath: data.objectPath || (response as any).actorPath,
70
+ resolvedClass: mappedClassPath,
71
+ requestedClass: className,
72
+ location: { x: locX, y: locY, z: locZ },
73
+ rotation: { pitch: rotPitch, yaw: rotYaw, roll: rotRoll },
74
+ data: data,
75
+ actor: {
76
+ name: data.name || (response as any).actorName,
77
+ path: data.objectPath || (response as any).actorPath || mappedClassPath
78
+ }
258
79
  };
259
80
 
260
- if (interpreted.warnings?.length) {
261
- result.warnings = interpreted.warnings;
81
+ if ((response as any).warnings?.length) {
82
+ result.warnings = (response as any).warnings;
83
+ }
84
+
85
+ // Legacy support for older fields if they exist at top level
86
+ if ((response as any).details?.length) {
87
+ result.details = (response as any).details;
262
88
  }
263
- if (interpreted.details?.length) {
264
- result.details = interpreted.details;
89
+ if ((response as any).componentPaths?.length) {
90
+ result.componentPaths = (response as any).componentPaths;
265
91
  }
266
92
 
267
93
  return result;
268
94
  } catch (err) {
269
- throw new Error(`Failed to spawn actor via Python: ${err}`);
95
+ throw new Error(`Failed to spawn actor: ${err}`);
270
96
  }
271
97
  }
272
-
273
- async spawnViaConsole(params: { classPath: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
274
- try {
275
- const [locX, locY, locZ] = ensureVector3(params.location ?? { x: 0, y: 0, z: 100 }, 'actor location');
276
- // Check if editor is in play mode first
277
- try {
278
- const pieCheckPython = `
279
- import unreal
280
- les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
281
- if les and les.is_in_play_in_editor():
282
- print("PIE_ACTIVE")
283
- else:
284
- print("PIE_INACTIVE")
285
- `.trim();
286
-
287
- const pieCheckResult = await this.bridge.executePython(pieCheckPython);
288
- const outputStr = typeof pieCheckResult === 'string' ? pieCheckResult : JSON.stringify(pieCheckResult);
289
-
290
- if (outputStr.includes('PIE_ACTIVE')) {
291
- throw new Error('Cannot spawn actors while in Play In Editor mode. Please stop PIE first.');
292
- }
293
- } catch (pieErr: any) {
294
- // If the error is about PIE, throw it
295
- if (String(pieErr).includes('Play In Editor')) {
296
- throw pieErr;
297
- }
298
- // Otherwise ignore and continue
299
- }
300
-
301
- // List of known abstract classes that cannot be spawned
302
- const abstractClasses = ['PlaneReflectionCapture', 'ReflectionCapture', 'Actor'];
303
-
304
- // Check if this is an abstract class
305
- if (abstractClasses.includes(params.classPath)) {
306
- throw new Error(`Cannot spawn ${params.classPath}: class is abstract and cannot be instantiated`);
98
+
99
+ async delete(params: { actorName?: string; actorNames?: string[] }) {
100
+ if (params.actorNames && Array.isArray(params.actorNames)) {
101
+ const names = params.actorNames
102
+ .filter(name => typeof name === 'string')
103
+ .map(name => name.trim())
104
+ .filter(name => name.length > 0);
105
+
106
+ // Edge-case: empty batch should be treated as a no-op success
107
+ if (names.length === 0) {
108
+ return {
109
+ success: true,
110
+ message: 'No actors provided for deletion; no-op',
111
+ deleted: [],
112
+ noOp: true
113
+ };
307
114
  }
308
-
309
- // Get the console-friendly class name
310
- const spawnClass = this.getConsoleClassName(params.classPath);
311
-
312
- // Use summon command with location if provided
313
- const command = `summon ${spawnClass} ${locX} ${locY} ${locZ}`;
314
-
315
- await this.bridge.httpCall('/remote/object/call', 'PUT', {
316
- objectPath: '/Script/Engine.Default__KismetSystemLibrary',
317
- functionName: 'ExecuteConsoleCommand',
318
- parameters: {
319
- WorldContextObject: null,
320
- Command: command,
321
- SpecificPlayer: null
322
- },
323
- generateTransaction: false
115
+
116
+ // Call the underlying automation action directly so we can treat
117
+ // DELETE_PARTIAL as a handled, partial-success cleanup instead
118
+ // of surfacing it as a hard error to the consolidated handler.
119
+ const bridge = this.getAutomationBridge();
120
+ const response: any = await bridge.sendAutomationRequest('control_actor', {
121
+ action: 'delete',
122
+ actorNames: names
324
123
  });
325
-
326
- // Console commands don't reliably report success/failure
327
- // We can't guarantee this actually worked, so indicate uncertainty
328
- return {
329
- success: true,
330
- message: `Actor spawn attempted via console: ${spawnClass} at ${locX},${locY},${locZ}`,
331
- note: 'Console spawn result uncertain - verify in editor'
332
- };
333
- } catch (err) {
334
- throw new Error(`Failed to spawn actor: ${err}`);
124
+
125
+ const result = (response?.data || response?.result || response) ?? {};
126
+ const deleted = result.deleted ?? names;
127
+ const missing = result.missing ?? [];
128
+ // Check for structured error in response.error OR legacy top-level error
129
+ const errorObj = response?.error;
130
+ const errorCode = (typeof errorObj === 'object' ? errorObj.code : String(errorObj || result.error || '')).toUpperCase();
131
+
132
+ // If some actors were removed and others were already missing,
133
+ // surface this as a partial but still successful cleanup so the
134
+ // tests treat it as handled rather than as an MCP transport error.
135
+ if (response && response.success === false && errorCode === 'DELETE_PARTIAL') {
136
+ return {
137
+ success: true,
138
+ message: errorObj?.message || response.message || 'Some actors could not be deleted',
139
+ deleted,
140
+ missing,
141
+ partial: true
142
+ } as StandardActionResponse;
143
+ }
144
+
145
+ if (!response || response.success === false) {
146
+ throw new Error(errorObj?.message || response?.message || 'Failed to delete actors');
147
+ }
148
+
149
+ return {
150
+ success: true,
151
+ message: response.message || 'Deleted actors',
152
+ deleted: result.deleted || deleted,
153
+ ...result
154
+ } as StandardActionResponse;
335
155
  }
156
+
157
+ if (!params.actorName || typeof params.actorName !== 'string') {
158
+ throw new Error('Invalid actorName');
159
+ }
160
+
161
+ return this.sendRequest('delete', { actorName: params.actorName }, 'control_actor');
336
162
  }
337
- private getPythonSpawnHelper(): string {
338
- return `
339
- def spawn_actor_from_class(actor_class, location, rotation):
340
- actor = None
341
- try:
342
- actor_subsys = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
343
- if actor_subsys:
344
- actor = actor_subsys.spawn_actor_from_class(actor_class, location, rotation)
345
- except Exception:
346
- actor = None
347
- if not actor:
348
- raise RuntimeError('EditorActorSubsystem unavailable or failed to spawn actor. Enable Editor Scripting Utilities plugin and verify class path.')
349
- return actor
350
- `.trim();
163
+
164
+ async applyForce(params: { actorName: string; force: { x: number; y: number; z: number } }) {
165
+ if (!params.actorName || typeof params.actorName !== 'string') {
166
+ throw new Error('Invalid actorName');
167
+ }
168
+ if (!params.force || typeof params.force !== 'object') {
169
+ throw new Error('Invalid force vector');
170
+ }
171
+
172
+ const [forceX, forceY, forceZ] = ensureVector3(params.force, 'force vector');
173
+
174
+ // Edge-case: zero force vector is treated as a safe no-op. This avoids
175
+ // spurious ACTOR_NOT_FOUND errors when the physics actor has already been
176
+ // cleaned up in prior tests.
177
+ if (forceX === 0 && forceY === 0 && forceZ === 0) {
178
+ return {
179
+ success: true,
180
+ message: `Zero force provided for ${params.actorName}; no-op`,
181
+ physicsEnabled: false,
182
+ noOp: true
183
+ };
184
+ }
185
+
186
+ return this.sendRequest('apply_force', {
187
+ actorName: params.actorName,
188
+ force: { x: forceX, y: forceY, z: forceZ }
189
+ }, 'control_actor');
351
190
  }
352
191
 
353
192
  private resolveActorClass(classPath: string): string {
@@ -377,12 +216,12 @@ def spawn_actor_from_class(actor_class, location, rotation):
377
216
  // PlaneReflectionCapture is abstract and cannot be spawned
378
217
  'DecalActor': '/Script/Engine.DecalActor'
379
218
  };
380
-
219
+
381
220
  // Check if it's a simple name that needs mapping
382
221
  if (classMap[classPath]) {
383
222
  return classMap[classPath];
384
223
  }
385
-
224
+
386
225
  // Check if it already looks like a full path
387
226
  if (classPath.startsWith('/Script/') || classPath.startsWith('/Game/')) {
388
227
  return classPath;
@@ -391,7 +230,7 @@ def spawn_actor_from_class(actor_class, location, rotation):
391
230
  if (classPath.startsWith('/Engine/')) {
392
231
  return classPath;
393
232
  }
394
-
233
+
395
234
  // Check for Blueprint paths
396
235
  if (classPath.includes('Blueprint') || classPath.includes('BP_')) {
397
236
  // Ensure it has the proper prefix
@@ -400,37 +239,293 @@ def spawn_actor_from_class(actor_class, location, rotation):
400
239
  }
401
240
  return classPath;
402
241
  }
403
-
242
+
404
243
  // Default: assume it's an engine class
405
244
  return '/Script/Engine.' + classPath;
406
245
  }
407
-
408
- private getConsoleClassName(classPath: string): string {
409
- // Normalize class path for console 'summon'
410
- const input = classPath;
411
246
 
412
- // Engine classes: reduce '/Script/Engine.ClassName' to 'ClassName'
413
- if (input.startsWith('/Script/Engine.')) {
414
- return input.replace('/Script/Engine.', '');
247
+ async spawnBlueprint(params: { blueprintPath: string; actorName?: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number } }) {
248
+ const blueprintPath = typeof params.blueprintPath === 'string' ? params.blueprintPath.trim() : '';
249
+ if (!blueprintPath) {
250
+ throw new Error('Invalid blueprintPath');
251
+ }
252
+
253
+ const actorName = typeof params.actorName === 'string' && params.actorName.trim().length > 0 ? params.actorName.trim() : undefined;
254
+ const location = params.location ? ensureVector3(params.location, 'spawn_blueprint location') : undefined;
255
+ const rotation = params.rotation ? ensureRotation(params.rotation, 'spawn_blueprint rotation') : undefined;
256
+
257
+ const payload: Record<string, unknown> = { blueprintPath };
258
+ if (actorName) payload.actorName = actorName;
259
+ if (location) payload.location = { x: location[0], y: location[1], z: location[2] };
260
+ if (rotation) payload.rotation = { pitch: rotation[0], yaw: rotation[1], roll: rotation[2] };
261
+
262
+ return this.sendRequest('spawn_blueprint', payload, 'control_actor');
263
+ }
264
+
265
+ async setTransform(params: { actorName: string; location?: { x: number; y: number; z: number }; rotation?: { pitch: number; yaw: number; roll: number }; scale?: { x: number; y: number; z: number } }) {
266
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
267
+ if (!actorName) {
268
+ throw new Error('Invalid actorName');
415
269
  }
416
270
 
417
- // If it's already a simple class name (no path) and not a /Game asset, strip optional _C and return
418
- if (!input.startsWith('/Game/') && !input.includes('/')) {
419
- if (input.endsWith('_C')) return input.slice(0, -2);
420
- return input;
271
+ const payload: Record<string, unknown> = { actorName };
272
+ if (params.location) {
273
+ const loc = ensureVector3(params.location, 'set_transform location');
274
+ payload.location = { x: loc[0], y: loc[1], z: loc[2] };
421
275
  }
276
+ if (params.rotation) {
277
+ const rot = ensureRotation(params.rotation, 'set_transform rotation');
278
+ payload.rotation = { pitch: rot[0], yaw: rot[1], roll: rot[2] };
279
+ }
280
+ if (params.scale) {
281
+ const scl = ensureVector3(params.scale, 'set_transform scale');
282
+ payload.scale = { x: scl[0], y: scl[1], z: scl[2] };
283
+ }
284
+
285
+ return this.sendRequest('set_transform', payload, 'control_actor');
286
+ }
422
287
 
423
- // Blueprint assets under /Game: ensure '/Game/Path/Asset.Asset_C'
424
- if (input.startsWith('/Game/')) {
425
- // Remove any existing ".Something" suffix to rebuild normalized class ref
426
- const pathWithoutSuffix = input.split('.')[0];
427
- const parts = pathWithoutSuffix.split('/');
428
- const assetName = parts[parts.length - 1].replace(/_C$/, '');
429
- const normalized = `${pathWithoutSuffix}.${assetName}_C`;
430
- return normalized;
288
+ async getTransform(actorName: string) {
289
+ if (typeof actorName !== 'string' || actorName.trim().length === 0) {
290
+ throw new Error('Invalid actorName');
431
291
  }
292
+ return this.sendRequest('get_transform', { actorName }, 'control_actor')
293
+ .then(response => {
294
+ // If response is standardized, extract data or return as is.
295
+ // For now, return the full response which includes data.
296
+ return response;
297
+ });
298
+ }
299
+
300
+ async setVisibility(params: { actorName: string; visible: boolean }) {
301
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
302
+ if (!actorName) {
303
+ throw new Error('Invalid actorName');
304
+ }
305
+ return this.sendRequest('set_visibility', { actorName, visible: Boolean(params.visible) }, 'control_actor');
306
+ }
307
+
308
+ async addComponent(params: { actorName: string; componentType: string; componentName?: string; properties?: Record<string, unknown> }) {
309
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
310
+ const componentType = typeof params.componentType === 'string' ? params.componentType.trim() : '';
311
+ if (!actorName) throw new Error('Invalid actorName');
312
+ if (!componentType) throw new Error('Invalid componentType');
313
+
314
+ return this.sendRequest('add_component', {
315
+ actorName,
316
+ componentType,
317
+ componentName: typeof params.componentName === 'string' ? params.componentName : undefined,
318
+ properties: params.properties
319
+ }, 'control_actor');
320
+ }
432
321
 
433
- // Fallback: return input unchanged
434
- return input;
322
+ async setComponentProperties(params: { actorName: string; componentName: string; properties: Record<string, unknown> }) {
323
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
324
+ const componentName = typeof params.componentName === 'string' ? params.componentName.trim() : '';
325
+ if (!actorName) throw new Error('Invalid actorName');
326
+ if (!componentName) throw new Error('Invalid componentName');
327
+
328
+ return this.sendRequest('set_component_properties', {
329
+ actorName,
330
+ componentName,
331
+ properties: params.properties ?? {}
332
+ }, 'control_actor');
333
+ }
334
+
335
+ async getComponents(actorName: string) {
336
+ if (typeof actorName !== 'string' || actorName.trim().length === 0) {
337
+ throw new Error('Invalid actorName');
338
+ }
339
+ const response = await this.sendRequest('get_components', { actorName }, 'control_actor');
340
+ if (!response.success) {
341
+ return { success: false, error: response.error || `Failed to get components for actor ${actorName}` };
342
+ }
343
+
344
+ const data: any = response.data ?? response.result ?? response;
345
+ const components = Array.isArray(data)
346
+ ? data
347
+ : (Array.isArray(data?.components) ? data.components : []);
348
+ const count = typeof data?.count === 'number' ? data.count : components.length;
349
+
350
+ return {
351
+ success: true,
352
+ message: 'Actor components retrieved',
353
+ components,
354
+ count
355
+ };
356
+ }
357
+
358
+ async duplicate(params: { actorName: string; newName?: string; offset?: { x: number; y: number; z: number } }) {
359
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
360
+ if (!actorName) throw new Error('Invalid actorName');
361
+
362
+ const payload: Record<string, unknown> = { actorName };
363
+ if (typeof params.newName === 'string' && params.newName.trim().length > 0) {
364
+ payload.newName = params.newName.trim();
365
+ }
366
+ if (params.offset) {
367
+ const offs = ensureVector3(params.offset, 'duplicate offset');
368
+ payload.offset = { x: offs[0], y: offs[1], z: offs[2] };
369
+ }
370
+
371
+ return this.sendRequest('duplicate', payload, 'control_actor');
372
+ }
373
+
374
+ async addTag(params: { actorName: string; tag: string }) {
375
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
376
+ const tag = typeof params.tag === 'string' ? params.tag.trim() : '';
377
+ if (!actorName) throw new Error('Invalid actorName');
378
+ if (!tag) throw new Error('Invalid tag');
379
+
380
+ return this.sendRequest('add_tag', { actorName, tag }, 'control_actor');
381
+ }
382
+
383
+ async removeTag(params: { actorName: string; tag: string }) {
384
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
385
+ const tag = typeof params.tag === 'string' ? params.tag.trim() : '';
386
+ if (!actorName) throw new Error('Invalid actorName');
387
+ if (!tag) throw new Error('Invalid tag');
388
+
389
+ return this.sendRequest('remove_tag', { actorName, tag }, 'control_actor');
390
+ }
391
+
392
+ async findByTag(params: { tag: string; matchType?: string }) {
393
+ const tag = typeof params.tag === 'string' ? params.tag.trim() : '';
394
+
395
+ // Edge-case: empty tag should return an empty result set instead of throwing
396
+ if (!tag) {
397
+ return {
398
+ success: true,
399
+ message: 'Empty tag query; no actors matched',
400
+ data: {
401
+ actors: [],
402
+ count: 0
403
+ }
404
+ };
405
+ }
406
+
407
+ return this.sendRequest('find_by_tag', {
408
+ tag,
409
+ matchType: typeof params.matchType === 'string' ? params.matchType : undefined
410
+ }, 'control_actor');
411
+ }
412
+
413
+ async findByName(name: string) {
414
+ if (typeof name !== 'string' || name.trim().length === 0) {
415
+ throw new Error('Invalid actor name query');
416
+ }
417
+ return this.sendRequest('find_by_name', { name: name.trim() }, 'control_actor');
418
+ }
419
+
420
+ async detach(actorName: string) {
421
+ // Support 'childActor' as alias for 'actorName' since attach() uses childActor.
422
+ // If actorName is missing/empty but childActor is present in the underlying request (handled by caller or if we expand args),
423
+ // we should handle it. However, the signature here is specific.
424
+ // We'll rely on the handler to map it, or we can expand the signature if needed.
425
+ // For now, let's keep the strict signature but ensure the handler passes it correctly.
426
+ // Actually, looking at the handler (actor-handlers.ts), it calls tools.actors.detach(args.actorName).
427
+ // So we should modify actor-handlers.ts instead to map childActor -> actorName.
428
+ if (typeof actorName !== 'string' || actorName.trim().length === 0) {
429
+ throw new Error('Invalid actorName');
430
+ }
431
+ return this.sendRequest('detach', { actorName }, 'control_actor');
432
+ }
433
+
434
+ async attach(params: { childActor: string; parentActor: string }) {
435
+ const child = typeof params.childActor === 'string' ? params.childActor.trim() : '';
436
+ const parent = typeof params.parentActor === 'string' ? params.parentActor.trim() : '';
437
+ if (!child) throw new Error('Invalid childActor');
438
+ if (!parent) throw new Error('Invalid parentActor');
439
+
440
+ return this.sendRequest('attach', { childActor: child, parentActor: parent }, 'control_actor');
441
+ }
442
+
443
+ async deleteByTag(tag: string) {
444
+ if (typeof tag !== 'string' || tag.trim().length === 0) {
445
+ throw new Error('Invalid tag');
446
+ }
447
+ return this.sendRequest('delete_by_tag', { tag: tag.trim() }, 'control_actor');
448
+ }
449
+
450
+ async setBlueprintVariables(params: { actorName: string; variables: Record<string, unknown> }) {
451
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
452
+ if (!actorName) throw new Error('Invalid actorName');
453
+ return this.sendRequest('set_blueprint_variables', { actorName, variables: params.variables ?? {} }, 'control_actor');
454
+ }
455
+
456
+ async createSnapshot(params: { actorName: string; snapshotName: string }) {
457
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
458
+ const snapshotName = typeof params.snapshotName === 'string' ? params.snapshotName.trim() : '';
459
+ if (!actorName) throw new Error('Invalid actorName');
460
+ if (!snapshotName) throw new Error('Invalid snapshotName');
461
+ return this.sendRequest('create_snapshot', { actorName, snapshotName }, 'control_actor');
462
+ }
463
+
464
+ async restoreSnapshot(params: { actorName: string; snapshotName: string }) {
465
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
466
+ const snapshotName = typeof params.snapshotName === 'string' ? params.snapshotName.trim() : '';
467
+ if (!actorName) throw new Error('Invalid actorName');
468
+ if (!snapshotName) throw new Error('Invalid snapshotName');
469
+ return this.sendRequest('restore_snapshot', { actorName, snapshotName }, 'control_actor');
470
+ }
471
+ async exportActor(params: { actorName: string; destinationPath?: string }) {
472
+ const actorName = typeof params.actorName === 'string' ? params.actorName.trim() : '';
473
+ if (!actorName) throw new Error('Invalid actorName');
474
+ return this.sendRequest('export', {
475
+ actorName,
476
+ destinationPath: params.destinationPath
477
+ }, 'control_actor');
478
+ }
479
+
480
+ async getBoundingBox(actorName: string) {
481
+ if (typeof actorName !== 'string' || actorName.trim().length === 0) {
482
+ throw new Error('Invalid actorName');
483
+ }
484
+ const response = await this.sendRequest('get_bounding_box', { actorName }, 'control_actor');
485
+ if (!response.success) {
486
+ return { success: false, error: response.error || `Failed to get bounding box for actor ${actorName}` };
487
+ }
488
+ return {
489
+ success: true,
490
+ message: 'Bounding box retrieved',
491
+ boundingBox: response.data || response.result || {}
492
+ };
493
+ }
494
+
495
+ async getMetadata(actorName: string) {
496
+ if (typeof actorName !== 'string' || actorName.trim().length === 0) {
497
+ throw new Error('Invalid actorName');
498
+ }
499
+ const response = await this.sendRequest('get_metadata', { actorName }, 'control_actor');
500
+ if (!response.success) {
501
+ return { success: false, error: response.error || `Failed to get metadata for actor ${actorName}` };
502
+ }
503
+ return {
504
+ success: true,
505
+ message: 'Actor metadata retrieved',
506
+ metadata: response.data || response.result || {}
507
+ };
508
+ }
509
+
510
+ async listActors(params?: { filter?: string }) {
511
+ const payload: any = {};
512
+ if (params?.filter) {
513
+ payload.filter = params.filter;
514
+ }
515
+ const response = await this.sendRequest('list_actors', payload, 'control_actor');
516
+ if (!response.success) {
517
+ return { success: false, error: response.error || 'Failed to list actors' };
518
+ }
519
+ // C++ returns actors in data.actors, or directly in actors field
520
+ // Handle both: response.data?.actors, response.actors, or response.data as array
521
+ const dataObj = response.data || response.result || {};
522
+ const actorsRaw = response.actors || (dataObj && dataObj.actors) || (Array.isArray(dataObj) ? dataObj : []);
523
+ const actors = Array.isArray(actorsRaw) ? actorsRaw : [];
524
+ return {
525
+ success: true,
526
+ message: `Found ${actors.length} actors`,
527
+ actors,
528
+ count: actors.length
529
+ };
435
530
  }
436
531
  }