@strapi/upload 5.57.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.
- package/dist/admin/components/MediaLibraryInput/AddAssetMenu.js +141 -0
- package/dist/admin/components/MediaLibraryInput/AddAssetMenu.js.map +1 -0
- package/dist/admin/components/MediaLibraryInput/AddAssetMenu.mjs +139 -0
- package/dist/admin/components/MediaLibraryInput/AddAssetMenu.mjs.map +1 -0
- package/dist/admin/components/MediaLibraryInput/AssetDropZone.js +129 -0
- package/dist/admin/components/MediaLibraryInput/AssetDropZone.js.map +1 -0
- package/dist/admin/components/MediaLibraryInput/AssetDropZone.mjs +127 -0
- package/dist/admin/components/MediaLibraryInput/AssetDropZone.mjs.map +1 -0
- package/dist/admin/components/MediaLibraryInput/AssetRow.js +144 -0
- package/dist/admin/components/MediaLibraryInput/AssetRow.js.map +1 -0
- package/dist/admin/components/MediaLibraryInput/AssetRow.mjs +142 -0
- package/dist/admin/components/MediaLibraryInput/AssetRow.mjs.map +1 -0
- package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js +397 -0
- package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.js.map +1 -0
- package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs +395 -0
- package/dist/admin/components/MediaLibraryInput/MediaLibraryInput.mjs.map +1 -0
- package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.js +45 -0
- package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.js.map +1 -0
- package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.mjs +43 -0
- package/dist/admin/components/MediaLibraryInput/NoLibraryAccessDialog.mjs.map +1 -0
- package/dist/admin/hooks/useAIMetadataAvailability.js +5 -1
- package/dist/admin/hooks/useAIMetadataAvailability.js.map +1 -1
- package/dist/admin/hooks/useAIMetadataAvailability.mjs +5 -1
- package/dist/admin/hooks/useAIMetadataAvailability.mjs.map +1 -1
- package/dist/admin/hooks/useAIMetadataEnabled.js +7 -1
- package/dist/admin/hooks/useAIMetadataEnabled.js.map +1 -1
- package/dist/admin/hooks/useAIMetadataEnabled.mjs +7 -1
- package/dist/admin/hooks/useAIMetadataEnabled.mjs.map +1 -1
- package/dist/admin/hooks/useMediaLibraryPermissions.js +4 -0
- package/dist/admin/hooks/useMediaLibraryPermissions.js.map +1 -1
- package/dist/admin/hooks/useMediaLibraryPermissions.mjs +4 -0
- package/dist/admin/hooks/useMediaLibraryPermissions.mjs.map +1 -1
- package/dist/admin/hooks/useTracking.js +5 -1
- package/dist/admin/hooks/useTracking.js.map +1 -1
- package/dist/admin/hooks/useTracking.mjs +5 -1
- package/dist/admin/hooks/useTracking.mjs.map +1 -1
- package/dist/admin/index.js +7 -2
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/index.mjs +7 -2
- package/dist/admin/index.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetCard/AssetCardBase.js.map +1 -1
- package/dist/admin/legacy/components/AssetCard/AssetCardBase.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetCard/UploadingAssetCard.js +9 -1
- package/dist/admin/legacy/components/AssetCard/UploadingAssetCard.js.map +1 -1
- package/dist/admin/legacy/components/AssetCard/UploadingAssetCard.mjs +9 -1
- package/dist/admin/legacy/components/AssetCard/UploadingAssetCard.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetCard/VideoPreview.js +2 -1
- package/dist/admin/legacy/components/AssetCard/VideoPreview.js.map +1 -1
- package/dist/admin/legacy/components/AssetCard/VideoPreview.mjs +2 -1
- package/dist/admin/legacy/components/AssetCard/VideoPreview.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/AssetDialog.js +3 -1
- package/dist/admin/legacy/components/AssetDialog/AssetDialog.js.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/AssetDialog.mjs +3 -1
- package/dist/admin/legacy/components/AssetDialog/AssetDialog.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.js +3 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.js.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.mjs +3 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/BrowseStep.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/Filters.js.map +1 -1
- package/dist/admin/legacy/components/AssetDialog/BrowseStep/Filters.mjs.map +1 -1
- package/dist/admin/legacy/components/AssetGridList/AssetGridList.js +7 -6
- package/dist/admin/legacy/components/AssetGridList/AssetGridList.js.map +1 -1
- package/dist/admin/legacy/components/AssetGridList/AssetGridList.mjs +7 -6
- package/dist/admin/legacy/components/AssetGridList/AssetGridList.mjs.map +1 -1
- package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.js +2 -1
- package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.js.map +1 -1
- package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.mjs +2 -1
- package/dist/admin/legacy/components/Breadcrumbs/CrumbSimpleMenuAsync.mjs.map +1 -1
- package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.js +2 -1
- package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.js.map +1 -1
- package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.mjs +2 -1
- package/dist/admin/legacy/components/BulkMoveDialog/BulkMoveDialog.mjs.map +1 -1
- package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.js +3 -1
- package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.js.map +1 -1
- package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.mjs +3 -1
- package/dist/admin/legacy/components/EditAssetDialog/EditAssetContent.mjs.map +1 -1
- package/dist/admin/legacy/components/EditAssetDialog/PreviewBox/PreviewBox.js +20 -6
- package/dist/admin/legacy/components/EditAssetDialog/PreviewBox/PreviewBox.js.map +1 -1
- package/dist/admin/legacy/components/EditAssetDialog/PreviewBox/PreviewBox.mjs +20 -6
- package/dist/admin/legacy/components/EditAssetDialog/PreviewBox/PreviewBox.mjs.map +1 -1
- package/dist/admin/legacy/components/FilterList/FilterList.js.map +1 -1
- package/dist/admin/legacy/components/FilterList/FilterList.mjs.map +1 -1
- package/dist/admin/legacy/components/FilterPopover/FilterPopover.js.map +1 -1
- package/dist/admin/legacy/components/FilterPopover/FilterPopover.mjs.map +1 -1
- package/dist/admin/legacy/components/FilterPopover/FilterValueInput.js +2 -1
- package/dist/admin/legacy/components/FilterPopover/FilterValueInput.js.map +1 -1
- package/dist/admin/legacy/components/FilterPopover/FilterValueInput.mjs +2 -1
- package/dist/admin/legacy/components/FilterPopover/FilterValueInput.mjs.map +1 -1
- package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.js +4 -2
- package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.js.map +1 -1
- package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.mjs +4 -2
- package/dist/admin/legacy/components/MediaLibraryDialog/MediaLibraryDialog.mjs.map +1 -1
- package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.js +2 -1
- package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.js.map +1 -1
- package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.mjs +2 -1
- package/dist/admin/legacy/components/MediaLibraryInput/MediaLibraryInput.mjs.map +1 -1
- package/dist/admin/legacy/components/SelectTree/SelectTree.js +3 -1
- package/dist/admin/legacy/components/SelectTree/SelectTree.js.map +1 -1
- package/dist/admin/legacy/components/SelectTree/SelectTree.mjs +3 -1
- package/dist/admin/legacy/components/SelectTree/SelectTree.mjs.map +1 -1
- package/dist/admin/legacy/components/TableList/TableList.js +3 -1
- package/dist/admin/legacy/components/TableList/TableList.js.map +1 -1
- package/dist/admin/legacy/components/TableList/TableList.mjs +3 -1
- package/dist/admin/legacy/components/TableList/TableList.mjs.map +1 -1
- package/dist/admin/legacy/components/TableList/TableRows.js +3 -1
- package/dist/admin/legacy/components/TableList/TableRows.js.map +1 -1
- package/dist/admin/legacy/components/TableList/TableRows.mjs +3 -1
- package/dist/admin/legacy/components/TableList/TableRows.mjs.map +1 -1
- package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.js +3 -1
- package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.js.map +1 -1
- package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.mjs +3 -1
- package/dist/admin/legacy/components/UploadAssetDialog/UploadAssetDialog.mjs.map +1 -1
- package/dist/admin/legacy/hooks/useEditAsset.js +11 -6
- package/dist/admin/legacy/hooks/useEditAsset.js.map +1 -1
- package/dist/admin/legacy/hooks/useEditAsset.mjs +12 -7
- package/dist/admin/legacy/hooks/useEditAsset.mjs.map +1 -1
- package/dist/admin/legacy/hooks/useUpload.js +6 -4
- package/dist/admin/legacy/hooks/useUpload.js.map +1 -1
- package/dist/admin/legacy/hooks/useUpload.mjs +6 -4
- package/dist/admin/legacy/hooks/useUpload.mjs.map +1 -1
- package/dist/admin/legacy/pages/App/ConfigureTheView/ConfigureTheView.js.map +1 -1
- package/dist/admin/legacy/pages/App/ConfigureTheView/ConfigureTheView.mjs.map +1 -1
- package/dist/admin/legacy/pages/App/components/BulkActions.js +2 -1
- package/dist/admin/legacy/pages/App/components/BulkActions.js.map +1 -1
- package/dist/admin/legacy/pages/App/components/BulkActions.mjs +2 -1
- package/dist/admin/legacy/pages/App/components/BulkActions.mjs.map +1 -1
- package/dist/admin/legacy/pages/App/components/BulkMoveButton.js +2 -1
- package/dist/admin/legacy/pages/App/components/BulkMoveButton.js.map +1 -1
- package/dist/admin/legacy/pages/App/components/BulkMoveButton.mjs +2 -1
- package/dist/admin/legacy/pages/App/components/BulkMoveButton.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/AssetsGrid.js +2 -1
- package/dist/admin/pages/Assets/components/AssetsGrid.js.map +1 -1
- package/dist/admin/pages/Assets/components/AssetsGrid.mjs +2 -1
- package/dist/admin/pages/Assets/components/AssetsGrid.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/AssetsTable.js +2 -1
- package/dist/admin/pages/Assets/components/AssetsTable.js.map +1 -1
- package/dist/admin/pages/Assets/components/AssetsTable.mjs +2 -1
- package/dist/admin/pages/Assets/components/AssetsTable.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/BulkActionsBar.js +3 -1
- package/dist/admin/pages/Assets/components/BulkActionsBar.js.map +1 -1
- package/dist/admin/pages/Assets/components/BulkActionsBar.mjs +3 -1
- package/dist/admin/pages/Assets/components/BulkActionsBar.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/DateRangeCalendar.js +10 -7
- package/dist/admin/pages/Assets/components/DateRangeCalendar.js.map +1 -1
- package/dist/admin/pages/Assets/components/DateRangeCalendar.mjs +10 -7
- package/dist/admin/pages/Assets/components/DateRangeCalendar.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.js +4 -2
- package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.js.map +1 -1
- package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.mjs +5 -3
- package/dist/admin/pages/Assets/components/DropZone/UploadDropZoneContext.mjs.map +1 -1
- package/dist/admin/pages/Assets/components/FilterBadges.js +32 -3
- package/dist/admin/pages/Assets/components/FilterBadges.js.map +1 -1
- package/dist/admin/pages/Assets/components/FilterBadges.mjs +33 -4
- package/dist/admin/pages/Assets/components/FilterBadges.mjs.map +1 -1
- package/dist/admin/services/api.js +15 -5
- package/dist/admin/services/api.js.map +1 -1
- package/dist/admin/services/api.mjs +15 -5
- package/dist/admin/services/api.mjs.map +1 -1
- package/dist/admin/src/components/MediaLibraryInput/AddAssetMenu.d.ts +19 -0
- package/dist/admin/src/components/MediaLibraryInput/AssetDropZone.d.ts +13 -0
- package/dist/admin/src/components/MediaLibraryInput/AssetRow.d.ts +15 -0
- package/dist/admin/src/components/MediaLibraryInput/MediaLibraryInput.d.ts +26 -0
- package/dist/admin/src/components/MediaLibraryInput/NoLibraryAccessDialog.d.ts +10 -0
- package/dist/admin/src/hooks/useMediaLibraryPermissions.d.ts +1 -0
- package/dist/admin/src/legacy/components/BulkMoveDialog/BulkMoveDialog.d.ts +1 -1
- package/dist/admin/src/legacy/hooks/useEditAsset.d.ts +13 -22
- package/dist/admin/src/legacy/hooks/useUpload.d.ts +1 -1
- package/dist/admin/src/legacy/pages/App/components/BulkActions.d.ts +1 -1
- package/dist/admin/src/services/api.d.ts +6 -0
- package/dist/admin/src/utils/allowedMediaTypes.d.ts +16 -0
- package/dist/admin/translations/ca.json.js +3 -3
- package/dist/admin/translations/ca.json.mjs +3 -3
- package/dist/admin/translations/da.json.js +1 -1
- package/dist/admin/translations/da.json.mjs +1 -1
- package/dist/admin/translations/de.json.js +1 -1
- package/dist/admin/translations/de.json.mjs +1 -1
- package/dist/admin/translations/en.json.js +9 -0
- package/dist/admin/translations/en.json.js.map +1 -1
- package/dist/admin/translations/en.json.mjs +9 -0
- package/dist/admin/translations/en.json.mjs.map +1 -1
- package/dist/admin/translations/es.json.js +1 -1
- package/dist/admin/translations/es.json.mjs +1 -1
- package/dist/admin/translations/sk.json.js +2 -2
- package/dist/admin/translations/sk.json.mjs +2 -2
- package/dist/admin/translations/tr.json.js +2 -2
- package/dist/admin/translations/tr.json.mjs +2 -2
- package/dist/admin/translations/zh-Hans.json.js +1 -1
- package/dist/admin/translations/zh-Hans.json.mjs +1 -1
- package/dist/admin/translations/zh.json.js +2 -2
- package/dist/admin/translations/zh.json.mjs +2 -2
- package/dist/admin/utils/allowedMediaTypes.js +42 -0
- package/dist/admin/utils/allowedMediaTypes.js.map +1 -0
- package/dist/admin/utils/allowedMediaTypes.mjs +39 -0
- package/dist/admin/utils/allowedMediaTypes.mjs.map +1 -0
- package/dist/server/config.js +6 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/config.mjs +6 -0
- package/dist/server/config.mjs.map +1 -1
- package/dist/server/controllers/validation/admin/upload.js.map +1 -1
- package/dist/server/controllers/validation/admin/upload.mjs.map +1 -1
- package/dist/server/controllers/validation/content-api/upload.js.map +1 -1
- package/dist/server/controllers/validation/content-api/upload.mjs.map +1 -1
- package/dist/server/mcp/ambient-instance.js +7 -5
- package/dist/server/mcp/ambient-instance.js.map +1 -1
- package/dist/server/mcp/ambient-instance.mjs +7 -5
- package/dist/server/mcp/ambient-instance.mjs.map +1 -1
- package/dist/server/mcp/handlers/folder-handlers.js +4 -16
- package/dist/server/mcp/handlers/folder-handlers.js.map +1 -1
- package/dist/server/mcp/handlers/folder-handlers.mjs +5 -17
- package/dist/server/mcp/handlers/folder-handlers.mjs.map +1 -1
- package/dist/server/mcp/handlers/read-handlers.js +6 -14
- package/dist/server/mcp/handlers/read-handlers.js.map +1 -1
- package/dist/server/mcp/handlers/read-handlers.mjs +7 -15
- package/dist/server/mcp/handlers/read-handlers.mjs.map +1 -1
- package/dist/server/mcp/handlers/write-handlers.js +3 -13
- package/dist/server/mcp/handlers/write-handlers.js.map +1 -1
- package/dist/server/mcp/handlers/write-handlers.mjs +3 -13
- package/dist/server/mcp/handlers/write-handlers.mjs.map +1 -1
- package/dist/server/mcp/register-upload-mcp-tools.js +215 -195
- package/dist/server/mcp/register-upload-mcp-tools.js.map +1 -1
- package/dist/server/mcp/register-upload-mcp-tools.mjs +215 -195
- package/dist/server/mcp/register-upload-mcp-tools.mjs.map +1 -1
- package/dist/server/services/extensions/utils.js +1 -1
- package/dist/server/services/extensions/utils.js.map +1 -1
- package/dist/server/services/extensions/utils.mjs +1 -1
- package/dist/server/services/extensions/utils.mjs.map +1 -1
- package/dist/server/services/image-manipulation.js +33 -14
- package/dist/server/services/image-manipulation.js.map +1 -1
- package/dist/server/services/image-manipulation.mjs +33 -14
- package/dist/server/services/image-manipulation.mjs.map +1 -1
- package/dist/server/src/config.d.ts +3 -0
- package/dist/server/src/config.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/admin/upload.d.ts.map +1 -1
- package/dist/server/src/controllers/validation/content-api/upload.d.ts.map +1 -1
- package/dist/server/src/index.d.ts +3 -0
- package/dist/server/src/index.d.ts.map +1 -1
- package/dist/server/src/mcp/ambient-instance.d.ts +7 -5
- package/dist/server/src/mcp/ambient-instance.d.ts.map +1 -1
- package/dist/server/src/mcp/handlers/folder-handlers.d.ts.map +1 -1
- package/dist/server/src/mcp/handlers/read-handlers.d.ts +3 -2
- package/dist/server/src/mcp/handlers/read-handlers.d.ts.map +1 -1
- package/dist/server/src/mcp/handlers/write-handlers.d.ts.map +1 -1
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts +9 -14
- package/dist/server/src/mcp/register-upload-mcp-tools.d.ts.map +1 -1
- package/dist/server/src/services/extensions/utils.d.ts +1 -1
- package/dist/server/src/services/image-manipulation.d.ts.map +1 -1
- package/dist/server/src/types.d.ts +1 -0
- package/dist/server/src/types.d.ts.map +1 -1
- package/dist/server/utils/mime-validation.js +1 -1
- package/dist/server/utils/mime-validation.js.map +1 -1
- package/dist/server/utils/mime-validation.mjs +1 -1
- package/dist/server/utils/mime-validation.mjs.map +1 -1
- package/package.json +7 -7
|
@@ -152,9 +152,7 @@ var utils = require('../utils.js');
|
|
|
152
152
|
*
|
|
153
153
|
* `create` rather than the `update` the other folder tools use: creating a folder is the one
|
|
154
154
|
* folder operation whose admin route gates on `assets.create`.
|
|
155
|
-
*/ const createMediaCreateFolderHandler = (strapi, context)=>{
|
|
156
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
157
|
-
return async ({ args })=>{
|
|
155
|
+
*/ const createMediaCreateFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
158
156
|
const { name, parent = null } = args;
|
|
159
157
|
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.create, constants$1.FOLDER_MODEL_UID);
|
|
160
158
|
await assertParentExists(strapi, parent);
|
|
@@ -169,7 +167,6 @@ var utils = require('../utils.js');
|
|
|
169
167
|
data: await readFolderForOutput(strapi, created.id, created)
|
|
170
168
|
});
|
|
171
169
|
};
|
|
172
|
-
};
|
|
173
170
|
/**
|
|
174
171
|
* `media_rename_folder` — changes a folder's name and nothing else.
|
|
175
172
|
*
|
|
@@ -177,9 +174,7 @@ var utils = require('../utils.js');
|
|
|
177
174
|
* `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant
|
|
178
175
|
* paths. Passing the folder's existing parent would take the move branch and rewrite the whole
|
|
179
176
|
* subtree to compute the identical paths.
|
|
180
|
-
*/ const createMediaRenameFolderHandler = (strapi, context)=>{
|
|
181
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
182
|
-
return async ({ args })=>{
|
|
177
|
+
*/ const createMediaRenameFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
183
178
|
const { id, name } = args;
|
|
184
179
|
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FOLDER_MODEL_UID);
|
|
185
180
|
const folder = await findFolderById(strapi, id, {
|
|
@@ -205,16 +200,13 @@ var utils = require('../utils.js');
|
|
|
205
200
|
})
|
|
206
201
|
});
|
|
207
202
|
};
|
|
208
|
-
};
|
|
209
203
|
/**
|
|
210
204
|
* `media_move_folder` — re-parents a folder, carrying its whole subtree with it.
|
|
211
205
|
*
|
|
212
206
|
* `folder.update()` recalculates the materialized `path` of every descendant folder and the
|
|
213
207
|
* `folderPath` of every contained file inside a transaction, so the move is atomic and no
|
|
214
208
|
* separate bookkeeping is needed here.
|
|
215
|
-
*/ const createMediaMoveFolderHandler = (strapi, context)=>{
|
|
216
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
217
|
-
return async ({ args })=>{
|
|
209
|
+
*/ const createMediaMoveFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
218
210
|
const { id, parent } = args;
|
|
219
211
|
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FOLDER_MODEL_UID);
|
|
220
212
|
const folder = await findFolderById(strapi, id);
|
|
@@ -235,7 +227,6 @@ var utils = require('../utils.js');
|
|
|
235
227
|
data: await readFolderForOutput(strapi, id, moved ?? folder)
|
|
236
228
|
});
|
|
237
229
|
};
|
|
238
|
-
};
|
|
239
230
|
/**
|
|
240
231
|
* `media_delete_folder` — previews or performs a cascading folder deletion.
|
|
241
232
|
*
|
|
@@ -245,9 +236,7 @@ var utils = require('../utils.js');
|
|
|
245
236
|
*
|
|
246
237
|
* `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting
|
|
247
238
|
* takes an explicit `dryRun: false`.
|
|
248
|
-
*/ const createMediaDeleteFolderHandler = (strapi, context)=>{
|
|
249
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
250
|
-
return async ({ args })=>{
|
|
239
|
+
*/ const createMediaDeleteFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
251
240
|
const { ids, dryRun = true } = args;
|
|
252
241
|
permissions.assertMediaPermission(strapi, context, constants$1.ACTIONS.update, constants$1.FOLDER_MODEL_UID);
|
|
253
242
|
const matched = await strapi.db.query(constants$1.FOLDER_MODEL_UID).findMany({
|
|
@@ -299,7 +288,6 @@ var utils = require('../utils.js');
|
|
|
299
288
|
totalFileNumber
|
|
300
289
|
});
|
|
301
290
|
};
|
|
302
|
-
};
|
|
303
291
|
|
|
304
292
|
exports.createMediaCreateFolderHandler = createMediaCreateFolderHandler;
|
|
305
293
|
exports.createMediaDeleteFolderHandler = createMediaDeleteFolderHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder-handlers.js","sources":["../../../../server/src/mcp/handlers/folder-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { assertAmbientInstance, getFolderService } from '../ambient-instance';\nimport { ACTIONS, FOLDER_MODEL_UID, FILE_MODEL_UID } from '../../constants';\nimport { isFolderOrChild } from '../../controllers/utils/folders';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaFolder } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_FOLDER,\n MCP_FOLDER_NAME_TAKEN,\n MCP_PARENT_FOLDER_NOT_FOUND,\n MCP_FOLDER_MOVE_INTO_SELF,\n MCP_DELETE_FOLDER_UNRESOLVED_IDS,\n} from './constants';\nimport { ok } from '../utils';\n\nimport type { Folder } from '../../types';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's strict Zod\n// input schema before the handler runs, so unknown keys never reach here.\ntype CreateFolderArgs = {\n name: string;\n parent?: number | null;\n};\n\ntype RenameFolderArgs = {\n id: number;\n name: string;\n};\n\ntype MoveFolderArgs = {\n id: number;\n parent: number | null;\n};\n\ntype DeleteFolderArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The folder fields the handlers need; `path` drives the cascade and subtree checks. */\ntype FolderRow = Pick<Folder, 'id' | 'name' | 'path'>;\n\n/**\n * `withParent` populates the parent relation in the same round trip. Rename needs the parent id\n * to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the\n * subtree and cascade callers do not, and skip the join.\n */\nconst findFolderById = async (\n strapi: Core.Strapi,\n id: number,\n { withParent = false }: { withParent?: boolean } = {}\n): Promise<(FolderRow & { parent?: { id: number } | null }) | null> =>\n strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name', 'path'],\n where: { id },\n ...(withParent ? { populate: { parent: { select: ['id'] } } } : {}),\n });\n\n/**\n * Re-reads a folder with its parent populated, so the write tools can echo back the same shape\n * regardless of what the service returned.\n *\n * `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename\n * it never touches the join table at all — so the parent has to be read back to be reported.\n */\nconst readFolderForOutput = async (\n strapi: Core.Strapi,\n id: number,\n fallback: Record<string, unknown>\n) => {\n const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n where: { id },\n populate: { parent: { select: ['id', 'name'] } },\n });\n\n // The write succeeded, so report it even if the read-back comes back empty rather than\n // failing a completed operation on a follow-up query.\n return sanitizeMediaFolder(row ?? fallback);\n};\n\n/**\n * Replicates the admin folder controller's `parent` check: a named parent must exist.\n *\n * null is the media library root and always valid, so only a numeric id is looked up.\n */\nconst assertParentExists = async (strapi: Core.Strapi, parent: number | null | undefined) => {\n if (parent === null || parent === undefined) {\n return;\n }\n\n const exists = await getFolderService(strapi).exists({ id: parent });\n\n if (!exists) {\n throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);\n }\n};\n\n/**\n * Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique\n * among its siblings.\n *\n * `excludeId` is the folder being renamed — without it a rename to the folder's current name\n * would collide with itself.\n */\nconst assertNameAvailable = async (\n strapi: Core.Strapi,\n name: string,\n parent: number | null,\n excludeId?: number\n) => {\n const filters: Record<string, unknown> = { name, parent: parent ?? null };\n\n if (excludeId !== undefined) {\n filters.id = { $ne: excludeId };\n }\n\n if (await getFolderService(strapi).exists(filters)) {\n throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);\n }\n};\n\n/**\n * Replicates `dont-move-inside-self`: a folder cannot become its own descendant.\n *\n * Without this the folder service would rewrite the subtree's materialized paths into a cycle,\n * orphaning every descendant — the tree has no root to walk back to afterwards.\n */\nconst assertNotMovedIntoOwnSubtree = async (\n strapi: Core.Strapi,\n folder: FolderRow,\n parent: number | null\n) => {\n if (parent === null) {\n return;\n }\n\n const destination = await findFolderById(strapi, parent);\n\n // A missing destination is already reported by `assertParentExists`.\n if (destination === null) {\n return;\n }\n\n if (isFolderOrChild(destination, folder)) {\n throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);\n }\n};\n\n/**\n * Counts the folders and files a `deleteByIds` of these paths would remove.\n *\n * Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the\n * `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on\n * rather than an independent estimate that could drift from it.\n */\nconst countCascade = async (strapi: Core.Strapi, paths: string[]) => {\n const pathPredicate = (field: string) => ({\n $or: paths.flatMap((path) => [\n { [field]: { $eq: path } },\n { [field]: { $startsWith: `${path}/` } },\n ]),\n });\n\n const [totalFolderNumber, totalFileNumber] = await Promise.all([\n strapi.db.query(FOLDER_MODEL_UID).count({ where: pathPredicate('path') }),\n strapi.db.query(FILE_MODEL_UID).count({ where: pathPredicate('folderPath') }),\n ]);\n\n return { totalFolderNumber, totalFileNumber };\n};\n\n/**\n * `media_create_folder` — a new folder, optionally nested.\n *\n * Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same\n * permission, the same uniqueness and parent-existence validation, the same `folder.create()`\n * call, and the same `createdBy` attribution from the session user.\n *\n * `create` rather than the `update` the other folder tools use: creating a folder is the one\n * folder operation whose admin route gates on `assets.create`.\n */\nexport const createMediaCreateFolderHandler = (\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 { name, parent = null } = args as CreateFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);\n\n await assertParentExists(strapi, parent);\n await assertNameAvailable(strapi, name, parent);\n\n const created = await getFolderService(strapi).create({ name, parent }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, created.id, created) });\n };\n};\n\n/**\n * `media_rename_folder` — changes a folder's name and nothing else.\n *\n * Deliberately does not forward `parent` to `folder.update()`: the service branches on\n * `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant\n * paths. Passing the folder's existing parent would take the move branch and rewrite the whole\n * subtree to compute the identical paths.\n */\nexport const createMediaRenameFolderHandler = (\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, name } = args as RenameFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id, { withParent: true });\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n // Siblings are the folders sharing this folder's parent, so uniqueness is checked against\n // the current location — a rename never changes it.\n const parentId = folder.parent?.id ?? null;\n\n await assertNameAvailable(strapi, name, parentId, id);\n\n // `parent` is omitted on purpose — see the note above.\n const renamed = await getFolderService(strapi).update(id, { name }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, id, renamed ?? { ...folder, name }) });\n };\n};\n\n/**\n * `media_move_folder` — re-parents a folder, carrying its whole subtree with it.\n *\n * `folder.update()` recalculates the materialized `path` of every descendant folder and the\n * `folderPath` of every contained file inside a transaction, so the move is atomic and no\n * separate bookkeeping is needed here.\n */\nexport const createMediaMoveFolderHandler = (\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, parent } = args as MoveFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id);\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n await assertParentExists(strapi, parent);\n await assertNotMovedIntoOwnSubtree(strapi, folder, parent);\n\n // A folder keeps its name across a move, so uniqueness must hold in the destination.\n await assertNameAvailable(strapi, folder.name, parent, id);\n\n const moved = await getFolderService(strapi).update(\n id,\n { name: folder.name, parent },\n { user: context.user }\n );\n\n return ok({ data: await readFolderForOutput(strapi, id, moved ?? folder) });\n };\n};\n\n/**\n * `media_delete_folder` — previews or performs a cascading folder deletion.\n *\n * Two branches behind one tool, because the preview and the deletion must agree on what the\n * cascade covers; splitting them across tools would let the two drift apart, and an agent could\n * reach the destructive one without ever seeing a count.\n *\n * `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting\n * takes an explicit `dryRun: false`.\n */\nexport const createMediaDeleteFolderHandler = (\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 DeleteFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const matched: FolderRow[] = await strapi.db.query(FOLDER_MODEL_UID).findMany({\n select: ['id', 'name', 'path'],\n where: { id: { $in: ids } },\n });\n\n /**\n * All-or-nothing: any id that does not resolve to a folder rejects the whole call, before\n * anything is deleted.\n *\n * Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the\n * mistake this tool exists to catch — and deleting the folders that did match while\n * reporting the rest back would be exactly the silent confusion that requirement forbids,\n * with the cascade already gone by the time the agent reads the response.\n *\n * An unresolvable id cannot be diagnosed further from here (an asset id and a deleted\n * folder id look identical), and it does not need to be: either way it is unusable.\n *\n * The dry run rejects on the same rule. A preview that reported a cascade for a request the\n * executing call would refuse is worse than no preview — it is a confirmation an agent\n * cannot act on.\n */\n const matchedIds = new Set(matched.map((folder) => folder.id));\n const unresolvedIds = ids.filter((id) => matchedIds.has(id) === false);\n\n if (unresolvedIds.length > 0) {\n throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));\n }\n\n const folders = matched.map((folder) => sanitizeMediaFolder(folder));\n\n if (dryRun) {\n const counts = await countCascade(\n strapi,\n matched.map((folder) => folder.path)\n );\n\n return ok({ dryRun: true, folders, ...counts });\n }\n\n const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(\n matched.map((folder) => folder.id)\n );\n\n return ok({\n dryRun: false,\n folders,\n totalFolderNumber,\n totalFileNumber,\n });\n };\n};\n"],"names":["findFolderById","strapi","id","withParent","db","query","FOLDER_MODEL_UID","findOne","select","where","populate","parent","readFolderForOutput","fallback","row","sanitizeMediaFolder","assertParentExists","undefined","exists","getFolderService","errors","ValidationError","MCP_PARENT_FOLDER_NOT_FOUND","assertNameAvailable","name","excludeId","filters","$ne","MCP_FOLDER_NAME_TAKEN","assertNotMovedIntoOwnSubtree","folder","destination","isFolderOrChild","MCP_FOLDER_MOVE_INTO_SELF","countCascade","paths","pathPredicate","field","$or","flatMap","path","$eq","$startsWith","totalFolderNumber","totalFileNumber","Promise","all","count","FILE_MODEL_UID","createMediaCreateFolderHandler","context","assertAmbientInstance","args","assertMediaPermission","ACTIONS","create","created","user","ok","data","createMediaRenameFolderHandler","update","NotFoundError","MCP_NOT_FOUND_FOLDER","parentId","renamed","createMediaMoveFolderHandler","moved","createMediaDeleteFolderHandler","ids","dryRun","matched","findMany","$in","matchedIds","Set","map","unresolvedIds","filter","has","length","MCP_DELETE_FOLDER_UNRESOLVED_IDS","folders","counts","deleteByIds"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"folder-handlers.js","sources":["../../../../server/src/mcp/handlers/folder-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getFolderService } from '../ambient-instance';\nimport { ACTIONS, FOLDER_MODEL_UID, FILE_MODEL_UID } from '../../constants';\nimport { isFolderOrChild } from '../../controllers/utils/folders';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaFolder } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_FOLDER,\n MCP_FOLDER_NAME_TAKEN,\n MCP_PARENT_FOLDER_NOT_FOUND,\n MCP_FOLDER_MOVE_INTO_SELF,\n MCP_DELETE_FOLDER_UNRESOLVED_IDS,\n} from './constants';\nimport { ok } from '../utils';\n\nimport type { Folder } from '../../types';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's strict Zod\n// input schema before the handler runs, so unknown keys never reach here.\ntype CreateFolderArgs = {\n name: string;\n parent?: number | null;\n};\n\ntype RenameFolderArgs = {\n id: number;\n name: string;\n};\n\ntype MoveFolderArgs = {\n id: number;\n parent: number | null;\n};\n\ntype DeleteFolderArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The folder fields the handlers need; `path` drives the cascade and subtree checks. */\ntype FolderRow = Pick<Folder, 'id' | 'name' | 'path'>;\n\n/**\n * `withParent` populates the parent relation in the same round trip. Rename needs the parent id\n * to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the\n * subtree and cascade callers do not, and skip the join.\n */\nconst findFolderById = async (\n strapi: Core.Strapi,\n id: number,\n { withParent = false }: { withParent?: boolean } = {}\n): Promise<(FolderRow & { parent?: { id: number } | null }) | null> =>\n strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name', 'path'],\n where: { id },\n ...(withParent ? { populate: { parent: { select: ['id'] } } } : {}),\n });\n\n/**\n * Re-reads a folder with its parent populated, so the write tools can echo back the same shape\n * regardless of what the service returned.\n *\n * `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename\n * it never touches the join table at all — so the parent has to be read back to be reported.\n */\nconst readFolderForOutput = async (\n strapi: Core.Strapi,\n id: number,\n fallback: Record<string, unknown>\n) => {\n const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n where: { id },\n populate: { parent: { select: ['id', 'name'] } },\n });\n\n // The write succeeded, so report it even if the read-back comes back empty rather than\n // failing a completed operation on a follow-up query.\n return sanitizeMediaFolder(row ?? fallback);\n};\n\n/**\n * Replicates the admin folder controller's `parent` check: a named parent must exist.\n *\n * null is the media library root and always valid, so only a numeric id is looked up.\n */\nconst assertParentExists = async (strapi: Core.Strapi, parent: number | null | undefined) => {\n if (parent === null || parent === undefined) {\n return;\n }\n\n const exists = await getFolderService(strapi).exists({ id: parent });\n\n if (!exists) {\n throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);\n }\n};\n\n/**\n * Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique\n * among its siblings.\n *\n * `excludeId` is the folder being renamed — without it a rename to the folder's current name\n * would collide with itself.\n */\nconst assertNameAvailable = async (\n strapi: Core.Strapi,\n name: string,\n parent: number | null,\n excludeId?: number\n) => {\n const filters: Record<string, unknown> = { name, parent: parent ?? null };\n\n if (excludeId !== undefined) {\n filters.id = { $ne: excludeId };\n }\n\n if (await getFolderService(strapi).exists(filters)) {\n throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);\n }\n};\n\n/**\n * Replicates `dont-move-inside-self`: a folder cannot become its own descendant.\n *\n * Without this the folder service would rewrite the subtree's materialized paths into a cycle,\n * orphaning every descendant — the tree has no root to walk back to afterwards.\n */\nconst assertNotMovedIntoOwnSubtree = async (\n strapi: Core.Strapi,\n folder: FolderRow,\n parent: number | null\n) => {\n if (parent === null) {\n return;\n }\n\n const destination = await findFolderById(strapi, parent);\n\n // A missing destination is already reported by `assertParentExists`.\n if (destination === null) {\n return;\n }\n\n if (isFolderOrChild(destination, folder)) {\n throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);\n }\n};\n\n/**\n * Counts the folders and files a `deleteByIds` of these paths would remove.\n *\n * Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the\n * `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on\n * rather than an independent estimate that could drift from it.\n */\nconst countCascade = async (strapi: Core.Strapi, paths: string[]) => {\n const pathPredicate = (field: string) => ({\n $or: paths.flatMap((path) => [\n { [field]: { $eq: path } },\n { [field]: { $startsWith: `${path}/` } },\n ]),\n });\n\n const [totalFolderNumber, totalFileNumber] = await Promise.all([\n strapi.db.query(FOLDER_MODEL_UID).count({ where: pathPredicate('path') }),\n strapi.db.query(FILE_MODEL_UID).count({ where: pathPredicate('folderPath') }),\n ]);\n\n return { totalFolderNumber, totalFileNumber };\n};\n\n/**\n * `media_create_folder` — a new folder, optionally nested.\n *\n * Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same\n * permission, the same uniqueness and parent-existence validation, the same `folder.create()`\n * call, and the same `createdBy` attribution from the session user.\n *\n * `create` rather than the `update` the other folder tools use: creating a folder is the one\n * folder operation whose admin route gates on `assets.create`.\n */\nexport const createMediaCreateFolderHandler =\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 { name, parent = null } = args as CreateFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);\n\n await assertParentExists(strapi, parent);\n await assertNameAvailable(strapi, name, parent);\n\n const created = await getFolderService(strapi).create({ name, parent }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, created.id, created) });\n };\n\n/**\n * `media_rename_folder` — changes a folder's name and nothing else.\n *\n * Deliberately does not forward `parent` to `folder.update()`: the service branches on\n * `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant\n * paths. Passing the folder's existing parent would take the move branch and rewrite the whole\n * subtree to compute the identical paths.\n */\nexport const createMediaRenameFolderHandler =\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, name } = args as RenameFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id, { withParent: true });\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n // Siblings are the folders sharing this folder's parent, so uniqueness is checked against\n // the current location — a rename never changes it.\n const parentId = folder.parent?.id ?? null;\n\n await assertNameAvailable(strapi, name, parentId, id);\n\n // `parent` is omitted on purpose — see the note above.\n const renamed = await getFolderService(strapi).update(id, { name }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, id, renamed ?? { ...folder, name }) });\n };\n\n/**\n * `media_move_folder` — re-parents a folder, carrying its whole subtree with it.\n *\n * `folder.update()` recalculates the materialized `path` of every descendant folder and the\n * `folderPath` of every contained file inside a transaction, so the move is atomic and no\n * separate bookkeeping is needed here.\n */\nexport const createMediaMoveFolderHandler =\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, parent } = args as MoveFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id);\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n await assertParentExists(strapi, parent);\n await assertNotMovedIntoOwnSubtree(strapi, folder, parent);\n\n // A folder keeps its name across a move, so uniqueness must hold in the destination.\n await assertNameAvailable(strapi, folder.name, parent, id);\n\n const moved = await getFolderService(strapi).update(\n id,\n { name: folder.name, parent },\n { user: context.user }\n );\n\n return ok({ data: await readFolderForOutput(strapi, id, moved ?? folder) });\n };\n\n/**\n * `media_delete_folder` — previews or performs a cascading folder deletion.\n *\n * Two branches behind one tool, because the preview and the deletion must agree on what the\n * cascade covers; splitting them across tools would let the two drift apart, and an agent could\n * reach the destructive one without ever seeing a count.\n *\n * `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting\n * takes an explicit `dryRun: false`.\n */\nexport const createMediaDeleteFolderHandler =\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 DeleteFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const matched: FolderRow[] = await strapi.db.query(FOLDER_MODEL_UID).findMany({\n select: ['id', 'name', 'path'],\n where: { id: { $in: ids } },\n });\n\n /**\n * All-or-nothing: any id that does not resolve to a folder rejects the whole call, before\n * anything is deleted.\n *\n * Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the\n * mistake this tool exists to catch — and deleting the folders that did match while\n * reporting the rest back would be exactly the silent confusion that requirement forbids,\n * with the cascade already gone by the time the agent reads the response.\n *\n * An unresolvable id cannot be diagnosed further from here (an asset id and a deleted\n * folder id look identical), and it does not need to be: either way it is unusable.\n *\n * The dry run rejects on the same rule. A preview that reported a cascade for a request the\n * executing call would refuse is worse than no preview — it is a confirmation an agent\n * cannot act on.\n */\n const matchedIds = new Set(matched.map((folder) => folder.id));\n const unresolvedIds = ids.filter((id) => matchedIds.has(id) === false);\n\n if (unresolvedIds.length > 0) {\n throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));\n }\n\n const folders = matched.map((folder) => sanitizeMediaFolder(folder));\n\n if (dryRun) {\n const counts = await countCascade(\n strapi,\n matched.map((folder) => folder.path)\n );\n\n return ok({ dryRun: true, folders, ...counts });\n }\n\n const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(\n matched.map((folder) => folder.id)\n );\n\n return ok({\n dryRun: false,\n folders,\n totalFolderNumber,\n totalFileNumber,\n });\n };\n"],"names":["findFolderById","strapi","id","withParent","db","query","FOLDER_MODEL_UID","findOne","select","where","populate","parent","readFolderForOutput","fallback","row","sanitizeMediaFolder","assertParentExists","undefined","exists","getFolderService","errors","ValidationError","MCP_PARENT_FOLDER_NOT_FOUND","assertNameAvailable","name","excludeId","filters","$ne","MCP_FOLDER_NAME_TAKEN","assertNotMovedIntoOwnSubtree","folder","destination","isFolderOrChild","MCP_FOLDER_MOVE_INTO_SELF","countCascade","paths","pathPredicate","field","$or","flatMap","path","$eq","$startsWith","totalFolderNumber","totalFileNumber","Promise","all","count","FILE_MODEL_UID","createMediaCreateFolderHandler","context","args","assertMediaPermission","ACTIONS","create","created","user","ok","data","createMediaRenameFolderHandler","update","NotFoundError","MCP_NOT_FOUND_FOLDER","parentId","renamed","createMediaMoveFolderHandler","moved","createMediaDeleteFolderHandler","ids","dryRun","matched","findMany","$in","matchedIds","Set","map","unresolvedIds","filter","has","length","MCP_DELETE_FOLDER_UNRESOLVED_IDS","folders","counts","deleteByIds"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { errors } from '@strapi/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { getFolderService } from '../ambient-instance.mjs';
|
|
3
3
|
import { FOLDER_MODEL_UID, FILE_MODEL_UID, ACTIONS } from '../../constants.mjs';
|
|
4
4
|
import { isFolderOrChild } from '../../controllers/utils/folders.mjs';
|
|
5
5
|
import { assertMediaPermission } from '../permissions.mjs';
|
|
@@ -150,9 +150,7 @@ import { ok } from '../utils.mjs';
|
|
|
150
150
|
*
|
|
151
151
|
* `create` rather than the `update` the other folder tools use: creating a folder is the one
|
|
152
152
|
* folder operation whose admin route gates on `assets.create`.
|
|
153
|
-
*/ const createMediaCreateFolderHandler = (strapi, context)=>{
|
|
154
|
-
assertAmbientInstance(strapi);
|
|
155
|
-
return async ({ args })=>{
|
|
153
|
+
*/ const createMediaCreateFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
156
154
|
const { name, parent = null } = args;
|
|
157
155
|
assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);
|
|
158
156
|
await assertParentExists(strapi, parent);
|
|
@@ -167,7 +165,6 @@ import { ok } from '../utils.mjs';
|
|
|
167
165
|
data: await readFolderForOutput(strapi, created.id, created)
|
|
168
166
|
});
|
|
169
167
|
};
|
|
170
|
-
};
|
|
171
168
|
/**
|
|
172
169
|
* `media_rename_folder` — changes a folder's name and nothing else.
|
|
173
170
|
*
|
|
@@ -175,9 +172,7 @@ import { ok } from '../utils.mjs';
|
|
|
175
172
|
* `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant
|
|
176
173
|
* paths. Passing the folder's existing parent would take the move branch and rewrite the whole
|
|
177
174
|
* subtree to compute the identical paths.
|
|
178
|
-
*/ const createMediaRenameFolderHandler = (strapi, context)=>{
|
|
179
|
-
assertAmbientInstance(strapi);
|
|
180
|
-
return async ({ args })=>{
|
|
175
|
+
*/ const createMediaRenameFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
181
176
|
const { id, name } = args;
|
|
182
177
|
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
183
178
|
const folder = await findFolderById(strapi, id, {
|
|
@@ -203,16 +198,13 @@ import { ok } from '../utils.mjs';
|
|
|
203
198
|
})
|
|
204
199
|
});
|
|
205
200
|
};
|
|
206
|
-
};
|
|
207
201
|
/**
|
|
208
202
|
* `media_move_folder` — re-parents a folder, carrying its whole subtree with it.
|
|
209
203
|
*
|
|
210
204
|
* `folder.update()` recalculates the materialized `path` of every descendant folder and the
|
|
211
205
|
* `folderPath` of every contained file inside a transaction, so the move is atomic and no
|
|
212
206
|
* separate bookkeeping is needed here.
|
|
213
|
-
*/ const createMediaMoveFolderHandler = (strapi, context)=>{
|
|
214
|
-
assertAmbientInstance(strapi);
|
|
215
|
-
return async ({ args })=>{
|
|
207
|
+
*/ const createMediaMoveFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
216
208
|
const { id, parent } = args;
|
|
217
209
|
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
218
210
|
const folder = await findFolderById(strapi, id);
|
|
@@ -233,7 +225,6 @@ import { ok } from '../utils.mjs';
|
|
|
233
225
|
data: await readFolderForOutput(strapi, id, moved ?? folder)
|
|
234
226
|
});
|
|
235
227
|
};
|
|
236
|
-
};
|
|
237
228
|
/**
|
|
238
229
|
* `media_delete_folder` — previews or performs a cascading folder deletion.
|
|
239
230
|
*
|
|
@@ -243,9 +234,7 @@ import { ok } from '../utils.mjs';
|
|
|
243
234
|
*
|
|
244
235
|
* `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting
|
|
245
236
|
* takes an explicit `dryRun: false`.
|
|
246
|
-
*/ const createMediaDeleteFolderHandler = (strapi, context)=>{
|
|
247
|
-
assertAmbientInstance(strapi);
|
|
248
|
-
return async ({ args })=>{
|
|
237
|
+
*/ const createMediaDeleteFolderHandler = (strapi, context)=>async ({ args })=>{
|
|
249
238
|
const { ids, dryRun = true } = args;
|
|
250
239
|
assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);
|
|
251
240
|
const matched = await strapi.db.query(FOLDER_MODEL_UID).findMany({
|
|
@@ -297,7 +286,6 @@ import { ok } from '../utils.mjs';
|
|
|
297
286
|
totalFileNumber
|
|
298
287
|
});
|
|
299
288
|
};
|
|
300
|
-
};
|
|
301
289
|
|
|
302
290
|
export { createMediaCreateFolderHandler, createMediaDeleteFolderHandler, createMediaMoveFolderHandler, createMediaRenameFolderHandler };
|
|
303
291
|
//# sourceMappingURL=folder-handlers.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"folder-handlers.mjs","sources":["../../../../server/src/mcp/handlers/folder-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { assertAmbientInstance, getFolderService } from '../ambient-instance';\nimport { ACTIONS, FOLDER_MODEL_UID, FILE_MODEL_UID } from '../../constants';\nimport { isFolderOrChild } from '../../controllers/utils/folders';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaFolder } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_FOLDER,\n MCP_FOLDER_NAME_TAKEN,\n MCP_PARENT_FOLDER_NOT_FOUND,\n MCP_FOLDER_MOVE_INTO_SELF,\n MCP_DELETE_FOLDER_UNRESOLVED_IDS,\n} from './constants';\nimport { ok } from '../utils';\n\nimport type { Folder } from '../../types';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's strict Zod\n// input schema before the handler runs, so unknown keys never reach here.\ntype CreateFolderArgs = {\n name: string;\n parent?: number | null;\n};\n\ntype RenameFolderArgs = {\n id: number;\n name: string;\n};\n\ntype MoveFolderArgs = {\n id: number;\n parent: number | null;\n};\n\ntype DeleteFolderArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The folder fields the handlers need; `path` drives the cascade and subtree checks. */\ntype FolderRow = Pick<Folder, 'id' | 'name' | 'path'>;\n\n/**\n * `withParent` populates the parent relation in the same round trip. Rename needs the parent id\n * to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the\n * subtree and cascade callers do not, and skip the join.\n */\nconst findFolderById = async (\n strapi: Core.Strapi,\n id: number,\n { withParent = false }: { withParent?: boolean } = {}\n): Promise<(FolderRow & { parent?: { id: number } | null }) | null> =>\n strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name', 'path'],\n where: { id },\n ...(withParent ? { populate: { parent: { select: ['id'] } } } : {}),\n });\n\n/**\n * Re-reads a folder with its parent populated, so the write tools can echo back the same shape\n * regardless of what the service returned.\n *\n * `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename\n * it never touches the join table at all — so the parent has to be read back to be reported.\n */\nconst readFolderForOutput = async (\n strapi: Core.Strapi,\n id: number,\n fallback: Record<string, unknown>\n) => {\n const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n where: { id },\n populate: { parent: { select: ['id', 'name'] } },\n });\n\n // The write succeeded, so report it even if the read-back comes back empty rather than\n // failing a completed operation on a follow-up query.\n return sanitizeMediaFolder(row ?? fallback);\n};\n\n/**\n * Replicates the admin folder controller's `parent` check: a named parent must exist.\n *\n * null is the media library root and always valid, so only a numeric id is looked up.\n */\nconst assertParentExists = async (strapi: Core.Strapi, parent: number | null | undefined) => {\n if (parent === null || parent === undefined) {\n return;\n }\n\n const exists = await getFolderService(strapi).exists({ id: parent });\n\n if (!exists) {\n throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);\n }\n};\n\n/**\n * Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique\n * among its siblings.\n *\n * `excludeId` is the folder being renamed — without it a rename to the folder's current name\n * would collide with itself.\n */\nconst assertNameAvailable = async (\n strapi: Core.Strapi,\n name: string,\n parent: number | null,\n excludeId?: number\n) => {\n const filters: Record<string, unknown> = { name, parent: parent ?? null };\n\n if (excludeId !== undefined) {\n filters.id = { $ne: excludeId };\n }\n\n if (await getFolderService(strapi).exists(filters)) {\n throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);\n }\n};\n\n/**\n * Replicates `dont-move-inside-self`: a folder cannot become its own descendant.\n *\n * Without this the folder service would rewrite the subtree's materialized paths into a cycle,\n * orphaning every descendant — the tree has no root to walk back to afterwards.\n */\nconst assertNotMovedIntoOwnSubtree = async (\n strapi: Core.Strapi,\n folder: FolderRow,\n parent: number | null\n) => {\n if (parent === null) {\n return;\n }\n\n const destination = await findFolderById(strapi, parent);\n\n // A missing destination is already reported by `assertParentExists`.\n if (destination === null) {\n return;\n }\n\n if (isFolderOrChild(destination, folder)) {\n throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);\n }\n};\n\n/**\n * Counts the folders and files a `deleteByIds` of these paths would remove.\n *\n * Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the\n * `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on\n * rather than an independent estimate that could drift from it.\n */\nconst countCascade = async (strapi: Core.Strapi, paths: string[]) => {\n const pathPredicate = (field: string) => ({\n $or: paths.flatMap((path) => [\n { [field]: { $eq: path } },\n { [field]: { $startsWith: `${path}/` } },\n ]),\n });\n\n const [totalFolderNumber, totalFileNumber] = await Promise.all([\n strapi.db.query(FOLDER_MODEL_UID).count({ where: pathPredicate('path') }),\n strapi.db.query(FILE_MODEL_UID).count({ where: pathPredicate('folderPath') }),\n ]);\n\n return { totalFolderNumber, totalFileNumber };\n};\n\n/**\n * `media_create_folder` — a new folder, optionally nested.\n *\n * Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same\n * permission, the same uniqueness and parent-existence validation, the same `folder.create()`\n * call, and the same `createdBy` attribution from the session user.\n *\n * `create` rather than the `update` the other folder tools use: creating a folder is the one\n * folder operation whose admin route gates on `assets.create`.\n */\nexport const createMediaCreateFolderHandler = (\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 { name, parent = null } = args as CreateFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);\n\n await assertParentExists(strapi, parent);\n await assertNameAvailable(strapi, name, parent);\n\n const created = await getFolderService(strapi).create({ name, parent }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, created.id, created) });\n };\n};\n\n/**\n * `media_rename_folder` — changes a folder's name and nothing else.\n *\n * Deliberately does not forward `parent` to `folder.update()`: the service branches on\n * `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant\n * paths. Passing the folder's existing parent would take the move branch and rewrite the whole\n * subtree to compute the identical paths.\n */\nexport const createMediaRenameFolderHandler = (\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, name } = args as RenameFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id, { withParent: true });\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n // Siblings are the folders sharing this folder's parent, so uniqueness is checked against\n // the current location — a rename never changes it.\n const parentId = folder.parent?.id ?? null;\n\n await assertNameAvailable(strapi, name, parentId, id);\n\n // `parent` is omitted on purpose — see the note above.\n const renamed = await getFolderService(strapi).update(id, { name }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, id, renamed ?? { ...folder, name }) });\n };\n};\n\n/**\n * `media_move_folder` — re-parents a folder, carrying its whole subtree with it.\n *\n * `folder.update()` recalculates the materialized `path` of every descendant folder and the\n * `folderPath` of every contained file inside a transaction, so the move is atomic and no\n * separate bookkeeping is needed here.\n */\nexport const createMediaMoveFolderHandler = (\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, parent } = args as MoveFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id);\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n await assertParentExists(strapi, parent);\n await assertNotMovedIntoOwnSubtree(strapi, folder, parent);\n\n // A folder keeps its name across a move, so uniqueness must hold in the destination.\n await assertNameAvailable(strapi, folder.name, parent, id);\n\n const moved = await getFolderService(strapi).update(\n id,\n { name: folder.name, parent },\n { user: context.user }\n );\n\n return ok({ data: await readFolderForOutput(strapi, id, moved ?? folder) });\n };\n};\n\n/**\n * `media_delete_folder` — previews or performs a cascading folder deletion.\n *\n * Two branches behind one tool, because the preview and the deletion must agree on what the\n * cascade covers; splitting them across tools would let the two drift apart, and an agent could\n * reach the destructive one without ever seeing a count.\n *\n * `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting\n * takes an explicit `dryRun: false`.\n */\nexport const createMediaDeleteFolderHandler = (\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 DeleteFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const matched: FolderRow[] = await strapi.db.query(FOLDER_MODEL_UID).findMany({\n select: ['id', 'name', 'path'],\n where: { id: { $in: ids } },\n });\n\n /**\n * All-or-nothing: any id that does not resolve to a folder rejects the whole call, before\n * anything is deleted.\n *\n * Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the\n * mistake this tool exists to catch — and deleting the folders that did match while\n * reporting the rest back would be exactly the silent confusion that requirement forbids,\n * with the cascade already gone by the time the agent reads the response.\n *\n * An unresolvable id cannot be diagnosed further from here (an asset id and a deleted\n * folder id look identical), and it does not need to be: either way it is unusable.\n *\n * The dry run rejects on the same rule. A preview that reported a cascade for a request the\n * executing call would refuse is worse than no preview — it is a confirmation an agent\n * cannot act on.\n */\n const matchedIds = new Set(matched.map((folder) => folder.id));\n const unresolvedIds = ids.filter((id) => matchedIds.has(id) === false);\n\n if (unresolvedIds.length > 0) {\n throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));\n }\n\n const folders = matched.map((folder) => sanitizeMediaFolder(folder));\n\n if (dryRun) {\n const counts = await countCascade(\n strapi,\n matched.map((folder) => folder.path)\n );\n\n return ok({ dryRun: true, folders, ...counts });\n }\n\n const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(\n matched.map((folder) => folder.id)\n );\n\n return ok({\n dryRun: false,\n folders,\n totalFolderNumber,\n totalFileNumber,\n });\n };\n};\n"],"names":["findFolderById","strapi","id","withParent","db","query","FOLDER_MODEL_UID","findOne","select","where","populate","parent","readFolderForOutput","fallback","row","sanitizeMediaFolder","assertParentExists","undefined","exists","getFolderService","errors","ValidationError","MCP_PARENT_FOLDER_NOT_FOUND","assertNameAvailable","name","excludeId","filters","$ne","MCP_FOLDER_NAME_TAKEN","assertNotMovedIntoOwnSubtree","folder","destination","isFolderOrChild","MCP_FOLDER_MOVE_INTO_SELF","countCascade","paths","pathPredicate","field","$or","flatMap","path","$eq","$startsWith","totalFolderNumber","totalFileNumber","Promise","all","count","FILE_MODEL_UID","createMediaCreateFolderHandler","context","assertAmbientInstance","args","assertMediaPermission","ACTIONS","create","created","user","ok","data","createMediaRenameFolderHandler","update","NotFoundError","MCP_NOT_FOUND_FOLDER","parentId","renamed","createMediaMoveFolderHandler","moved","createMediaDeleteFolderHandler","ids","dryRun","matched","findMany","$in","matchedIds","Set","map","unresolvedIds","filter","has","length","MCP_DELETE_FOLDER_UNRESOLVED_IDS","folders","counts","deleteByIds"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"folder-handlers.mjs","sources":["../../../../server/src/mcp/handlers/folder-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getFolderService } from '../ambient-instance';\nimport { ACTIONS, FOLDER_MODEL_UID, FILE_MODEL_UID } from '../../constants';\nimport { isFolderOrChild } from '../../controllers/utils/folders';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaFolder } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_FOLDER,\n MCP_FOLDER_NAME_TAKEN,\n MCP_PARENT_FOLDER_NOT_FOUND,\n MCP_FOLDER_MOVE_INTO_SELF,\n MCP_DELETE_FOLDER_UNRESOLVED_IDS,\n} from './constants';\nimport { ok } from '../utils';\n\nimport type { Folder } from '../../types';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's strict Zod\n// input schema before the handler runs, so unknown keys never reach here.\ntype CreateFolderArgs = {\n name: string;\n parent?: number | null;\n};\n\ntype RenameFolderArgs = {\n id: number;\n name: string;\n};\n\ntype MoveFolderArgs = {\n id: number;\n parent: number | null;\n};\n\ntype DeleteFolderArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The folder fields the handlers need; `path` drives the cascade and subtree checks. */\ntype FolderRow = Pick<Folder, 'id' | 'name' | 'path'>;\n\n/**\n * `withParent` populates the parent relation in the same round trip. Rename needs the parent id\n * to scope its sibling-uniqueness check, so it would otherwise read the same row twice; the\n * subtree and cascade callers do not, and skip the join.\n */\nconst findFolderById = async (\n strapi: Core.Strapi,\n id: number,\n { withParent = false }: { withParent?: boolean } = {}\n): Promise<(FolderRow & { parent?: { id: number } | null }) | null> =>\n strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name', 'path'],\n where: { id },\n ...(withParent ? { populate: { parent: { select: ['id'] } } } : {}),\n });\n\n/**\n * Re-reads a folder with its parent populated, so the write tools can echo back the same shape\n * regardless of what the service returned.\n *\n * `folder.update()` resolves the row it wrote without the `parent` relation, and on a pure rename\n * it never touches the join table at all — so the parent has to be read back to be reported.\n */\nconst readFolderForOutput = async (\n strapi: Core.Strapi,\n id: number,\n fallback: Record<string, unknown>\n) => {\n const row = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n where: { id },\n populate: { parent: { select: ['id', 'name'] } },\n });\n\n // The write succeeded, so report it even if the read-back comes back empty rather than\n // failing a completed operation on a follow-up query.\n return sanitizeMediaFolder(row ?? fallback);\n};\n\n/**\n * Replicates the admin folder controller's `parent` check: a named parent must exist.\n *\n * null is the media library root and always valid, so only a numeric id is looked up.\n */\nconst assertParentExists = async (strapi: Core.Strapi, parent: number | null | undefined) => {\n if (parent === null || parent === undefined) {\n return;\n }\n\n const exists = await getFolderService(strapi).exists({ id: parent });\n\n if (!exists) {\n throw new errors.ValidationError(MCP_PARENT_FOLDER_NOT_FOUND);\n }\n};\n\n/**\n * Replicates `is-folder-unique` from the admin folder validation: a folder name must be unique\n * among its siblings.\n *\n * `excludeId` is the folder being renamed — without it a rename to the folder's current name\n * would collide with itself.\n */\nconst assertNameAvailable = async (\n strapi: Core.Strapi,\n name: string,\n parent: number | null,\n excludeId?: number\n) => {\n const filters: Record<string, unknown> = { name, parent: parent ?? null };\n\n if (excludeId !== undefined) {\n filters.id = { $ne: excludeId };\n }\n\n if (await getFolderService(strapi).exists(filters)) {\n throw new errors.ValidationError(MCP_FOLDER_NAME_TAKEN);\n }\n};\n\n/**\n * Replicates `dont-move-inside-self`: a folder cannot become its own descendant.\n *\n * Without this the folder service would rewrite the subtree's materialized paths into a cycle,\n * orphaning every descendant — the tree has no root to walk back to afterwards.\n */\nconst assertNotMovedIntoOwnSubtree = async (\n strapi: Core.Strapi,\n folder: FolderRow,\n parent: number | null\n) => {\n if (parent === null) {\n return;\n }\n\n const destination = await findFolderById(strapi, parent);\n\n // A missing destination is already reported by `assertParentExists`.\n if (destination === null) {\n return;\n }\n\n if (isFolderOrChild(destination, folder)) {\n throw new errors.ValidationError(MCP_FOLDER_MOVE_INTO_SELF);\n }\n};\n\n/**\n * Counts the folders and files a `deleteByIds` of these paths would remove.\n *\n * Mirrors the two `$or` predicates in `folder.deleteByIds` exactly — self path plus the\n * `${path}/` prefix — so the dry-run number is the same set the destructive branch acts on\n * rather than an independent estimate that could drift from it.\n */\nconst countCascade = async (strapi: Core.Strapi, paths: string[]) => {\n const pathPredicate = (field: string) => ({\n $or: paths.flatMap((path) => [\n { [field]: { $eq: path } },\n { [field]: { $startsWith: `${path}/` } },\n ]),\n });\n\n const [totalFolderNumber, totalFileNumber] = await Promise.all([\n strapi.db.query(FOLDER_MODEL_UID).count({ where: pathPredicate('path') }),\n strapi.db.query(FILE_MODEL_UID).count({ where: pathPredicate('folderPath') }),\n ]);\n\n return { totalFolderNumber, totalFileNumber };\n};\n\n/**\n * `media_create_folder` — a new folder, optionally nested.\n *\n * Gated on `plugin::upload.assets.create` and mirrors `POST /upload/folders`: the same\n * permission, the same uniqueness and parent-existence validation, the same `folder.create()`\n * call, and the same `createdBy` attribution from the session user.\n *\n * `create` rather than the `update` the other folder tools use: creating a folder is the one\n * folder operation whose admin route gates on `assets.create`.\n */\nexport const createMediaCreateFolderHandler =\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 { name, parent = null } = args as CreateFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.create, FOLDER_MODEL_UID);\n\n await assertParentExists(strapi, parent);\n await assertNameAvailable(strapi, name, parent);\n\n const created = await getFolderService(strapi).create({ name, parent }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, created.id, created) });\n };\n\n/**\n * `media_rename_folder` — changes a folder's name and nothing else.\n *\n * Deliberately does not forward `parent` to `folder.update()`: the service branches on\n * `isUndefined(parent)`, and the name-only branch skips the transaction that rewrites descendant\n * paths. Passing the folder's existing parent would take the move branch and rewrite the whole\n * subtree to compute the identical paths.\n */\nexport const createMediaRenameFolderHandler =\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, name } = args as RenameFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id, { withParent: true });\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n // Siblings are the folders sharing this folder's parent, so uniqueness is checked against\n // the current location — a rename never changes it.\n const parentId = folder.parent?.id ?? null;\n\n await assertNameAvailable(strapi, name, parentId, id);\n\n // `parent` is omitted on purpose — see the note above.\n const renamed = await getFolderService(strapi).update(id, { name }, { user: context.user });\n\n return ok({ data: await readFolderForOutput(strapi, id, renamed ?? { ...folder, name }) });\n };\n\n/**\n * `media_move_folder` — re-parents a folder, carrying its whole subtree with it.\n *\n * `folder.update()` recalculates the materialized `path` of every descendant folder and the\n * `folderPath` of every contained file inside a transaction, so the move is atomic and no\n * separate bookkeeping is needed here.\n */\nexport const createMediaMoveFolderHandler =\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, parent } = args as MoveFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const folder = await findFolderById(strapi, id);\n\n if (folder === null) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_FOLDER);\n }\n\n await assertParentExists(strapi, parent);\n await assertNotMovedIntoOwnSubtree(strapi, folder, parent);\n\n // A folder keeps its name across a move, so uniqueness must hold in the destination.\n await assertNameAvailable(strapi, folder.name, parent, id);\n\n const moved = await getFolderService(strapi).update(\n id,\n { name: folder.name, parent },\n { user: context.user }\n );\n\n return ok({ data: await readFolderForOutput(strapi, id, moved ?? folder) });\n };\n\n/**\n * `media_delete_folder` — previews or performs a cascading folder deletion.\n *\n * Two branches behind one tool, because the preview and the deletion must agree on what the\n * cascade covers; splitting them across tools would let the two drift apart, and an agent could\n * reach the destructive one without ever seeing a count.\n *\n * `dryRun` defaults to true (see the input schema): omitting the flag previews, and deleting\n * takes an explicit `dryRun: false`.\n */\nexport const createMediaDeleteFolderHandler =\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 DeleteFolderArgs;\n\n assertMediaPermission(strapi, context, ACTIONS.update, FOLDER_MODEL_UID);\n\n const matched: FolderRow[] = await strapi.db.query(FOLDER_MODEL_UID).findMany({\n select: ['id', 'name', 'path'],\n where: { id: { $in: ids } },\n });\n\n /**\n * All-or-nothing: any id that does not resolve to a folder rejects the whole call, before\n * anything is deleted.\n *\n * Folder ids and asset ids are indistinguishable integers, so a list mixing the two is the\n * mistake this tool exists to catch — and deleting the folders that did match while\n * reporting the rest back would be exactly the silent confusion that requirement forbids,\n * with the cascade already gone by the time the agent reads the response.\n *\n * An unresolvable id cannot be diagnosed further from here (an asset id and a deleted\n * folder id look identical), and it does not need to be: either way it is unusable.\n *\n * The dry run rejects on the same rule. A preview that reported a cascade for a request the\n * executing call would refuse is worse than no preview — it is a confirmation an agent\n * cannot act on.\n */\n const matchedIds = new Set(matched.map((folder) => folder.id));\n const unresolvedIds = ids.filter((id) => matchedIds.has(id) === false);\n\n if (unresolvedIds.length > 0) {\n throw new errors.ValidationError(MCP_DELETE_FOLDER_UNRESOLVED_IDS(unresolvedIds));\n }\n\n const folders = matched.map((folder) => sanitizeMediaFolder(folder));\n\n if (dryRun) {\n const counts = await countCascade(\n strapi,\n matched.map((folder) => folder.path)\n );\n\n return ok({ dryRun: true, folders, ...counts });\n }\n\n const { totalFolderNumber, totalFileNumber } = await getFolderService(strapi).deleteByIds(\n matched.map((folder) => folder.id)\n );\n\n return ok({\n dryRun: false,\n folders,\n totalFolderNumber,\n totalFileNumber,\n });\n };\n"],"names":["findFolderById","strapi","id","withParent","db","query","FOLDER_MODEL_UID","findOne","select","where","populate","parent","readFolderForOutput","fallback","row","sanitizeMediaFolder","assertParentExists","undefined","exists","getFolderService","errors","ValidationError","MCP_PARENT_FOLDER_NOT_FOUND","assertNameAvailable","name","excludeId","filters","$ne","MCP_FOLDER_NAME_TAKEN","assertNotMovedIntoOwnSubtree","folder","destination","isFolderOrChild","MCP_FOLDER_MOVE_INTO_SELF","countCascade","paths","pathPredicate","field","$or","flatMap","path","$eq","$startsWith","totalFolderNumber","totalFileNumber","Promise","all","count","FILE_MODEL_UID","createMediaCreateFolderHandler","context","args","assertMediaPermission","ACTIONS","create","created","user","ok","data","createMediaRenameFolderHandler","update","NotFoundError","MCP_NOT_FOUND_FOLDER","parentId","renamed","createMediaMoveFolderHandler","moved","createMediaDeleteFolderHandler","ids","dryRun","matched","findMany","$in","matchedIds","Set","map","unresolvedIds","filter","has","length","MCP_DELETE_FOLDER_UNRESOLVED_IDS","folders","counts","deleteByIds"],"mappings":"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"}
|
|
@@ -48,9 +48,7 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
48
48
|
* Permission conditions are applied through the permissions manager
|
|
49
49
|
* (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —
|
|
50
50
|
* sees the same subset it would through the admin API.
|
|
51
|
-
*/ const createMediaListAssetsHandler = (strapi, context)=>{
|
|
52
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
53
|
-
return async ({ args })=>{
|
|
51
|
+
*/ const createMediaListAssetsHandler = (strapi, context)=>async ({ args })=>{
|
|
54
52
|
const { folderId, mime, name, page, pageSize, sort } = args;
|
|
55
53
|
const pm = permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FILE_MODEL_UID);
|
|
56
54
|
const query = await pm.addPermissionsQueryTo({
|
|
@@ -77,7 +75,6 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
77
75
|
pagination
|
|
78
76
|
});
|
|
79
77
|
};
|
|
80
|
-
};
|
|
81
78
|
/**
|
|
82
79
|
* `media_get_asset` — a single asset by numeric id.
|
|
83
80
|
*
|
|
@@ -86,9 +83,7 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
86
83
|
* before building the CASL subject, which is what the admin conditions read. Checking a subject
|
|
87
84
|
* without those fields would deny a conditioned grant its own assets, and would disagree with
|
|
88
85
|
* `media_list_assets`, where the condition is pushed into the query instead.
|
|
89
|
-
*/ const createMediaGetAssetHandler = (strapi, context)=>{
|
|
90
|
-
ambientInstance.assertAmbientInstance(strapi);
|
|
91
|
-
return async ({ args })=>{
|
|
86
|
+
*/ const createMediaGetAssetHandler = (strapi, context)=>async ({ args })=>{
|
|
92
87
|
const { id } = args;
|
|
93
88
|
// Model-level gate first, so a token without upload read at all is refused before a lookup.
|
|
94
89
|
permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FILE_MODEL_UID);
|
|
@@ -107,7 +102,6 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
107
102
|
data: sanitizeMedia.sanitizeMediaAsset(asset)
|
|
108
103
|
});
|
|
109
104
|
};
|
|
110
|
-
};
|
|
111
105
|
/**
|
|
112
106
|
* `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.
|
|
113
107
|
*
|
|
@@ -115,18 +109,16 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
115
109
|
* filtering, so this is gated on the model-level read permission only — matching
|
|
116
110
|
* `GET /upload/folder-structure` in the admin API.
|
|
117
111
|
*
|
|
118
|
-
* The
|
|
119
|
-
*
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return async ()=>{
|
|
112
|
+
* The folder service queries the ambient `global.strapi` regardless of the instance passed to
|
|
113
|
+
* `getService`; `buildUploadMcpToolDefinitions` asserts the two agree before this handler is
|
|
114
|
+
* ever constructed — see `assertAmbientInstance` for why.
|
|
115
|
+
*/ const createMediaListFoldersHandler = (strapi, context)=>async ()=>{
|
|
123
116
|
permissions.assertMediaPermission(strapi, context, constants.ACTIONS.read, constants.FOLDER_MODEL_UID);
|
|
124
117
|
const structure = await ambientInstance.getFolderService(strapi).getStructure();
|
|
125
118
|
return utils.ok({
|
|
126
119
|
data: sanitizeMedia.sanitizeMediaFolderTree(structure)
|
|
127
120
|
});
|
|
128
121
|
};
|
|
129
|
-
};
|
|
130
122
|
|
|
131
123
|
exports.createMediaGetAssetHandler = createMediaGetAssetHandler;
|
|
132
124
|
exports.createMediaListAssetsHandler = createMediaListAssetsHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-handlers.js","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport {
|
|
1
|
+
{"version":3,"file":"read-handlers.js","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { getFolderService } from '../ambient-instance';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { assertMediaPermission } from '../permissions';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { sanitizeMediaAsset, sanitizeMediaFolderTree } from '../sanitizers/sanitize-media';\nimport { MCP_NOT_FOUND_ASSET } from './constants';\nimport { ok } from '../utils';\n\nconst DEFAULT_SORT = 'createdAt:DESC';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's Zod input\n// schema before the handler runs, so handlers accept the erased `Record<string, unknown>` the\n// registry hands them and narrow it here.\ntype ListMediaArgs = {\n folderId?: number | null;\n mime?: string;\n name?: string;\n page?: number;\n pageSize?: number;\n sort?: string;\n};\n\ntype GetMediaArgs = {\n id: number;\n};\n\n/**\n * Builds the `filters` clause for `media_list_assets`.\n *\n * `folderId: null` is meaningfully different from an omitted `folderId`: null means \"assets at\n * the media library root\" (no folder relation), while omitting it means \"any folder\".\n *\n * `mime` accepts both a full type and a bare prefix. An exact match on \"image\" would never hit,\n * so a value without a slash is matched as a prefix (\"image\" → every \"image/*\").\n */\nconst buildAssetFilters = (args: ListMediaArgs): Record<string, unknown> => {\n const filters: Record<string, unknown> = {};\n\n if (args.folderId === null) {\n filters.folder = null;\n } else if (args.folderId !== undefined) {\n filters.folder = { id: args.folderId };\n }\n\n if (args.mime !== undefined) {\n filters.mime = args.mime.includes('/')\n ? { $eqi: args.mime }\n : { $startsWithi: `${args.mime}/` };\n }\n\n if (args.name !== undefined) {\n filters.name = { $containsi: args.name };\n }\n\n return filters;\n};\n\n/**\n * `media_list_assets` — paginated, filtered listing of media files.\n *\n * Permission conditions are applied through the permissions manager\n * (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —\n * sees the same subset it would through the admin API.\n */\nexport const createMediaListAssetsHandler =\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 { folderId, mime, name, page, pageSize, sort } = args as ListMediaArgs;\n const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n const query = await pm.addPermissionsQueryTo({\n filters: buildAssetFilters({ folderId, mime, name }),\n sort: sort ?? DEFAULT_SORT,\n page: page ?? 1,\n pageSize: pageSize ?? 25,\n populate: { folder: { fields: ['id', 'name'] } },\n });\n\n const { results, pagination } = await getService('upload', strapi).findPage(query);\n\n return ok({\n results: results.map(sanitizeMediaAsset),\n pagination,\n });\n };\n\n/**\n * `media_get_asset` — a single asset by numeric id.\n *\n * Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same\n * helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles\n * before building the CASL subject, which is what the admin conditions read. Checking a subject\n * without those fields would deny a conditioned grant its own assets, and would disagree with\n * `media_list_assets`, where the condition is pushed into the query instead.\n */\nexport const createMediaGetAssetHandler =\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 } = args as GetMediaArgs;\n\n // Model-level gate first, so a token without upload read at all is refused before a lookup.\n assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n let asset;\n try {\n ({ file: asset } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.read,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n // The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an\n // agent, so restate it with the message the other media tools use.\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n throw error;\n }\n\n return ok({ data: sanitizeMediaAsset(asset) });\n };\n\n/**\n * `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.\n *\n * `getStructure()` returns the whole tree in one query and has no permission-condition\n * filtering, so this is gated on the model-level read permission only — matching\n * `GET /upload/folder-structure` in the admin API.\n *\n * The folder service queries the ambient `global.strapi` regardless of the instance passed to\n * `getService`; `buildUploadMcpToolDefinitions` asserts the two agree before this handler is\n * ever constructed — see `assertAmbientInstance` for why.\n */\nexport const createMediaListFoldersHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async (): Promise<Modules.MCP.McpToolHandlerReturn> => {\n assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);\n\n const structure = await getFolderService(strapi).getStructure();\n\n return ok({ data: sanitizeMediaFolderTree(structure) });\n };\n"],"names":["DEFAULT_SORT","buildAssetFilters","args","filters","folderId","folder","undefined","id","mime","includes","$eqi","$startsWithi","name","$containsi","createMediaListAssetsHandler","strapi","context","page","pageSize","sort","pm","assertMediaPermission","ACTIONS","read","FILE_MODEL_UID","query","addPermissionsQueryTo","populate","fields","results","pagination","getService","findPage","ok","map","sanitizeMediaAsset","createMediaGetAssetHandler","asset","file","findEntityAndCheckPermissions","userAbility","error","errors","NotFoundError","MCP_NOT_FOUND_ASSET","data","createMediaListFoldersHandler","FOLDER_MODEL_UID","structure","getFolderService","getStructure","sanitizeMediaFolderTree"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { errors } from '@strapi/utils';
|
|
2
2
|
import { getService } from '../../utils/index.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { getFolderService } from '../ambient-instance.mjs';
|
|
4
4
|
import { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants.mjs';
|
|
5
5
|
import { assertMediaPermission } from '../permissions.mjs';
|
|
6
6
|
import { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions.mjs';
|
|
@@ -46,9 +46,7 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
46
46
|
* Permission conditions are applied through the permissions manager
|
|
47
47
|
* (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —
|
|
48
48
|
* sees the same subset it would through the admin API.
|
|
49
|
-
*/ const createMediaListAssetsHandler = (strapi, context)=>{
|
|
50
|
-
assertAmbientInstance(strapi);
|
|
51
|
-
return async ({ args })=>{
|
|
49
|
+
*/ const createMediaListAssetsHandler = (strapi, context)=>async ({ args })=>{
|
|
52
50
|
const { folderId, mime, name, page, pageSize, sort } = args;
|
|
53
51
|
const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);
|
|
54
52
|
const query = await pm.addPermissionsQueryTo({
|
|
@@ -75,7 +73,6 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
75
73
|
pagination
|
|
76
74
|
});
|
|
77
75
|
};
|
|
78
|
-
};
|
|
79
76
|
/**
|
|
80
77
|
* `media_get_asset` — a single asset by numeric id.
|
|
81
78
|
*
|
|
@@ -84,9 +81,7 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
84
81
|
* before building the CASL subject, which is what the admin conditions read. Checking a subject
|
|
85
82
|
* without those fields would deny a conditioned grant its own assets, and would disagree with
|
|
86
83
|
* `media_list_assets`, where the condition is pushed into the query instead.
|
|
87
|
-
*/ const createMediaGetAssetHandler = (strapi, context)=>{
|
|
88
|
-
assertAmbientInstance(strapi);
|
|
89
|
-
return async ({ args })=>{
|
|
84
|
+
*/ const createMediaGetAssetHandler = (strapi, context)=>async ({ args })=>{
|
|
90
85
|
const { id } = args;
|
|
91
86
|
// Model-level gate first, so a token without upload read at all is refused before a lookup.
|
|
92
87
|
assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);
|
|
@@ -105,7 +100,6 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
105
100
|
data: sanitizeMediaAsset(asset)
|
|
106
101
|
});
|
|
107
102
|
};
|
|
108
|
-
};
|
|
109
103
|
/**
|
|
110
104
|
* `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.
|
|
111
105
|
*
|
|
@@ -113,18 +107,16 @@ const DEFAULT_SORT = 'createdAt:DESC';
|
|
|
113
107
|
* filtering, so this is gated on the model-level read permission only — matching
|
|
114
108
|
* `GET /upload/folder-structure` in the admin API.
|
|
115
109
|
*
|
|
116
|
-
* The
|
|
117
|
-
*
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return async ()=>{
|
|
110
|
+
* The folder service queries the ambient `global.strapi` regardless of the instance passed to
|
|
111
|
+
* `getService`; `buildUploadMcpToolDefinitions` asserts the two agree before this handler is
|
|
112
|
+
* ever constructed — see `assertAmbientInstance` for why.
|
|
113
|
+
*/ const createMediaListFoldersHandler = (strapi, context)=>async ()=>{
|
|
121
114
|
assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);
|
|
122
115
|
const structure = await getFolderService(strapi).getStructure();
|
|
123
116
|
return ok({
|
|
124
117
|
data: sanitizeMediaFolderTree(structure)
|
|
125
118
|
});
|
|
126
119
|
};
|
|
127
|
-
};
|
|
128
120
|
|
|
129
121
|
export { createMediaGetAssetHandler, createMediaListAssetsHandler, createMediaListFoldersHandler };
|
|
130
122
|
//# sourceMappingURL=read-handlers.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-handlers.mjs","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport {
|
|
1
|
+
{"version":3,"file":"read-handlers.mjs","sources":["../../../../server/src/mcp/handlers/read-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { getFolderService } from '../ambient-instance';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { assertMediaPermission } from '../permissions';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { sanitizeMediaAsset, sanitizeMediaFolderTree } from '../sanitizers/sanitize-media';\nimport { MCP_NOT_FOUND_ASSET } from './constants';\nimport { ok } from '../utils';\n\nconst DEFAULT_SORT = 'createdAt:DESC';\n\n// Arg types are type-level only: the MCP SDK validates `args` against the tool's Zod input\n// schema before the handler runs, so handlers accept the erased `Record<string, unknown>` the\n// registry hands them and narrow it here.\ntype ListMediaArgs = {\n folderId?: number | null;\n mime?: string;\n name?: string;\n page?: number;\n pageSize?: number;\n sort?: string;\n};\n\ntype GetMediaArgs = {\n id: number;\n};\n\n/**\n * Builds the `filters` clause for `media_list_assets`.\n *\n * `folderId: null` is meaningfully different from an omitted `folderId`: null means \"assets at\n * the media library root\" (no folder relation), while omitting it means \"any folder\".\n *\n * `mime` accepts both a full type and a bare prefix. An exact match on \"image\" would never hit,\n * so a value without a slash is matched as a prefix (\"image\" → every \"image/*\").\n */\nconst buildAssetFilters = (args: ListMediaArgs): Record<string, unknown> => {\n const filters: Record<string, unknown> = {};\n\n if (args.folderId === null) {\n filters.folder = null;\n } else if (args.folderId !== undefined) {\n filters.folder = { id: args.folderId };\n }\n\n if (args.mime !== undefined) {\n filters.mime = args.mime.includes('/')\n ? { $eqi: args.mime }\n : { $startsWithi: `${args.mime}/` };\n }\n\n if (args.name !== undefined) {\n filters.name = { $containsi: args.name };\n }\n\n return filters;\n};\n\n/**\n * `media_list_assets` — paginated, filtered listing of media files.\n *\n * Permission conditions are applied through the permissions manager\n * (`addPermissionsQueryTo`) so a token restricted by a condition — e.g. own-assets-only —\n * sees the same subset it would through the admin API.\n */\nexport const createMediaListAssetsHandler =\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 { folderId, mime, name, page, pageSize, sort } = args as ListMediaArgs;\n const pm = assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n const query = await pm.addPermissionsQueryTo({\n filters: buildAssetFilters({ folderId, mime, name }),\n sort: sort ?? DEFAULT_SORT,\n page: page ?? 1,\n pageSize: pageSize ?? 25,\n populate: { folder: { fields: ['id', 'name'] } },\n });\n\n const { results, pagination } = await getService('upload', strapi).findPage(query);\n\n return ok({\n results: results.map(sanitizeMediaAsset),\n pagination,\n });\n };\n\n/**\n * `media_get_asset` — a single asset by numeric id.\n *\n * Row-level permission *conditions* are enforced by `findEntityAndCheckPermissions`, the same\n * helper the admin `findOne` controller uses: it populates `createdBy` and the creator's roles\n * before building the CASL subject, which is what the admin conditions read. Checking a subject\n * without those fields would deny a conditioned grant its own assets, and would disagree with\n * `media_list_assets`, where the condition is pushed into the query instead.\n */\nexport const createMediaGetAssetHandler =\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 } = args as GetMediaArgs;\n\n // Model-level gate first, so a token without upload read at all is refused before a lookup.\n assertMediaPermission(strapi, context, ACTIONS.read, FILE_MODEL_UID);\n\n let asset;\n try {\n ({ file: asset } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.read,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n // The helper is written for HTTP, where a bare NotFoundError is enough. MCP answers an\n // agent, so restate it with the message the other media tools use.\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n throw error;\n }\n\n return ok({ data: sanitizeMediaAsset(asset) });\n };\n\n/**\n * `media_list_folders` — the nested folder structure, reusing `folder.getStructure()`.\n *\n * `getStructure()` returns the whole tree in one query and has no permission-condition\n * filtering, so this is gated on the model-level read permission only — matching\n * `GET /upload/folder-structure` in the admin API.\n *\n * The folder service queries the ambient `global.strapi` regardless of the instance passed to\n * `getService`; `buildUploadMcpToolDefinitions` asserts the two agree before this handler is\n * ever constructed — see `assertAmbientInstance` for why.\n */\nexport const createMediaListFoldersHandler =\n (strapi: Core.Strapi, context: Modules.MCP.McpHandlerContext) =>\n async (): Promise<Modules.MCP.McpToolHandlerReturn> => {\n assertMediaPermission(strapi, context, ACTIONS.read, FOLDER_MODEL_UID);\n\n const structure = await getFolderService(strapi).getStructure();\n\n return ok({ data: sanitizeMediaFolderTree(structure) });\n };\n"],"names":["DEFAULT_SORT","buildAssetFilters","args","filters","folderId","folder","undefined","id","mime","includes","$eqi","$startsWithi","name","$containsi","createMediaListAssetsHandler","strapi","context","page","pageSize","sort","pm","assertMediaPermission","ACTIONS","read","FILE_MODEL_UID","query","addPermissionsQueryTo","populate","fields","results","pagination","getService","findPage","ok","map","sanitizeMediaAsset","createMediaGetAssetHandler","asset","file","findEntityAndCheckPermissions","userAbility","error","errors","NotFoundError","MCP_NOT_FOUND_ASSET","data","createMediaListFoldersHandler","FOLDER_MODEL_UID","structure","getFolderService","getStructure","sanitizeMediaFolderTree"],"mappings":"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"}
|