ue-mcp 1.2.3 → 1.3.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 (255) hide show
  1. package/README.md +4 -3
  2. package/dist/action-class.js +56 -3
  3. package/dist/action-class.js.map +1 -1
  4. package/dist/bridge-timeouts.d.ts +64 -0
  5. package/dist/bridge-timeouts.js +118 -0
  6. package/dist/bridge-timeouts.js.map +1 -0
  7. package/dist/bridge.js +10 -2
  8. package/dist/bridge.js.map +1 -1
  9. package/dist/editor-control.d.ts +72 -5
  10. package/dist/editor-control.js +164 -140
  11. package/dist/editor-control.js.map +1 -1
  12. package/dist/editor-target.d.ts +55 -1
  13. package/dist/editor-target.js +107 -10
  14. package/dist/editor-target.js.map +1 -1
  15. package/dist/engine-observer.d.ts +59 -5
  16. package/dist/engine-observer.js +249 -29
  17. package/dist/engine-observer.js.map +1 -1
  18. package/dist/engine-root.d.ts +91 -0
  19. package/dist/engine-root.js +448 -0
  20. package/dist/engine-root.js.map +1 -0
  21. package/dist/epic-enrich.js +16 -1
  22. package/dist/epic-enrich.js.map +1 -1
  23. package/dist/feedback-cli.js +4 -0
  24. package/dist/feedback-cli.js.map +1 -1
  25. package/dist/feedback-deferred.d.ts +9 -0
  26. package/dist/feedback-deferred.js +2 -0
  27. package/dist/feedback-deferred.js.map +1 -1
  28. package/dist/feedback-fallback.d.ts +99 -0
  29. package/dist/feedback-fallback.js +173 -0
  30. package/dist/feedback-fallback.js.map +1 -0
  31. package/dist/flow/write-methods.js +20 -0
  32. package/dist/flow/write-methods.js.map +1 -1
  33. package/dist/index.js +41 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/lean-context.js +8 -3
  36. package/dist/lean-context.js.map +1 -1
  37. package/dist/locking.js +11 -2
  38. package/dist/locking.js.map +1 -1
  39. package/dist/plugin/loader.d.ts +4 -0
  40. package/dist/plugin/loader.js +22 -8
  41. package/dist/plugin/loader.js.map +1 -1
  42. package/dist/plugin/manifest.d.ts +72 -52
  43. package/dist/plugin/manifest.js +118 -21
  44. package/dist/plugin/manifest.js.map +1 -1
  45. package/dist/plugin-cli.js +16 -3
  46. package/dist/plugin-cli.js.map +1 -1
  47. package/dist/project.d.ts +11 -0
  48. package/dist/project.js +23 -2
  49. package/dist/project.js.map +1 -1
  50. package/dist/python-gate.d.ts +95 -0
  51. package/dist/python-gate.js +175 -0
  52. package/dist/python-gate.js.map +1 -0
  53. package/dist/tool-counts.json +16 -16
  54. package/dist/tools/animation.js +288 -24
  55. package/dist/tools/animation.js.map +1 -1
  56. package/dist/tools/asset.js +79 -14
  57. package/dist/tools/asset.js.map +1 -1
  58. package/dist/tools/audio.js +59 -5
  59. package/dist/tools/audio.js.map +1 -1
  60. package/dist/tools/blueprint.js +29 -12
  61. package/dist/tools/blueprint.js.map +1 -1
  62. package/dist/tools/chooser.js +4 -1
  63. package/dist/tools/chooser.js.map +1 -1
  64. package/dist/tools/editor.js +81 -43
  65. package/dist/tools/editor.js.map +1 -1
  66. package/dist/tools/feedback.js +214 -48
  67. package/dist/tools/feedback.js.map +1 -1
  68. package/dist/tools/foliage.js +27 -1
  69. package/dist/tools/foliage.js.map +1 -1
  70. package/dist/tools/gameplay.js +38 -11
  71. package/dist/tools/gameplay.js.map +1 -1
  72. package/dist/tools/gas.js +17 -5
  73. package/dist/tools/gas.js.map +1 -1
  74. package/dist/tools/landscape.js +22 -7
  75. package/dist/tools/landscape.js.map +1 -1
  76. package/dist/tools/level.js +322 -51
  77. package/dist/tools/level.js.map +1 -1
  78. package/dist/tools/material.js +10 -4
  79. package/dist/tools/material.js.map +1 -1
  80. package/dist/tools/niagara.js +3 -2
  81. package/dist/tools/niagara.js.map +1 -1
  82. package/dist/tools/pcg.js +6 -5
  83. package/dist/tools/pcg.js.map +1 -1
  84. package/dist/tools/plugins.js +2 -0
  85. package/dist/tools/plugins.js.map +1 -1
  86. package/dist/tools/project.js +47 -20
  87. package/dist/tools/project.js.map +1 -1
  88. package/dist/types.d.ts +24 -0
  89. package/dist/types.js +46 -3
  90. package/dist/types.js.map +1 -1
  91. package/dist/ue-mcp.default.yml +493 -125
  92. package/package.json +2 -2
  93. package/plugin/ue_mcp_bridge/NATIVE_RUNTIME_VISIBILITY.md +53 -0
  94. package/plugin/ue_mcp_bridge/NATIVE_STATIC_MESH_USAGE_PROGRESS.md +115 -0
  95. package/plugin/ue_mcp_bridge/README_MassHandlers.md +97 -0
  96. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/BridgeServer.cpp +14 -0
  97. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.cpp +21 -4
  98. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/GameThreadExecutor.h +9 -0
  99. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/HandlerJsonProperty.h +395 -69
  100. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.cpp +79 -3
  101. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers.h +25 -0
  102. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRig.cpp +3 -3
  103. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_ControlRigSequencer.cpp +3061 -0
  104. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRetargeterAuthoring.cpp +961 -0
  105. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_IKRigAuthoring.cpp +900 -0
  106. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Pose.cpp +769 -0
  107. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Sequence.cpp +44 -3
  108. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_SkeletalLive.cpp +217 -11
  109. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_StateMachine.cpp +551 -24
  110. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AnimationHandlers_Validation.cpp +735 -0
  111. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.cpp +352 -73
  112. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers.h +8 -0
  113. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkProperties.cpp +1 -1
  114. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.cpp +503 -0
  115. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkRead.h +21 -0
  116. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_BulkUpsert.cpp +2 -2
  117. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Enum.cpp +2 -2
  118. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.cpp +902 -0
  119. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Geometry.h +33 -0
  120. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Import.cpp +478 -134
  121. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Mesh.cpp +5 -5
  122. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.cpp +933 -0
  123. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_MeshBoolean.h +35 -0
  124. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Redirectors.cpp +478 -0
  125. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_RenderTarget.cpp +1 -1
  126. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Sockets.cpp +4 -4
  127. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Struct.cpp +2 -2
  128. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AssetHandlers_Subobject.cpp +319 -0
  129. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/AudioHandlers_SoundCue.cpp +3 -3
  130. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.cpp +482 -35
  131. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers.h +10 -0
  132. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.cpp +502 -0
  133. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Collision.h +43 -0
  134. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Functions.cpp +89 -7
  135. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Graph.cpp +17 -51
  136. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Internal.h +107 -0
  137. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Properties.cpp +10 -10
  138. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/BlueprintHandlers_Search.cpp +648 -0
  139. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DemoHandlers.cpp +5 -1
  140. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.cpp +252 -1
  141. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/DiffHandlers.h +10 -2
  142. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.cpp +252 -41
  143. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers.h +4 -0
  144. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_Build.cpp +250 -27
  145. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIE.cpp +303 -125
  146. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_PIERuntime.cpp +73 -50
  147. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/EditorHandlers_RuntimeVisibility.cpp +757 -0
  148. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.cpp +11 -2
  149. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers.h +3 -0
  150. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/FoliageHandlers_Batch.cpp +419 -0
  151. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.cpp +39 -161
  152. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers.h +66 -1
  153. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BTAuthoring.cpp +1323 -0
  154. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GameplayHandlers_BehaviorTree.cpp +1211 -0
  155. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.cpp +2 -0
  156. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers.h +60 -0
  157. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/GasHandlers_Runtime.cpp +363 -12
  158. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/HandlerEditorState.h +84 -0
  159. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.cpp +1288 -36
  160. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers.h +3 -0
  161. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LandscapeHandlers_Sculpt.cpp +41 -16
  162. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.cpp +959 -213
  163. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers.h +66 -0
  164. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_BatchWrite.cpp +1571 -0
  165. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Convert.cpp +316 -0
  166. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Inspection.cpp +280 -0
  167. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection.cpp +666 -0
  168. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_InstanceProjection_Internal.h +63 -0
  169. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Lights.cpp +43 -12
  170. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_MultiLevel.cpp +13 -47
  171. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_PostProcess.cpp +527 -0
  172. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Query.cpp +1169 -0
  173. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Refresh.cpp +723 -0
  174. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Save.cpp +325 -0
  175. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Spatial.cpp +360 -0
  176. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Trace.cpp +178 -58
  177. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Transient.cpp +337 -0
  178. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_Volumes.cpp +25 -12
  179. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartition.cpp +10 -9
  180. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/LevelHandlers_WorldPartitionSettings.cpp +686 -0
  181. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.cpp +467 -0
  182. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MassHandlers.h +22 -0
  183. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.cpp +452 -175
  184. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers.h +5 -0
  185. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/MaterialHandlers_Build.cpp +597 -0
  186. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/NiagaraHandlers.cpp +17 -15
  187. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PCGHandlers.cpp +38 -168
  188. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/PhysicsHandlers.cpp +39 -30
  189. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.cpp +392 -78
  190. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers.h +2 -0
  191. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SequencerHandlers_Internal.h +83 -0
  192. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.cpp +200 -0
  193. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SkeletalMeshHandlers.h +15 -0
  194. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/SplineHandlers.cpp +19 -12
  195. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/StateTreeHandlers.cpp +31 -1
  196. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.cpp +307 -69
  197. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers.h +84 -0
  198. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Extraction.cpp +8 -41
  199. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Handlers/WidgetHandlers_Properties.cpp +33 -52
  200. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/ActorSelectorTests.cpp +344 -0
  201. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationControlRigTimelineTests.cpp +60 -0
  202. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AnimationPoseTests.cpp +115 -0
  203. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetBulkReadTests.cpp +177 -0
  204. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetDeleteGuardTests.cpp +276 -0
  205. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPathResolutionTests.cpp +243 -0
  206. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetPropertyPersistTests.cpp +213 -0
  207. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AssetSubobjectTests.cpp +235 -0
  208. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/AutomationRunnerTests.cpp +72 -0
  209. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeAuthoringTests.cpp +526 -0
  210. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BehaviorTreeNodeTests.cpp +334 -0
  211. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BlueprintHandlerSurfaceTests.cpp +362 -0
  212. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/BridgeProtocolTests.cpp +14 -0
  213. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/CollisionQueryTests.cpp +288 -0
  214. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/DataTableRowWriteTests.cpp +453 -0
  215. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/EditorFunctionCallTests.cpp +223 -0
  216. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/FoliageBatchTests.cpp +147 -0
  217. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GameThreadGateTests.cpp +82 -0
  218. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/GasLiveAttributeTests.cpp +183 -0
  219. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LandscapeSampleTests.cpp +119 -0
  220. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelBatchWriteTests.cpp +176 -0
  221. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelComponentTreeInstanceCountTests.cpp +189 -0
  222. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelConvertBrushesTests.cpp +100 -0
  223. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelInstanceProjectionTests.cpp +265 -0
  224. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelPostProcessOverrideTests.cpp +258 -0
  225. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelQueryComponentsTests.cpp +195 -0
  226. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelRefreshTests.cpp +142 -0
  227. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelSaveReportTests.cpp +125 -0
  228. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelScriptHandlersTests.cpp +42 -0
  229. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelStaticMeshUsageTests.cpp +412 -0
  230. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTraceWorldScopeTests.cpp +133 -0
  231. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelTransientActorTests.cpp +205 -0
  232. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/LevelWorldPartitionSettingsTests.cpp +168 -0
  233. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTestTypes.h +51 -0
  234. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MCPInstancedStructPathTests.cpp +379 -0
  235. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MaterialBuildTests.cpp +238 -0
  236. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshBooleanTests.cpp +170 -0
  237. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/MeshGeometryTests.cpp +116 -0
  238. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageSaveExtensionTests.cpp +161 -0
  239. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/PackageWriteGuardTests.cpp +253 -0
  240. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/RuntimeVisibilityTests.cpp +143 -0
  241. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerHandlerTests.cpp +660 -0
  242. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SequencerScrubTests.cpp +50 -0
  243. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/SkeletonDiffTests.cpp +685 -0
  244. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/Tests/WidgetBlueprintResolveTests.cpp +145 -0
  245. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Private/UE_MCP_Bridge.cpp +80 -0
  246. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerFunctionCall.h +509 -0
  247. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerQuery.h +407 -0
  248. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/Public/HandlerUtils.h +1008 -77
  249. package/plugin/ue_mcp_bridge/Source/UE_MCP_Bridge/UE_MCP_Bridge.Build.cs +87 -0
  250. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/MCPEngineStatus.cpp +152 -14
  251. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Private/Tests/MCPEngineStatusTests.cpp +65 -0
  252. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/Public/MCPEngineStatus.h +32 -1
  253. package/plugin/ue_mcp_bridge/Source/UE_MCP_BridgeStatus/UE_MCP_BridgeStatus.Build.cs +2 -0
  254. package/skills/ue-mcp-animation/SKILL.md +154 -0
  255. package/skills/ue-mcp-blueprint/SKILL.md +71 -0
@@ -14,6 +14,7 @@
14
14
  #include "Engine/Level.h"
15
15
  #include "Engine/LevelScriptBlueprint.h"
16
16
  #include "Kismet2/KismetEditorUtilities.h"
17
+ #include "Kismet2/BlueprintEditorUtils.h"
17
18
  #include "Subsystems/AssetEditorSubsystem.h"
18
19
  #include "Editor.h"
19
20
  #include "FileHelpers.h"
@@ -75,17 +76,11 @@
75
76
  #include "AI/Navigation/NavCollisionBase.h"
76
77
 
77
78
  // ─── Protected mount guardrail ──────────────────────────────────────────
78
- // The rule itself is MCPIsProtectedAssetPath in HandlerUtils.h, shared by
79
- // every asset translation unit so the write paths cannot drift apart.
79
+ // The rule itself is MCPIsProtectedAssetPath in HandlerUtils.h, and the
80
+ // refusal it produces is MCPProtectedPathError beside it, shared by every
81
+ // asset translation unit so the write paths cannot drift apart.
80
82
  namespace
81
83
  {
82
- TSharedPtr<FJsonValue> MakeProtectedPathError(const FString& Path)
83
- {
84
- return MCPError(FString::Printf(
85
- TEXT("Refusing to mutate protected mount: %s. Engine, /Script/, /Memory/, /Temp/ are read-only via the bridge."),
86
- *Path));
87
- }
88
-
89
84
  // Split "/Game/Foo/Bar.Bar" (or "/Game/Foo/Bar") into mount "/Game/" + rel "Foo/Bar".
90
85
  // Returns false if the path is malformed or has no mount segment.
91
86
  bool SplitMountAndRel(const FString& AssetOrPackagePath, FString& OutMountRoot, FString& OutRelPath, FString& OutPackageName, FString& OutAssetName)
@@ -151,6 +146,9 @@ void FAssetHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
151
146
  Registry.RegisterHandler(TEXT("delete_asset"), &DeleteAsset);
152
147
  Registry.RegisterHandler(TEXT("delete_asset_batch"), &DeleteAssetBatch);
153
148
  Registry.RegisterHandler(TEXT("bulk_rename_assets"), &BulkRename);
149
+ // #908: bounded redirector clean-up after a move. Timeout raised because a
150
+ // fix-up loads and resaves every referencing package.
151
+ Registry.RegisterHandlerWithTimeout(TEXT("fixup_redirectors"), &FixupRedirectors, 300.0f);
154
152
  Registry.RegisterHandler(TEXT("create_data_asset"), &CreateDataAsset);
155
153
  // Bounded batch upsert plus its rollback inverse. A 500-item batch that
156
154
  // preflights every item on a transient copy before touching a package can
@@ -161,6 +159,9 @@ void FAssetHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
161
159
  // #726: generic create-any-concrete-UObject-class asset (physical materials,
162
160
  // curves, settings objects) - not restricted to UDataAsset subclasses.
163
161
  Registry.RegisterHandler(TEXT("create_asset_by_class"), &CreateAssetByClass);
162
+ // #975: a named subobject inside an existing asset package, which
163
+ // create_asset_by_class cannot make because it always creates a package.
164
+ Registry.RegisterHandler(TEXT("create_subobject"), &CreateSubobject);
164
165
  Registry.RegisterHandler(TEXT("save_asset"), &SaveAsset);
165
166
  Registry.RegisterHandler(TEXT("save_all_dirty"), &SaveAllDirty);
166
167
  Registry.RegisterHandler(TEXT("list_textures"), &ListTextures);
@@ -242,6 +243,7 @@ void FAssetHandlers::RegisterHandlers(FMCPHandlerRegistry& Registry)
242
243
  Registry.RegisterHandler(TEXT("set_stringtable_entry"), &SetStringTableEntry);
243
244
  Registry.RegisterHandler(TEXT("remove_stringtable_entry"), &RemoveStringTableEntry);
244
245
  Registry.RegisterHandler(TEXT("import_stringtable"), &ImportStringTable);
246
+ Registry.RegisterHandler(TEXT("import_stringtable_csv"), &ImportStringTableCsv);
245
247
 
246
248
  // v0.7.8 stubs - FTS5-backed asset search
247
249
  Registry.RegisterHandler(TEXT("search_assets_fts"), &SearchAssetsFTS);
@@ -563,12 +565,10 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReadAsset(const TSharedPtr<FJsonObject>&
563
565
  FString AssetPath;
564
566
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
565
567
 
566
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
567
- if (!Asset)
568
- {
569
- // Fallback to LoadObject for full object paths
570
- Asset = LoadObject<UObject>(nullptr, *AssetPath);
571
- }
568
+ // The LoadAsset-then-LoadObject pair this action has always used now lives
569
+ // in MCPLoadAssetObject, so the type-specific readers resolve a path the
570
+ // same way this one does (#930).
571
+ UObject* Asset = MCPLoadAssetObject(AssetPath);
572
572
  if (!Asset)
573
573
  {
574
574
  return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
@@ -693,14 +693,21 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReadAsset(const TSharedPtr<FJsonObject>&
693
693
  // own properties (ParentClass, etc.) are rarely what a caller wants. Resolve to
694
694
  // the generated-class CDO so asset property reads/writes hit the real defaults
695
695
  // and can author Instanced sub-object arrays. Non-Blueprint assets pass through.
696
- static UObject* MCPResolveAssetToCDO(UObject* Asset)
696
+ //
697
+ // OutBlueprint carries the wrapper back out, because a write to the CDO has
698
+ // bookkeeping the CDO alone cannot do: the package to save is the Blueprint's,
699
+ // and the compiler's own record of the variable's default has to be reconciled
700
+ // with the value just written (#931).
701
+ static UObject* MCPResolveAssetToCDO(UObject* Asset, UBlueprint** OutBlueprint = nullptr)
697
702
  {
703
+ if (OutBlueprint) *OutBlueprint = nullptr;
698
704
  if (UBlueprint* BP = Cast<UBlueprint>(Asset))
699
705
  {
700
706
  if (UClass* GenClass = BP->GeneratedClass)
701
707
  {
702
708
  if (UObject* CDO = GenClass->GetDefaultObject())
703
709
  {
710
+ if (OutBlueprint) *OutBlueprint = BP;
704
711
  return CDO;
705
712
  }
706
713
  }
@@ -708,16 +715,124 @@ static UObject* MCPResolveAssetToCDO(UObject* Asset)
708
715
  return Asset;
709
716
  }
710
717
 
718
+ // #931: a property write marked the package dirty and stopped there, so it read
719
+ // back correctly, survived until the editor closed, and was gone on the next
720
+ // start. A GameplayAbility whose default reverted computed correct values and
721
+ // applied no effect, with nothing pointing at the cause. A write now either
722
+ // reaches the package on disk or says why it did not, and never reports plain
723
+ // success for a change that only exists in memory.
724
+ //
725
+ // Returns true when the package was written. On false, OutReason carries the
726
+ // sentence to hand back to the caller.
727
+ static bool MCPPersistAssetWrite(UObject* Asset, UBlueprint* OwningBlueprint, bool bSave, FString& OutReason)
728
+ {
729
+ OutReason.Reset();
730
+ UPackage* Package = Asset ? Asset->GetOutermost() : nullptr;
731
+ if (!Package)
732
+ {
733
+ OutReason = TEXT("The asset has no package, so there is nothing to write.");
734
+ return false;
735
+ }
736
+
737
+ const FString PackageName = Package->GetName();
738
+ if (!bSave)
739
+ {
740
+ OutReason = FString::Printf(
741
+ TEXT("save=false was requested, so '%s' is dirty in memory only and the change is lost when the editor closes. ")
742
+ TEXT("Call asset(save) for it, or repeat the write with save=true."),
743
+ *PackageName);
744
+ return false;
745
+ }
746
+ // Protected mounts and read-only package files are both answered before the
747
+ // engine is asked to write, by the one shared guard (#932): asking it to
748
+ // open a file it cannot is what took the editor down with a fatal error.
749
+ // For a Blueprint the package's asset is the UBlueprint; the CDO is one of
750
+ // its exports and rides along, so the guard and the save see the same
751
+ // object.
752
+ UObject* WriteTarget = OwningBlueprint ? static_cast<UObject*>(OwningBlueprint) : Asset;
753
+ return SaveAssetPackageChecked(WriteTarget, OutReason);
754
+ }
755
+
756
+ // A CDO write is a genuine package export and survives both the save and a
757
+ // later recompile, but two pieces of editor bookkeeping do not happen on their
758
+ // own, and both read as "my write did not take" (#931).
759
+ //
760
+ // UBlueprintGeneratedClass keeps a post-construct property list that newly
761
+ // spawned instances initialise from, and only MarkBlueprintAsModified rebuilds
762
+ // it. And FBPVariableDescription::DefaultValue, when it is not empty, is
763
+ // replayed onto the CDO by the next full compile AFTER the old CDO's values are
764
+ // copied forward, so a stale string default silently overwrites the value that
765
+ // was just written. Clearing it makes the CDO the single record of the default,
766
+ // which is what the Blueprint editor's own Class Defaults panel does.
767
+ static void MCPNoteBlueprintCDOWrite(UBlueprint* Blueprint, const FString& PropertyPath)
768
+ {
769
+ if (!Blueprint) return;
770
+
771
+ // The root segment is the variable the compiler knows by name. A dotted or
772
+ // indexed path lands inside that variable's value, and the string default
773
+ // it would replay covers the whole variable either way.
774
+ FString RootName = PropertyPath;
775
+ int32 Cut = INDEX_NONE;
776
+ if (RootName.FindChar(TEXT('.'), Cut)) RootName = RootName.Left(Cut);
777
+ if (RootName.FindChar(TEXT('['), Cut)) RootName = RootName.Left(Cut);
778
+ const FName RootVarName(*RootName);
779
+
780
+ FProperty* ChangedProperty = nullptr;
781
+ if (UClass* GenClass = Blueprint->GeneratedClass)
782
+ {
783
+ ChangedProperty = GenClass->FindPropertyByName(RootVarName);
784
+ }
785
+
786
+ for (FBPVariableDescription& Variable : Blueprint->NewVariables)
787
+ {
788
+ if (Variable.VarName == RootVarName)
789
+ {
790
+ Variable.DefaultValue.Empty();
791
+ break;
792
+ }
793
+ }
794
+
795
+ FPropertyChangedEvent ChangeEvent(ChangedProperty, EPropertyChangeType::ValueSet);
796
+ FBlueprintEditorUtils::MarkBlueprintAsModified(Blueprint, ChangeEvent);
797
+ }
798
+
799
+ /** Report where a property write ended up: the package, whether it reached
800
+ * disk, and the reason when it did not. A write the caller asked to persist
801
+ * and that did not persist is a failure, not a success with a footnote. */
802
+ static void MCPDescribePropertyWritePersistence(
803
+ TSharedPtr<FJsonObject>& Result,
804
+ UObject* Asset,
805
+ const FString& PropertyName,
806
+ bool bSaveRequested,
807
+ bool bPersisted,
808
+ const FString& PersistReason)
809
+ {
810
+ if (UPackage* Package = Asset ? Asset->GetOutermost() : nullptr)
811
+ {
812
+ Result->SetStringField(TEXT("packageName"), Package->GetName());
813
+ Result->SetBoolField(TEXT("packageDirty"), Package->IsDirty());
814
+ }
815
+ Result->SetBoolField(TEXT("persisted"), bPersisted);
816
+ Result->SetBoolField(TEXT("saved"), bPersisted);
817
+ if (bPersisted) return;
818
+
819
+ Result->SetStringField(TEXT("persistError"), PersistReason);
820
+ if (bSaveRequested)
821
+ {
822
+ Result->SetBoolField(TEXT("success"), false);
823
+ Result->SetStringField(TEXT("error"), FString::Printf(
824
+ TEXT("Set '%s' in memory but could not persist it: %s"), *PropertyName, *PersistReason));
825
+ }
826
+ }
827
+
711
828
  TSharedPtr<FJsonValue> FAssetHandlers::ReadAssetProperties(const TSharedPtr<FJsonObject>& Params)
712
829
  {
713
830
  FString AssetPath;
714
831
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
715
832
 
716
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
717
- if (!Asset)
718
- {
719
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
720
- }
833
+ TSharedPtr<FJsonValue> LoadError;
834
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
835
+ if (!Asset) return LoadError;
721
836
  Asset = MCPResolveAssetToCDO(Asset); // #568
722
837
 
723
838
  FString ValueFormat;
@@ -978,17 +1093,11 @@ TSharedPtr<FJsonValue> FAssetHandlers::DuplicateAsset(const TSharedPtr<FJsonObje
978
1093
  const FString OnConflict = OptionalString(Params, TEXT("onConflict"), TEXT("skip"));
979
1094
 
980
1095
  // #441: DoesAssetExist returns false for some Blueprints in 5.7 even when
981
- // the registry/loader can resolve them. Confirm via load-or-load_blueprint
1096
+ // the registry/loader can resolve them. Confirm via the shared resolver
982
1097
  // before erroring out so duplicate doesn't bounce off valid paths.
983
- UObject* SourceObj = UEditorAssetLibrary::LoadAsset(SourcePath);
984
- if (!SourceObj)
985
- {
986
- SourceObj = LoadObject<UObject>(nullptr, *SourcePath);
987
- }
988
- if (!SourceObj)
989
- {
990
- return MCPError(FString::Printf(TEXT("Source asset not found: %s"), *SourcePath));
991
- }
1098
+ TSharedPtr<FJsonValue> SourceLoadError;
1099
+ UObject* SourceObj = MCPRequireAssetObject(SourcePath, SourceLoadError, TEXT("Source asset"));
1100
+ if (!SourceObj) return SourceLoadError;
992
1101
 
993
1102
  // Idempotency: if the destination already exists, short-circuit.
994
1103
  if (UEditorAssetLibrary::DoesAssetExist(DestPath))
@@ -1150,7 +1259,7 @@ static TSharedPtr<FJsonValue> RenameWorldWithExternals(const FString& SourceAsse
1150
1259
  Gather(ExtActorsSrc, SrcActors);
1151
1260
  Gather(ExtObjectsSrc, SrcObjects);
1152
1261
 
1153
- UObject* World = UEditorAssetLibrary::LoadAsset(SourceAssetPath);
1262
+ UObject* World = MCPLoadAssetObject(SourceAssetPath);
1154
1263
  if (!World)
1155
1264
  {
1156
1265
  return MCPError(FString::Printf(TEXT("Failed to load World asset: %s. No changes made."), *SourceAssetPath));
@@ -1466,8 +1575,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::RenameAsset(const TSharedPtr<FJsonObject>
1466
1575
  return MCPError(TEXT("Missing 'sourcePath'+'destinationPath' or 'assetPath'+'newName'"));
1467
1576
  }
1468
1577
 
1469
- if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
1470
- if (MCPIsProtectedAssetPath(DestPath)) return MakeProtectedPathError(DestPath);
1578
+ if (MCPIsProtectedAssetPath(SourcePath)) return MCPProtectedPathError(SourcePath);
1579
+ if (MCPIsProtectedAssetPath(DestPath)) return MCPProtectedPathError(DestPath);
1471
1580
 
1472
1581
  // Idempotency: if already at destination, no-op.
1473
1582
  if (SourcePath == DestPath)
@@ -1561,7 +1670,7 @@ namespace
1561
1670
  UAssetEditorSubsystem* AES = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
1562
1671
  if (!AES) return false;
1563
1672
 
1564
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
1673
+ UObject* Asset = MCPLoadAssetObject(AssetPath);
1565
1674
  if (!Asset) return false;
1566
1675
 
1567
1676
  const TArray<IAssetEditorInstance*> Editors = AES->FindEditorsForAsset(Asset);
@@ -1573,6 +1682,89 @@ namespace
1573
1682
  return true;
1574
1683
  }
1575
1684
 
1685
+ // #976: UEditorAssetLibrary::DeleteAsset is the FORCE-delete entry point.
1686
+ // Its own header says so: "It doesn't check if the asset has references in
1687
+ // other Levels or by Actors." Both delete handlers called it whatever the
1688
+ // caller passed for `force`, so a referenced asset was destroyed and its
1689
+ // referencers were rewritten to point at nothing, with `force=false`
1690
+ // reading as a safety flag that did not exist.
1691
+ //
1692
+ // The Asset Registry holds the same reference graph the Content Browser's
1693
+ // reference viewer draws, so asking it first is what turns a force delete
1694
+ // back into a checked one. Self-references are dropped: a package always
1695
+ // lists itself.
1696
+ TArray<FString> CollectPackageReferencers(const FString& AssetPath)
1697
+ {
1698
+ TArray<FString> Out;
1699
+
1700
+ const FMCPAssetPathForms Forms = MCPAssetPathForms(AssetPath);
1701
+ if (Forms.PackagePath.IsEmpty()) return Out;
1702
+
1703
+ FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
1704
+ const FName PackageFName(*Forms.PackagePath);
1705
+ TArray<FName> Refs;
1706
+ ARM.Get().GetReferencers(PackageFName, Refs);
1707
+ for (const FName& R : Refs)
1708
+ {
1709
+ if (R != PackageFName)
1710
+ {
1711
+ Out.AddUnique(R.ToString());
1712
+ }
1713
+ }
1714
+ Out.Sort([](const FString& A, const FString& B) { return A < B; });
1715
+ return Out;
1716
+ }
1717
+
1718
+ /** The refusal a checked delete hands back. Names the packages that would
1719
+ * have been rewritten, so the caller can decide rather than discover. */
1720
+ void ApplyReferencerRefusalToJson(
1721
+ const TSharedPtr<FJsonObject>& Out,
1722
+ const FString& AssetPath,
1723
+ const TArray<FString>& Referencers)
1724
+ {
1725
+ // Long lists are the interesting case and truncating them would hide
1726
+ // exactly the referencer a caller is looking for, so the count is
1727
+ // reported alongside a bounded sample rather than a silent slice.
1728
+ constexpr int32 MaxNamed = 25;
1729
+ const int32 Named = FMath::Min(Referencers.Num(), MaxNamed);
1730
+
1731
+ TArray<FString> Sample;
1732
+ Sample.Append(Referencers.GetData(), Named);
1733
+
1734
+ Out->SetStringField(TEXT("error"), FString::Printf(
1735
+ TEXT("'%s' is referenced by %d package(s) and force=false, so it was not deleted. ")
1736
+ TEXT("Deleting it would rewrite those referencers to point at nothing. ")
1737
+ TEXT("Repoint or delete the referencers first, or pass force=true to delete anyway. Referencers: %s%s"),
1738
+ *AssetPath,
1739
+ Referencers.Num(),
1740
+ *FString::Join(Sample, TEXT(", ")),
1741
+ Referencers.Num() > Named ? TEXT(", ...") : TEXT("")));
1742
+ Out->SetStringField(TEXT("path"), AssetPath);
1743
+ Out->SetBoolField(TEXT("deleted"), false);
1744
+ Out->SetStringField(TEXT("reason"), TEXT("has_referencers"));
1745
+ Out->SetNumberField(TEXT("referencerCount"), Referencers.Num());
1746
+
1747
+ TArray<TSharedPtr<FJsonValue>> RefsJson;
1748
+ for (const FString& R : Referencers)
1749
+ {
1750
+ RefsJson.Add(MakeShared<FJsonValueString>(R));
1751
+ }
1752
+ Out->SetArrayField(TEXT("referencers"), RefsJson);
1753
+ }
1754
+
1755
+ /** Run the checked-delete gate for one path. Returns true and fills Out
1756
+ * with the refusal when the asset has referencers; false when the delete
1757
+ * may proceed. Both delete handlers go through this so the single and
1758
+ * batch forms cannot drift into enforcing different rules. */
1759
+ bool CheckedDeleteRefusal(const FString& AssetPath, const TSharedPtr<FJsonObject>& Out)
1760
+ {
1761
+ const TArray<FString> Referencers = CollectPackageReferencers(AssetPath);
1762
+ if (Referencers.IsEmpty()) return false;
1763
+ Out->SetStringField(TEXT("status"), TEXT("refused"));
1764
+ ApplyReferencerRefusalToJson(Out, AssetPath, Referencers);
1765
+ return true;
1766
+ }
1767
+
1576
1768
  FDeleteDiagnostics DiagnoseDeleteFailure(const FString& AssetPath)
1577
1769
  {
1578
1770
  FDeleteDiagnostics Diag;
@@ -1581,7 +1773,7 @@ namespace
1581
1773
  {
1582
1774
  if (UAssetEditorSubsystem* AES = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>())
1583
1775
  {
1584
- if (UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath))
1776
+ if (UObject* Asset = MCPLoadAssetObject(AssetPath))
1585
1777
  {
1586
1778
  Diag.bOpenInEditor = AES->FindEditorsForAsset(Asset).Num() > 0;
1587
1779
  }
@@ -1604,7 +1796,7 @@ namespace
1604
1796
  // #601: when there are no editors/on-disk referencers the delete still
1605
1797
  // fails for non-obvious reasons. Gather the common culprits so callers
1606
1798
  // get something actionable instead of a bare "unknown".
1607
- if (UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath))
1799
+ if (UObject* Asset = MCPLoadAssetObject(AssetPath))
1608
1800
  {
1609
1801
  // Live (in-memory) references beyond the asset's own package.
1610
1802
  FReferencerInformationList RefInfo;
@@ -1675,7 +1867,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAsset(const TSharedPtr<FJsonObject>
1675
1867
  FString AssetPath;
1676
1868
  if (auto Err = RequireStringAlt(Params, TEXT("path"), TEXT("assetPath"), AssetPath)) return Err;
1677
1869
 
1678
- if (MCPIsProtectedAssetPath(AssetPath)) return MakeProtectedPathError(AssetPath);
1870
+ if (MCPIsProtectedAssetPath(AssetPath)) return MCPProtectedPathError(AssetPath);
1679
1871
 
1680
1872
  const bool bForce = OptionalBool(Params, TEXT("force"), false);
1681
1873
 
@@ -1688,6 +1880,18 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAsset(const TSharedPtr<FJsonObject>
1688
1880
  return MCPResult(Result);
1689
1881
  }
1690
1882
 
1883
+ // #976: the delete below is a force delete, so the reference check has to
1884
+ // happen here rather than being left to an API that does not do one.
1885
+ if (!bForce)
1886
+ {
1887
+ TSharedPtr<FJsonObject> Refused = MakeShared<FJsonObject>();
1888
+ Refused->SetBoolField(TEXT("success"), false);
1889
+ if (CheckedDeleteRefusal(AssetPath, Refused))
1890
+ {
1891
+ return MCPResult(Refused);
1892
+ }
1893
+ }
1894
+
1691
1895
  bool bClosedEditor = false;
1692
1896
  if (bForce)
1693
1897
  {
@@ -1699,6 +1903,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAsset(const TSharedPtr<FJsonObject>
1699
1903
  auto Result = MCPSuccess();
1700
1904
  Result->SetStringField(TEXT("path"), AssetPath);
1701
1905
  Result->SetBoolField(TEXT("deleted"), bSuccess);
1906
+ Result->SetBoolField(TEXT("forced"), bForce);
1702
1907
  if (bClosedEditor)
1703
1908
  {
1704
1909
  Result->SetBoolField(TEXT("closedOpenEditor"), true);
@@ -1727,6 +1932,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAssetBatch(const TSharedPtr<FJsonOb
1727
1932
  int32 Deleted = 0;
1728
1933
  int32 Absent = 0;
1729
1934
  int32 Failed = 0;
1935
+ int32 Refused = 0;
1730
1936
  int32 ClosedEditors = 0;
1731
1937
 
1732
1938
  int32 Protected = 0;
@@ -1752,6 +1958,15 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAssetBatch(const TSharedPtr<FJsonOb
1752
1958
  Entry->SetStringField(TEXT("status"), TEXT("absent"));
1753
1959
  Absent++;
1754
1960
  }
1961
+ else if (!bForce && CheckedDeleteRefusal(Path, Entry))
1962
+ {
1963
+ // #976: same rule as the single delete. Per-asset, because one
1964
+ // referenced entry in a batch is not a reason to refuse the rest,
1965
+ // and a bare "failed" count never said which entry was skipped or
1966
+ // why. Status is its own value so a caller can tell a refusal
1967
+ // apart from a delete the editor attempted and could not finish.
1968
+ Refused++;
1969
+ }
1755
1970
  else
1756
1971
  {
1757
1972
  bool bClosed = false;
@@ -1781,6 +1996,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::DeleteAssetBatch(const TSharedPtr<FJsonOb
1781
1996
  Result->SetNumberField(TEXT("deleted"), Deleted);
1782
1997
  Result->SetNumberField(TEXT("absent"), Absent);
1783
1998
  Result->SetNumberField(TEXT("failed"), Failed);
1999
+ Result->SetNumberField(TEXT("refused"), Refused);
2000
+ Result->SetBoolField(TEXT("forced"), bForce);
1784
2001
  if (Protected > 0) Result->SetNumberField(TEXT("protected"), Protected);
1785
2002
  Result->SetNumberField(TEXT("total"), PerPath.Num());
1786
2003
  if (ClosedEditors > 0) Result->SetNumberField(TEXT("closedEditors"), ClosedEditors);
@@ -1891,7 +2108,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRename(const TSharedPtr<FJsonObject>&
1891
2108
  continue;
1892
2109
  }
1893
2110
 
1894
- UObject* Asset = UEditorAssetLibrary::LoadAsset(SourcePath);
2111
+ UObject* Asset = MCPLoadAssetObject(SourcePath);
1895
2112
  if (!Asset)
1896
2113
  {
1897
2114
  Record->SetStringField(TEXT("status"), TEXT("not_found"));
@@ -1948,6 +2165,48 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRename(const TSharedPtr<FJsonObject>&
1948
2165
  }
1949
2166
  }
1950
2167
 
2168
+ // #908: RenameAssets runs one redirector fix-up pass, and a referencer that
2169
+ // is not loaded is invisible to it, so a stub is left at the old path with
2170
+ // nothing in the response saying so. 29 renames left 19 of them, and the
2171
+ // first sign of it was a later search finding redirectors nobody expected.
2172
+ IAssetRegistry& RenameRegistry =
2173
+ FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
2174
+ int32 RedirectorsLeft = 0;
2175
+ TArray<TSharedPtr<FJsonValue>> RedirectorPackages;
2176
+ for (const TSharedPtr<FJsonValue>& Value : PerItem)
2177
+ {
2178
+ TSharedPtr<FJsonObject> Rec = Value.IsValid() ? Value->AsObject() : nullptr;
2179
+ if (!Rec.IsValid()) continue;
2180
+ FString Status;
2181
+ Rec->TryGetStringField(TEXT("status"), Status);
2182
+ if (Status != TEXT("renamed")) continue;
2183
+
2184
+ FString SourcePath;
2185
+ Rec->TryGetStringField(TEXT("sourcePath"), SourcePath);
2186
+ const FMCPAssetPathForms SourceForms = MCPAssetPathForms(SourcePath);
2187
+
2188
+ bool bRedirectorLeft = false;
2189
+ TArray<FAssetData> AtOldPath;
2190
+ RenameRegistry.GetAssetsByPackageName(FName(*SourceForms.PackagePath), AtOldPath);
2191
+ for (const FAssetData& Data : AtOldPath)
2192
+ {
2193
+ if (Data.IsRedirector()) { bRedirectorLeft = true; break; }
2194
+ }
2195
+ // The registry can lag a rename that just happened, so an in-memory
2196
+ // redirector counts too rather than reading as a clean result.
2197
+ if (!bRedirectorLeft && FindObject<UObjectRedirector>(nullptr, *SourceForms.ObjectPath))
2198
+ {
2199
+ bRedirectorLeft = true;
2200
+ }
2201
+
2202
+ Rec->SetBoolField(TEXT("redirectorLeft"), bRedirectorLeft);
2203
+ if (bRedirectorLeft)
2204
+ {
2205
+ ++RedirectorsLeft;
2206
+ RedirectorPackages.Add(MakeShared<FJsonValueString>(SourceForms.PackagePath));
2207
+ }
2208
+ }
2209
+
1951
2210
  auto Result = MCPSuccess();
1952
2211
  if (Succeeded > 0) MCPSetUpdated(Result);
1953
2212
  Result->SetBoolField(TEXT("success"), bOk);
@@ -1956,6 +2215,16 @@ TSharedPtr<FJsonValue> FAssetHandlers::BulkRename(const TSharedPtr<FJsonObject>&
1956
2215
  Result->SetNumberField(TEXT("skipped"), Skipped);
1957
2216
  Result->SetNumberField(TEXT("total"), PerItem.Num());
1958
2217
  Result->SetArrayField(TEXT("results"), PerItem);
2218
+ Result->SetNumberField(TEXT("redirectorsLeft"), RedirectorsLeft);
2219
+ Result->SetNumberField(TEXT("redirectorsRemoved"), FMath::Max(0, Succeeded - RedirectorsLeft));
2220
+ Result->SetArrayField(TEXT("redirectorPackages"), RedirectorPackages);
2221
+ if (RedirectorsLeft > 0)
2222
+ {
2223
+ Result->SetStringField(TEXT("note"), FString::Printf(
2224
+ TEXT("%d of %d renamed assets left an ObjectRedirector at the old path, still pointed at by packages this rename did not load. ")
2225
+ TEXT("Pass redirectorPackages to asset(fixup_redirectors) to load exactly those referencers, rewrite them, and delete the stubs that come out unreferenced."),
2226
+ RedirectorsLeft, Succeeded));
2227
+ }
1959
2228
  return MCPResult(Result);
1960
2229
  }
1961
2230
 
@@ -2160,7 +2429,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::SaveAsset(const TSharedPtr<FJsonObject>&
2160
2429
  UObject* Asset = LoadAssetByPath<UObject>(AssetPath);
2161
2430
  if (!Asset)
2162
2431
  {
2163
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
2432
+ return MCPAssetNotFoundError(AssetPath);
2164
2433
  }
2165
2434
  UPackage* Package = Asset->GetOutermost();
2166
2435
  if (!Package)
@@ -2175,7 +2444,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::SaveAsset(const TSharedPtr<FJsonObject>&
2175
2444
  else
2176
2445
  {
2177
2446
  bSuccess = UEditorAssetLibrary::SaveAsset(AssetPath);
2178
- if (UObject* Asset = FindObject<UObject>(nullptr, *AssetPath))
2447
+ if (UObject* Asset = MCPLoadAssetObject(AssetPath))
2179
2448
  {
2180
2449
  if (UPackage* Package = Asset->GetOutermost())
2181
2450
  {
@@ -2287,11 +2556,9 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReloadPackage(const TSharedPtr<FJsonObjec
2287
2556
  FString AssetPath;
2288
2557
  if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
2289
2558
 
2290
- UObject* Asset = UEditorAssetLibrary::LoadAsset(AssetPath);
2291
- if (!Asset)
2292
- {
2293
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *AssetPath));
2294
- }
2559
+ TSharedPtr<FJsonValue> LoadError;
2560
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
2561
+ if (!Asset) return LoadError;
2295
2562
 
2296
2563
  UPackage* Package = Asset->GetOutermost();
2297
2564
  if (!Package)
@@ -2312,7 +2579,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ReloadPackage(const TSharedPtr<FJsonObjec
2312
2579
  CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
2313
2580
 
2314
2581
  // Reload
2315
- UObject* Reloaded = UEditorAssetLibrary::LoadAsset(AssetPath);
2582
+ UObject* Reloaded = MCPLoadAssetObject(AssetPath);
2316
2583
  bSuccess = (Reloaded != nullptr);
2317
2584
  }
2318
2585
 
@@ -2533,8 +2800,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::MoveFolder(const TSharedPtr<FJsonObject>&
2533
2800
  SourcePath.RemoveFromEnd(TEXT("/"));
2534
2801
  DestinationPath.RemoveFromEnd(TEXT("/"));
2535
2802
 
2536
- if (MCPIsProtectedAssetPath(SourcePath)) return MakeProtectedPathError(SourcePath);
2537
- if (MCPIsProtectedAssetPath(DestinationPath)) return MakeProtectedPathError(DestinationPath);
2803
+ if (MCPIsProtectedAssetPath(SourcePath)) return MCPProtectedPathError(SourcePath);
2804
+ if (MCPIsProtectedAssetPath(DestinationPath)) return MCPProtectedPathError(DestinationPath);
2538
2805
 
2539
2806
  // Scan source path to discover all assets
2540
2807
  IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry")).Get();
@@ -2809,7 +3076,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::HealthCheck(const TSharedPtr<FJsonObject>
2809
3076
  bool bCanLoad = bIsLoaded;
2810
3077
  if (!bIsLoaded)
2811
3078
  {
2812
- UObject* Probe = UEditorAssetLibrary::LoadAsset(AssetPath);
3079
+ UObject* Probe = MCPLoadAssetObject(AssetPath);
2813
3080
  bCanLoad = Probe != nullptr;
2814
3081
  if (Probe) InMemory = Probe;
2815
3082
  }
@@ -2838,7 +3105,8 @@ TSharedPtr<FJsonValue> FAssetHandlers::ForceReload(const TSharedPtr<FJsonObject>
2838
3105
  FString AssetPath;
2839
3106
  if (auto Err = RequireStringAlt(Params, TEXT("assetPath"), TEXT("path"), AssetPath)) return Err;
2840
3107
 
2841
- const FString PackageName = FPackageName::ObjectPathToPackageName(AssetPath);
3108
+ const FMCPAssetPathForms Forms = MCPAssetPathForms(AssetPath);
3109
+ const FString PackageName = Forms.PackagePath;
2842
3110
  FString PackageFileName;
2843
3111
  if (!FPackageName::DoesPackageExist(PackageName, &PackageFileName))
2844
3112
  {
@@ -2851,7 +3119,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ForceReload(const TSharedPtr<FJsonObject>
2851
3119
  {
2852
3120
  if (UAssetEditorSubsystem* AES = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>())
2853
3121
  {
2854
- if (UObject* Existing = StaticFindObject(UObject::StaticClass(), nullptr, *AssetPath))
3122
+ if (UObject* Existing = StaticFindObject(UObject::StaticClass(), nullptr, *Forms.ObjectPath))
2855
3123
  {
2856
3124
  if (AES->FindEditorsForAsset(Existing).Num() > 0)
2857
3125
  {
@@ -2869,7 +3137,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ForceReload(const TSharedPtr<FJsonObject>
2869
3137
  // package, recompiles the Blueprint and reinstances against the new class.
2870
3138
  // A package the editor refuses to release is reported, not papered over.
2871
3139
  UPackage* ExistingPkg = FindPackage(nullptr, *PackageName);
2872
- UObject* PreviousObject = StaticFindObject(UObject::StaticClass(), nullptr, *AssetPath);
3140
+ UObject* PreviousObject = StaticFindObject(UObject::StaticClass(), nullptr, *Forms.ObjectPath);
2873
3141
  const TWeakObjectPtr<UObject> PreviousWeak(PreviousObject);
2874
3142
  const bool bWasDirty = ExistingPkg != nullptr && ExistingPkg->IsDirty();
2875
3143
  const bool bDiscardUnsaved = OptionalBool(Params, TEXT("discardUnsaved"), false);
@@ -2913,7 +3181,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::ForceReload(const TSharedPtr<FJsonObject>
2913
3181
  CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
2914
3182
  }
2915
3183
 
2916
- UObject* Reloaded = LoadObject<UObject>(nullptr, *AssetPath, nullptr, LOAD_None);
3184
+ UObject* Reloaded = LoadObject<UObject>(nullptr, *Forms.ObjectPath, nullptr, LOAD_None);
2917
3185
  const bool bSuccess = Reloaded != nullptr;
2918
3186
  // A package that was already in memory must come back as a new object for
2919
3187
  // the read to be trustworthy. Same pointer means the editor kept its copy.
@@ -2968,12 +3236,15 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetAssetProperty(const TSharedPtr<FJsonOb
2968
3236
  return MCPError(TEXT("Missing 'value' parameter"));
2969
3237
  }
2970
3238
 
2971
- UObject* Asset = LoadObject<UObject>(nullptr, *AssetPath);
2972
- if (!Asset)
2973
- {
2974
- return MCPError(FString::Printf(TEXT("Could not load asset '%s'"), *AssetPath));
2975
- }
2976
- Asset = MCPResolveAssetToCDO(Asset); // #568 - author the generated-class CDO for Blueprint paths
3239
+ // #931: default to persisting. A write that only marked the package dirty
3240
+ // was the whole defect, so opting out has to be deliberate.
3241
+ const bool bSave = OptionalBool(Params, TEXT("save"), true);
3242
+
3243
+ TSharedPtr<FJsonValue> LoadError;
3244
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
3245
+ if (!Asset) return LoadError;
3246
+ UBlueprint* OwningBlueprint = nullptr;
3247
+ Asset = MCPResolveAssetToCDO(Asset, &OwningBlueprint); // #568 - author the generated-class CDO for Blueprint paths
2977
3248
 
2978
3249
  // Resolve the (possibly indexed, possibly subobject-descending) path.
2979
3250
  // Supports "Config.Traits[1].Params.RepresentationActorManagementClass"
@@ -3010,16 +3281,21 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetAssetProperty(const TSharedPtr<FJsonOb
3010
3281
  if (LeafOwner) LeafOwner->PostEditChange();
3011
3282
  Asset->PostEditChange();
3012
3283
  Asset->MarkPackageDirty();
3284
+ MCPNoteBlueprintCDOWrite(OwningBlueprint, PropertyName);
3013
3285
 
3014
3286
  FString NewValue;
3015
3287
  FinalProp->ExportText_Direct(NewValue, ValuePtr, ValuePtr, nullptr, PPF_None);
3016
3288
 
3289
+ FString PersistReason;
3290
+ const bool bPersisted = MCPPersistAssetWrite(Asset, OwningBlueprint, bSave, PersistReason);
3291
+
3017
3292
  auto Result = MCPSuccess();
3018
3293
  MCPSetUpdated(Result);
3019
3294
  Result->SetStringField(TEXT("assetPath"), AssetPath);
3020
3295
  Result->SetStringField(TEXT("propertyName"), PropertyName);
3021
3296
  Result->SetStringField(TEXT("previousValue"), PrevValue);
3022
3297
  Result->SetStringField(TEXT("value"), NewValue);
3298
+ MCPDescribePropertyWritePersistence(Result, Asset, PropertyName, bSave, bPersisted, PersistReason);
3023
3299
  if (bMapBearing)
3024
3300
  {
3025
3301
  Result->SetNumberField(TEXT("mapPairCount"), MCPPropertyText::CountMapPairs(FinalProp, ValuePtr));
@@ -3061,12 +3337,13 @@ TSharedPtr<FJsonValue> FAssetHandlers::AppendAssetArrayElements(const TSharedPtr
3061
3337
  return MCPError(TEXT("'elements' must contain at least one value"));
3062
3338
  }
3063
3339
 
3064
- UObject* Asset = LoadObject<UObject>(nullptr, *AssetPath);
3065
- if (!Asset)
3066
- {
3067
- return MCPError(FString::Printf(TEXT("Could not load asset '%s'"), *AssetPath));
3068
- }
3069
- Asset = MCPResolveAssetToCDO(Asset);
3340
+ const bool bSave = OptionalBool(Params, TEXT("save"), true);
3341
+
3342
+ TSharedPtr<FJsonValue> LoadError;
3343
+ UObject* Asset = MCPRequireAssetObject(AssetPath, LoadError);
3344
+ if (!Asset) return LoadError;
3345
+ UBlueprint* OwningBlueprint = nullptr;
3346
+ Asset = MCPResolveAssetToCDO(Asset, &OwningBlueprint);
3070
3347
 
3071
3348
  FProperty* FinalProp = nullptr;
3072
3349
  void* ValuePtr = nullptr;
@@ -3139,6 +3416,10 @@ TSharedPtr<FJsonValue> FAssetHandlers::AppendAssetArrayElements(const TSharedPtr
3139
3416
  if (LeafOwner && LeafOwner != Asset) LeafOwner->PostEditChange();
3140
3417
  Asset->PostEditChange();
3141
3418
  Asset->MarkPackageDirty();
3419
+ MCPNoteBlueprintCDOWrite(OwningBlueprint, PropertyName);
3420
+
3421
+ FString PersistReason;
3422
+ const bool bPersisted = MCPPersistAssetWrite(Asset, OwningBlueprint, bSave, PersistReason);
3142
3423
 
3143
3424
  TArray<TSharedPtr<FJsonValue>> AppendedIndices;
3144
3425
  AppendedIndices.Reserve(AppendedCount);
@@ -3157,7 +3438,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::AppendAssetArrayElements(const TSharedPtr
3157
3438
  Result->SetNumberField(TEXT("newNum"), PreviousNum + AppendedCount);
3158
3439
  Result->SetArrayField(TEXT("appendedIndices"), AppendedIndices);
3159
3440
  Result->SetField(TEXT("previousValue"), PreviousValue);
3160
- Result->SetBoolField(TEXT("saved"), false);
3441
+ MCPDescribePropertyWritePersistence(Result, Asset, PropertyName, bSave, bPersisted, PersistReason);
3161
3442
 
3162
3443
  TSharedPtr<FJsonObject> Payload = MakeShared<FJsonObject>();
3163
3444
  Payload->SetStringField(TEXT("assetPath"), AssetPath);
@@ -3221,7 +3502,7 @@ TSharedPtr<FJsonValue> FAssetHandlers::SetTextureSettingsByType(const TSharedPtr
3221
3502
  {
3222
3503
  FString TexPath;
3223
3504
  if (!V->TryGetString(TexPath)) continue;
3224
- UTexture2D* Tex = LoadObject<UTexture2D>(nullptr, *TexPath);
3505
+ UTexture2D* Tex = LoadAssetByPath<UTexture2D>(TexPath);
3225
3506
  if (!Tex)
3226
3507
  {
3227
3508
  TSharedPtr<FJsonObject> F = MakeShared<FJsonObject>();
@@ -3453,11 +3734,9 @@ TSharedPtr<FJsonValue> FAssetHandlers::MigrateAssets(const TSharedPtr<FJsonObjec
3453
3734
  TArray<TSharedPtr<FJsonValue>> Resolved;
3454
3735
  for (const FString& Path : AssetPaths)
3455
3736
  {
3456
- UObject* Asset = UEditorAssetLibrary::LoadAsset(Path);
3457
- if (!Asset)
3458
- {
3459
- return MCPError(FString::Printf(TEXT("Asset not found: %s"), *Path));
3460
- }
3737
+ TSharedPtr<FJsonValue> LoadError;
3738
+ UObject* Asset = MCPRequireAssetObject(Path, LoadError);
3739
+ if (!Asset) return LoadError;
3461
3740
  UPackage* Package = Asset->GetOutermost();
3462
3741
  const FName PackageName(*Package->GetName());
3463
3742