@strapi/upload 5.58.0 → 5.59.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 (226) hide show
  1. package/dist/admin/components/MediaLibraryInput/AddAssetMenu.js +141 -0
  2. package/dist/admin/components/MediaLibraryInput/AddAssetMenu.js.map +1 -0
  3. package/dist/admin/components/MediaLibraryInput/AddAssetMenu.mjs +139 -0
  4. package/dist/admin/components/MediaLibraryInput/AddAssetMenu.mjs.map +1 -0
  5. package/dist/admin/components/MediaLibraryInput/AssetDropZone.js +129 -0
  6. package/dist/admin/components/MediaLibraryInput/AssetDropZone.js.map +1 -0
  7. package/dist/admin/components/MediaLibraryInput/AssetDropZone.mjs +127 -0
  8. package/dist/admin/components/MediaLibraryInput/AssetDropZone.mjs.map +1 -0
  9. package/dist/admin/components/MediaLibraryInput/AssetRow.js +144 -0
  10. package/dist/admin/components/MediaLibraryInput/AssetRow.js.map +1 -0
  11. package/dist/admin/components/MediaLibraryInput/AssetRow.mjs +142 -0
  12. package/dist/admin/components/MediaLibraryInput/AssetRow.mjs.map +1 -0
  13. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js +397 -0
  14. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js.map +1 -0
  15. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs +395 -0
  16. package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs.map +1 -0
  17. package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.js +45 -0
  18. package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.js.map +1 -0
  19. package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.mjs +43 -0
  20. package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.mjs.map +1 -0
  21. package/dist/admin/hooks/useAIMetadataAvailability.js +5 -1
  22. package/dist/admin/hooks/useAIMetadataAvailability.js.map +1 -1
  23. package/dist/admin/hooks/useAIMetadataAvailability.mjs +5 -1
  24. package/dist/admin/hooks/useAIMetadataAvailability.mjs.map +1 -1
  25. package/dist/admin/hooks/useAIMetadataEnabled.js +7 -1
  26. package/dist/admin/hooks/useAIMetadataEnabled.js.map +1 -1
  27. package/dist/admin/hooks/useAIMetadataEnabled.mjs +7 -1
  28. package/dist/admin/hooks/useAIMetadataEnabled.mjs.map +1 -1
  29. package/dist/admin/hooks/useMediaLibraryPermissions.js +4 -0
  30. package/dist/admin/hooks/useMediaLibraryPermissions.js.map +1 -1
  31. package/dist/admin/hooks/useMediaLibraryPermissions.mjs +4 -0
  32. package/dist/admin/hooks/useMediaLibraryPermissions.mjs.map +1 -1
  33. package/dist/admin/hooks/useTracking.js +5 -1
  34. package/dist/admin/hooks/useTracking.js.map +1 -1
  35. package/dist/admin/hooks/useTracking.mjs +5 -1
  36. package/dist/admin/hooks/useTracking.mjs.map +1 -1
  37. package/dist/admin/index.js +7 -2
  38. package/dist/admin/index.js.map +1 -1
  39. package/dist/admin/index.mjs +7 -2
  40. package/dist/admin/index.mjs.map +1 -1
  41. package/dist/admin/legacy/components/AssetCard/AssetCardBase.js.map +1 -1
  42. package/dist/admin/legacy/components/AssetCard/AssetCardBase.mjs.map +1 -1
  43. package/dist/admin/legacy/components/AssetCard/VideoPreview.js +2 -1
  44. package/dist/admin/legacy/components/AssetCard/VideoPreview.js.map +1 -1
  45. package/dist/admin/legacy/components/AssetCard/VideoPreview.mjs +2 -1
  46. package/dist/admin/legacy/components/AssetCard/VideoPreview.mjs.map +1 -1
  47. package/dist/admin/legacy/components/AssetDialog/AssetDialog.js +3 -1
  48. package/dist/admin/legacy/components/AssetDialog/AssetDialog.js.map +1 -1
  49. package/dist/admin/legacy/components/AssetDialog/AssetDialog.mjs +3 -1
  50. package/dist/admin/legacy/components/AssetDialog/AssetDialog.mjs.map +1 -1
  51. package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.js +3 -1
  52. package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.js.map +1 -1
  53. package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.mjs +3 -1
  54. package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.mjs.map +1 -1
  55. package/dist/admin/legacy/components/AssetDialog/BrowseStep/Filters.js.map +1 -1
  56. package/dist/admin/legacy/components/AssetDialog/BrowseStep/Filters.mjs.map +1 -1
  57. package/dist/admin/legacy/components/AssetGridList/AssetGridList.js +7 -6
  58. package/dist/admin/legacy/components/AssetGridList/AssetGridList.js.map +1 -1
  59. package/dist/admin/legacy/components/AssetGridList/AssetGridList.mjs +7 -6
  60. package/dist/admin/legacy/components/AssetGridList/AssetGridList.mjs.map +1 -1
  61. package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.js +2 -1
  62. package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.js.map +1 -1
  63. package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.mjs +2 -1
  64. package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.mjs.map +1 -1
  65. package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.js +2 -1
  66. package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.js.map +1 -1
  67. package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.mjs +2 -1
  68. package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.mjs.map +1 -1
  69. package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.js +3 -1
  70. package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.js.map +1 -1
  71. package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.mjs +3 -1
  72. package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
  73. package/dist/admin/legacy/components/FilterList/FilterList.js.map +1 -1
  74. package/dist/admin/legacy/components/FilterList/FilterList.mjs.map +1 -1
  75. package/dist/admin/legacy/components/FilterPopover/FilterPopover.js.map +1 -1
  76. package/dist/admin/legacy/components/FilterPopover/FilterPopover.mjs.map +1 -1
  77. package/dist/admin/legacy/components/FilterPopover/FilterValueInput.js +2 -1
  78. package/dist/admin/legacy/components/FilterPopover/FilterValueInput.js.map +1 -1
  79. package/dist/admin/legacy/components/FilterPopover/FilterValueInput.mjs +2 -1
  80. package/dist/admin/legacy/components/FilterPopover/FilterValueInput.mjs.map +1 -1
  81. package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.js +4 -2
  82. package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.js.map +1 -1
  83. package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.mjs +4 -2
  84. package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.mjs.map +1 -1
  85. package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.js +2 -1
  86. package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.js.map +1 -1
  87. package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.mjs +2 -1
  88. package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.mjs.map +1 -1
  89. package/dist/admin/legacy/components/SelectTree/SelectTree.js +3 -1
  90. package/dist/admin/legacy/components/SelectTree/SelectTree.js.map +1 -1
  91. package/dist/admin/legacy/components/SelectTree/SelectTree.mjs +3 -1
  92. package/dist/admin/legacy/components/SelectTree/SelectTree.mjs.map +1 -1
  93. package/dist/admin/legacy/components/TableList/TableList.js +3 -1
  94. package/dist/admin/legacy/components/TableList/TableList.js.map +1 -1
  95. package/dist/admin/legacy/components/TableList/TableList.mjs +3 -1
  96. package/dist/admin/legacy/components/TableList/TableList.mjs.map +1 -1
  97. package/dist/admin/legacy/components/TableList/TableRows.js +3 -1
  98. package/dist/admin/legacy/components/TableList/TableRows.js.map +1 -1
  99. package/dist/admin/legacy/components/TableList/TableRows.mjs +3 -1
  100. package/dist/admin/legacy/components/TableList/TableRows.mjs.map +1 -1
  101. package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.js +3 -1
  102. package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.js.map +1 -1
  103. package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.mjs +3 -1
  104. package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.mjs.map +1 -1
  105. package/dist/admin/legacy/pages/App/ConfigureTheView/ConfigureTheView.js.map +1 -1
  106. package/dist/admin/legacy/pages/App/ConfigureTheView/ConfigureTheView.mjs.map +1 -1
  107. package/dist/admin/legacy/pages/App/components/BulkActions.js +2 -1
  108. package/dist/admin/legacy/pages/App/components/BulkActions.js.map +1 -1
  109. package/dist/admin/legacy/pages/App/components/BulkActions.mjs +2 -1
  110. package/dist/admin/legacy/pages/App/components/BulkActions.mjs.map +1 -1
  111. package/dist/admin/legacy/pages/App/components/BulkMoveButton.js +2 -1
  112. package/dist/admin/legacy/pages/App/components/BulkMoveButton.js.map +1 -1
  113. package/dist/admin/legacy/pages/App/components/BulkMoveButton.mjs +2 -1
  114. package/dist/admin/legacy/pages/App/components/BulkMoveButton.mjs.map +1 -1
  115. package/dist/admin/pages/Assets/components/AssetsGrid.js +2 -1
  116. package/dist/admin/pages/Assets/components/AssetsGrid.js.map +1 -1
  117. package/dist/admin/pages/Assets/components/AssetsGrid.mjs +2 -1
  118. package/dist/admin/pages/Assets/components/AssetsGrid.mjs.map +1 -1
  119. package/dist/admin/pages/Assets/components/AssetsTable.js +2 -1
  120. package/dist/admin/pages/Assets/components/AssetsTable.js.map +1 -1
  121. package/dist/admin/pages/Assets/components/AssetsTable.mjs +2 -1
  122. package/dist/admin/pages/Assets/components/AssetsTable.mjs.map +1 -1
  123. package/dist/admin/pages/Assets/components/BulkActionsBar.js +3 -1
  124. package/dist/admin/pages/Assets/components/BulkActionsBar.js.map +1 -1
  125. package/dist/admin/pages/Assets/components/BulkActionsBar.mjs +3 -1
  126. package/dist/admin/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
  127. package/dist/admin/pages/Assets/components/DateRangeCalendar.js +10 -7
  128. package/dist/admin/pages/Assets/components/DateRangeCalendar.js.map +1 -1
  129. package/dist/admin/pages/Assets/components/DateRangeCalendar.mjs +10 -7
  130. package/dist/admin/pages/Assets/components/DateRangeCalendar.mjs.map +1 -1
  131. package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.js +4 -2
  132. package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
  133. package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +5 -3
  134. package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
  135. package/dist/admin/pages/Assets/components/FilterBadges.js +32 -3
  136. package/dist/admin/pages/Assets/components/FilterBadges.js.map +1 -1
  137. package/dist/admin/pages/Assets/components/FilterBadges.mjs +33 -4
  138. package/dist/admin/pages/Assets/components/FilterBadges.mjs.map +1 -1
  139. package/dist/admin/services/api.js +15 -5
  140. package/dist/admin/services/api.js.map +1 -1
  141. package/dist/admin/services/api.mjs +15 -5
  142. package/dist/admin/services/api.mjs.map +1 -1
  143. package/dist/admin/src/components/MediaLibraryInput/AddAssetMenu.d.ts +19 -0
  144. package/dist/admin/src/components/MediaLibraryInput/AssetDropZone.d.ts +13 -0
  145. package/dist/admin/src/components/MediaLibraryInput/AssetRow.d.ts +15 -0
  146. package/dist/admin/src/components/MediaLibraryInput/MediaLibraryInput.d.ts +26 -0
  147. package/dist/admin/src/components/MediaLibraryInput/NoLibraryAccessDialog.d.ts +10 -0
  148. package/dist/admin/src/hooks/useMediaLibraryPermissions.d.ts +1 -0
  149. package/dist/admin/src/legacy/components/BulkMoveDialog/BulkMoveDialog.d.ts +1 -1
  150. package/dist/admin/src/legacy/pages/App/components/BulkActions.d.ts +1 -1
  151. package/dist/admin/src/services/api.d.ts +6 -0
  152. package/dist/admin/src/utils/allowedMediaTypes.d.ts +16 -0
  153. package/dist/admin/translations/ca.json.js +3 -3
  154. package/dist/admin/translations/ca.json.mjs +3 -3
  155. package/dist/admin/translations/da.json.js +1 -1
  156. package/dist/admin/translations/da.json.mjs +1 -1
  157. package/dist/admin/translations/de.json.js +1 -1
  158. package/dist/admin/translations/de.json.mjs +1 -1
  159. package/dist/admin/translations/en.json.js +9 -0
  160. package/dist/admin/translations/en.json.js.map +1 -1
  161. package/dist/admin/translations/en.json.mjs +9 -0
  162. package/dist/admin/translations/en.json.mjs.map +1 -1
  163. package/dist/admin/translations/es.json.js +1 -1
  164. package/dist/admin/translations/es.json.mjs +1 -1
  165. package/dist/admin/translations/sk.json.js +2 -2
  166. package/dist/admin/translations/sk.json.mjs +2 -2
  167. package/dist/admin/translations/tr.json.js +2 -2
  168. package/dist/admin/translations/tr.json.mjs +2 -2
  169. package/dist/admin/translations/zh-Hans.json.js +1 -1
  170. package/dist/admin/translations/zh-Hans.json.mjs +1 -1
  171. package/dist/admin/translations/zh.json.js +2 -2
  172. package/dist/admin/translations/zh.json.mjs +2 -2
  173. package/dist/admin/utils/allowedMediaTypes.js +42 -0
  174. package/dist/admin/utils/allowedMediaTypes.js.map +1 -0
  175. package/dist/admin/utils/allowedMediaTypes.mjs +39 -0
  176. package/dist/admin/utils/allowedMediaTypes.mjs.map +1 -0
  177. package/dist/server/config.js +6 -0
  178. package/dist/server/config.js.map +1 -1
  179. package/dist/server/config.mjs +6 -0
  180. package/dist/server/config.mjs.map +1 -1
  181. package/dist/server/controllers/validation/admin/upload.js.map +1 -1
  182. package/dist/server/controllers/validation/admin/upload.mjs.map +1 -1
  183. package/dist/server/controllers/validation/content-api/upload.js.map +1 -1
  184. package/dist/server/controllers/validation/content-api/upload.mjs.map +1 -1
  185. package/dist/server/mcp/ambient-instance.js +7 -5
  186. package/dist/server/mcp/ambient-instance.js.map +1 -1
  187. package/dist/server/mcp/ambient-instance.mjs +7 -5
  188. package/dist/server/mcp/ambient-instance.mjs.map +1 -1
  189. package/dist/server/mcp/handlers/folder-handlers.js +4 -16
  190. package/dist/server/mcp/handlers/folder-handlers.js.map +1 -1
  191. package/dist/server/mcp/handlers/folder-handlers.mjs +5 -17
  192. package/dist/server/mcp/handlers/folder-handlers.mjs.map +1 -1
  193. package/dist/server/mcp/handlers/read-handlers.js +6 -14
  194. package/dist/server/mcp/handlers/read-handlers.js.map +1 -1
  195. package/dist/server/mcp/handlers/read-handlers.mjs +7 -15
  196. package/dist/server/mcp/handlers/read-handlers.mjs.map +1 -1
  197. package/dist/server/mcp/handlers/write-handlers.js +3 -13
  198. package/dist/server/mcp/handlers/write-handlers.js.map +1 -1
  199. package/dist/server/mcp/handlers/write-handlers.mjs +3 -13
  200. package/dist/server/mcp/handlers/write-handlers.mjs.map +1 -1
  201. package/dist/server/mcp/register-upload-mcp-tools.js +215 -195
  202. package/dist/server/mcp/register-upload-mcp-tools.js.map +1 -1
  203. package/dist/server/mcp/register-upload-mcp-tools.mjs +215 -195
  204. package/dist/server/mcp/register-upload-mcp-tools.mjs.map +1 -1
  205. package/dist/server/services/image-manipulation.js +33 -14
  206. package/dist/server/services/image-manipulation.js.map +1 -1
  207. package/dist/server/services/image-manipulation.mjs +33 -14
  208. package/dist/server/services/image-manipulation.mjs.map +1 -1
  209. package/dist/server/src/config.d.ts +3 -0
  210. package/dist/server/src/config.d.ts.map +1 -1
  211. package/dist/server/src/controllers/validation/admin/upload.d.ts.map +1 -1
  212. package/dist/server/src/controllers/validation/content-api/upload.d.ts.map +1 -1
  213. package/dist/server/src/index.d.ts +3 -0
  214. package/dist/server/src/index.d.ts.map +1 -1
  215. package/dist/server/src/mcp/ambient-instance.d.ts +7 -5
  216. package/dist/server/src/mcp/ambient-instance.d.ts.map +1 -1
  217. package/dist/server/src/mcp/handlers/folder-handlers.d.ts.map +1 -1
  218. package/dist/server/src/mcp/handlers/read-handlers.d.ts +3 -2
  219. package/dist/server/src/mcp/handlers/read-handlers.d.ts.map +1 -1
  220. package/dist/server/src/mcp/handlers/write-handlers.d.ts.map +1 -1
  221. package/dist/server/src/mcp/register-upload-mcp-tools.d.ts +9 -14
  222. package/dist/server/src/mcp/register-upload-mcp-tools.d.ts.map +1 -1
  223. package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
  224. package/dist/server/src/types.d.ts +1 -0
  225. package/dist/server/src/types.d.ts.map +1 -1
  226. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"write-handlers.js","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertAmbientInstance } from '../ambient-instance';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n};\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n};\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n};\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","assertAmbientInstance","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":"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"}
1
+ {"version":3,"file":"write-handlers.js","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":"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"}
@@ -2,7 +2,6 @@ import { errors } from '@strapi/utils';
2
2
  import { getService } from '../../utils/index.mjs';
3
3
  import { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants.mjs';
4
4
  import { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions.mjs';
5
- import { assertAmbientInstance } from '../ambient-instance.mjs';
6
5
  import { assertMediaPermission } from '../permissions.mjs';
7
6
  import { sanitizeMediaAsset } from '../sanitizers/sanitize-media.mjs';
8
7
  import { MCP_UPDATE_ASSET_NO_FIELDS, MCP_NOT_FOUND_ASSET, MCP_MOVE_ASSETS_ID_FAILED, MCP_DELETE_ASSETS_ID_FAILED, MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND, MCP_MOVE_ASSETS_ID_NOT_FOUND, MCP_MOVE_ASSETS_ID_FORBIDDEN, MCP_DELETE_ASSETS_ID_NOT_FOUND, MCP_DELETE_ASSETS_ID_FORBIDDEN } from './constants.mjs';
@@ -40,9 +39,7 @@ import { ok } from '../utils.mjs';
40
39
  * The response carries the updated asset through the read sanitizer, so a client can confirm
41
40
  * the write without a second `media_get_asset` round-trip — and so provider fields stay
42
41
  * invisible on the write path too.
43
- */ const createMediaUpdateAssetHandler = (strapi, context)=>{
44
- assertAmbientInstance(strapi);
45
- return async ({ args })=>{
42
+ */ const createMediaUpdateAssetHandler = (strapi, context)=>async ({ args })=>{
46
43
  const { id, ...metadata } = args;
47
44
  const fileInfo = buildFileInfo(metadata);
48
45
  // A patch with no writable field is a caller error, not a no-op success: the schema cannot
@@ -77,7 +74,6 @@ import { ok } from '../utils.mjs';
77
74
  data: sanitizeMediaAsset(updated ?? written)
78
75
  });
79
76
  };
80
- };
81
77
  /**
82
78
  * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.
83
79
  *
@@ -127,9 +123,7 @@ import { ok } from '../utils.mjs';
127
123
  * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its
128
124
  * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the
129
125
  * connection-pool contention of firing them in parallel.
130
- */ const createMediaMoveAssetsHandler = (strapi, context)=>{
131
- assertAmbientInstance(strapi);
132
- return async ({ args })=>{
126
+ */ const createMediaMoveAssetsHandler = (strapi, context)=>async ({ args })=>{
133
127
  const { ids, folder } = args;
134
128
  // Model-level gate first, so a token without the action is refused before any DB read.
135
129
  assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);
@@ -206,7 +200,6 @@ import { ok } from '../utils.mjs';
206
200
  failed
207
201
  });
208
202
  };
209
- };
210
203
  /**
211
204
  * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.
212
205
  *
@@ -231,9 +224,7 @@ import { ok } from '../utils.mjs';
231
224
  *
232
225
  * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch
233
226
  * (100 ids max, per the input schema) is not worth firing at a provider in parallel.
234
- */ const createMediaDeleteAssetsHandler = (strapi, context)=>{
235
- assertAmbientInstance(strapi);
236
- return async ({ args })=>{
227
+ */ const createMediaDeleteAssetsHandler = (strapi, context)=>async ({ args })=>{
237
228
  const { ids, dryRun = true } = args;
238
229
  // Model-level gate first, so a token without the action is refused before any DB read.
239
230
  // The preview takes the same gate: it reveals which assets exist and what they are.
@@ -305,7 +296,6 @@ import { ok } from '../utils.mjs';
305
296
  totalFileNumber: deleted.length
306
297
  });
307
298
  };
308
- };
309
299
 
310
300
  export { createMediaDeleteAssetsHandler, createMediaMoveAssetsHandler, createMediaUpdateAssetHandler };
311
301
  //# sourceMappingURL=write-handlers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"write-handlers.mjs","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertAmbientInstance } from '../ambient-instance';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n};\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n};\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n};\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","assertAmbientInstance","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":"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"}
1
+ {"version":3,"file":"write-handlers.mjs","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":"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"}