@strapi/upload 5.53.0 → 5.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/dist/admin/future/components/UploadProgressDialog.js +9 -8
  2. package/dist/admin/future/components/UploadProgressDialog.js.map +1 -1
  3. package/dist/admin/future/components/UploadProgressDialog.mjs +9 -8
  4. package/dist/admin/future/components/UploadProgressDialog.mjs.map +1 -1
  5. package/dist/admin/future/pages/Assets/AssetsPage.js +0 -2
  6. package/dist/admin/future/pages/Assets/AssetsPage.js.map +1 -1
  7. package/dist/admin/future/pages/Assets/AssetsPage.mjs +0 -2
  8. package/dist/admin/future/pages/Assets/AssetsPage.mjs.map +1 -1
  9. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js +7 -0
  10. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.js.map +1 -1
  11. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs +7 -0
  12. package/dist/admin/future/pages/Assets/components/Dnd/AssetsDndProvider.mjs.map +1 -1
  13. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js +4 -1
  14. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.js.map +1 -1
  15. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs +4 -1
  16. package/dist/admin/future/pages/Assets/components/Dnd/DragOverlayChip.mjs.map +1 -1
  17. package/dist/admin/future/services/api.js +17 -0
  18. package/dist/admin/future/services/api.js.map +1 -1
  19. package/dist/admin/future/services/api.mjs +18 -1
  20. package/dist/admin/future/services/api.mjs.map +1 -1
  21. package/dist/admin/future/store/uploadProgress.js +31 -22
  22. package/dist/admin/future/store/uploadProgress.js.map +1 -1
  23. package/dist/admin/future/store/uploadProgress.mjs +31 -22
  24. package/dist/admin/future/store/uploadProgress.mjs.map +1 -1
  25. package/dist/admin/index.js +11 -10
  26. package/dist/admin/index.js.map +1 -1
  27. package/dist/admin/index.mjs +11 -10
  28. package/dist/admin/index.mjs.map +1 -1
  29. package/dist/admin/src/future/store/uploadProgress.d.ts +14 -20
  30. package/dist/admin/translations/en.json.js +0 -3
  31. package/dist/admin/translations/en.json.js.map +1 -1
  32. package/dist/admin/translations/en.json.mjs +0 -3
  33. package/dist/admin/translations/en.json.mjs.map +1 -1
  34. package/dist/server/bootstrap.js +11 -0
  35. package/dist/server/bootstrap.js.map +1 -1
  36. package/dist/server/bootstrap.mjs +11 -0
  37. package/dist/server/bootstrap.mjs.map +1 -1
  38. package/dist/server/controllers/admin-upload.js +31 -1
  39. package/dist/server/controllers/admin-upload.js.map +1 -1
  40. package/dist/server/controllers/admin-upload.mjs +31 -1
  41. package/dist/server/controllers/admin-upload.mjs.map +1 -1
  42. package/dist/server/controllers/utils/find-entity-and-check-permissions.js +15 -4
  43. package/dist/server/controllers/utils/find-entity-and-check-permissions.js.map +1 -1
  44. package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs +15 -4
  45. package/dist/server/controllers/utils/find-entity-and-check-permissions.mjs.map +1 -1
  46. package/dist/server/controllers/utils/folders.js +1 -1
  47. package/dist/server/controllers/utils/folders.js.map +1 -1
  48. package/dist/server/controllers/utils/folders.mjs +1 -1
  49. package/dist/server/controllers/utils/folders.mjs.map +1 -1
  50. package/dist/server/mcp/ambient-instance.js +46 -0
  51. package/dist/server/mcp/ambient-instance.js.map +1 -0
  52. package/dist/server/mcp/ambient-instance.mjs +43 -0
  53. package/dist/server/mcp/ambient-instance.mjs.map +1 -0
  54. package/dist/server/mcp/handlers/constants.js +60 -0
  55. package/dist/server/mcp/handlers/constants.js.map +1 -0
  56. package/dist/server/mcp/handlers/constants.mjs +45 -0
  57. package/dist/server/mcp/handlers/constants.mjs.map +1 -0
  58. package/dist/server/mcp/handlers/folder-handlers.js +308 -0
  59. package/dist/server/mcp/handlers/folder-handlers.js.map +1 -0
  60. package/dist/server/mcp/handlers/folder-handlers.mjs +303 -0
  61. package/dist/server/mcp/handlers/folder-handlers.mjs.map +1 -0
  62. package/dist/server/mcp/handlers/read-handlers.js +134 -0
  63. package/dist/server/mcp/handlers/read-handlers.js.map +1 -0
  64. package/dist/server/mcp/handlers/read-handlers.mjs +130 -0
  65. package/dist/server/mcp/handlers/read-handlers.mjs.map +1 -0
  66. package/dist/server/mcp/handlers/write-handlers.js +315 -0
  67. package/dist/server/mcp/handlers/write-handlers.js.map +1 -0
  68. package/dist/server/mcp/handlers/write-handlers.mjs +311 -0
  69. package/dist/server/mcp/handlers/write-handlers.mjs.map +1 -0
  70. package/dist/server/mcp/permissions.js +36 -0
  71. package/dist/server/mcp/permissions.js.map +1 -0
  72. package/dist/server/mcp/permissions.mjs +33 -0
  73. package/dist/server/mcp/permissions.mjs.map +1 -0
  74. package/dist/server/mcp/register-upload-mcp-tools.js +235 -0
  75. package/dist/server/mcp/register-upload-mcp-tools.js.map +1 -0
  76. package/dist/server/mcp/register-upload-mcp-tools.mjs +232 -0
  77. package/dist/server/mcp/register-upload-mcp-tools.mjs.map +1 -0
  78. package/dist/server/mcp/sanitizers/sanitize-media.js +97 -0
  79. package/dist/server/mcp/sanitizers/sanitize-media.js.map +1 -0
  80. package/dist/server/mcp/sanitizers/sanitize-media.mjs +93 -0
  81. package/dist/server/mcp/sanitizers/sanitize-media.mjs.map +1 -0
  82. package/dist/server/mcp/schemas/input-schemas.js +206 -0
  83. package/dist/server/mcp/schemas/input-schemas.js.map +1 -0
  84. package/dist/server/mcp/schemas/input-schemas.mjs +190 -0
  85. package/dist/server/mcp/schemas/input-schemas.mjs.map +1 -0
  86. package/dist/server/mcp/schemas/output-schemas.js +158 -0
  87. package/dist/server/mcp/schemas/output-schemas.js.map +1 -0
  88. package/dist/server/mcp/schemas/output-schemas.mjs +142 -0
  89. package/dist/server/mcp/schemas/output-schemas.mjs.map +1 -0
  90. package/dist/server/mcp/utils.js +14 -0
  91. package/dist/server/mcp/utils.js.map +1 -0
  92. package/dist/server/mcp/utils.mjs +12 -0
  93. package/dist/server/mcp/utils.mjs.map +1 -0
  94. package/dist/server/media-library-default-notice.js +42 -0
  95. package/dist/server/media-library-default-notice.js.map +1 -0
  96. package/dist/server/media-library-default-notice.mjs +40 -0
  97. package/dist/server/media-library-default-notice.mjs.map +1 -0
  98. package/dist/server/register.js +5 -0
  99. package/dist/server/register.js.map +1 -1
  100. package/dist/server/register.mjs +5 -0
  101. package/dist/server/register.mjs.map +1 -1
  102. package/dist/server/services/folder.js.map +1 -1
  103. package/dist/server/services/folder.mjs.map +1 -1
  104. package/dist/server/services/upload.js +14 -2
  105. package/dist/server/services/upload.js.map +1 -1
  106. package/dist/server/services/upload.mjs +14 -2
  107. package/dist/server/services/upload.mjs.map +1 -1
  108. package/dist/server/src/bootstrap.d.ts.map +1 -1
  109. package/dist/server/src/controllers/admin-upload.d.ts +1 -0
  110. package/dist/server/src/controllers/admin-upload.d.ts.map +1 -1
  111. package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts +14 -1
  112. package/dist/server/src/controllers/utils/find-entity-and-check-permissions.d.ts.map +1 -1
  113. package/dist/server/src/controllers/utils/folders.d.ts +2 -1
  114. package/dist/server/src/controllers/utils/folders.d.ts.map +1 -1
  115. package/dist/server/src/index.d.ts +8 -2
  116. package/dist/server/src/index.d.ts.map +1 -1
  117. package/dist/server/src/mcp/ambient-instance.d.ts +65 -0
  118. package/dist/server/src/mcp/ambient-instance.d.ts.map +1 -0
  119. package/dist/server/src/mcp/handlers/constants.d.ts +48 -0
  120. package/dist/server/src/mcp/handlers/constants.d.ts.map +1 -0
  121. package/dist/server/src/mcp/handlers/folder-handlers.d.ts +49 -0
  122. package/dist/server/src/mcp/handlers/folder-handlers.d.ts.map +1 -0
  123. package/dist/server/src/mcp/handlers/index.d.ts +5 -0
  124. package/dist/server/src/mcp/handlers/index.d.ts.map +1 -0
  125. package/dist/server/src/mcp/handlers/read-handlers.d.ts +35 -0
  126. package/dist/server/src/mcp/handlers/read-handlers.d.ts.map +1 -0
  127. package/dist/server/src/mcp/handlers/write-handlers.d.ts +66 -0
  128. package/dist/server/src/mcp/handlers/write-handlers.d.ts.map +1 -0
  129. package/dist/server/src/mcp/index.d.ts +3 -0
  130. package/dist/server/src/mcp/index.d.ts.map +1 -0
  131. package/dist/server/src/mcp/permissions.d.ts +22 -0
  132. package/dist/server/src/mcp/permissions.d.ts.map +1 -0
  133. package/dist/server/src/mcp/register-upload-mcp-tools.d.ts +29 -0
  134. package/dist/server/src/mcp/register-upload-mcp-tools.d.ts.map +1 -0
  135. package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts +63 -0
  136. package/dist/server/src/mcp/sanitizers/sanitize-media.d.ts.map +1 -0
  137. package/dist/server/src/mcp/schemas/index.d.ts +3 -0
  138. package/dist/server/src/mcp/schemas/index.d.ts.map +1 -0
  139. package/dist/server/src/mcp/schemas/input-schemas.d.ts +148 -0
  140. package/dist/server/src/mcp/schemas/input-schemas.d.ts.map +1 -0
  141. package/dist/server/src/mcp/schemas/output-schemas.d.ts +276 -0
  142. package/dist/server/src/mcp/schemas/output-schemas.d.ts.map +1 -0
  143. package/dist/server/src/mcp/types.d.ts +19 -0
  144. package/dist/server/src/mcp/types.d.ts.map +1 -0
  145. package/dist/server/src/mcp/utils.d.ts +4 -0
  146. package/dist/server/src/mcp/utils.d.ts.map +1 -0
  147. package/dist/server/src/media-library-default-notice.d.ts +19 -0
  148. package/dist/server/src/media-library-default-notice.d.ts.map +1 -0
  149. package/dist/server/src/register.d.ts.map +1 -1
  150. package/dist/server/src/services/folder.d.ts +6 -2
  151. package/dist/server/src/services/folder.d.ts.map +1 -1
  152. package/dist/server/src/services/index.d.ts +8 -2
  153. package/dist/server/src/services/index.d.ts.map +1 -1
  154. package/dist/server/src/services/upload.d.ts +8 -1
  155. package/dist/server/src/services/upload.d.ts.map +1 -1
  156. package/dist/server/src/types.d.ts +2 -2
  157. package/dist/server/src/types.d.ts.map +1 -1
  158. package/dist/server/src/utils/index.d.ts +2 -1
  159. package/dist/server/src/utils/index.d.ts.map +1 -1
  160. package/dist/server/utils/index.js +2 -2
  161. package/dist/server/utils/index.js.map +1 -1
  162. package/dist/server/utils/index.mjs +2 -2
  163. package/dist/server/utils/index.mjs.map +1 -1
  164. package/dist/shared/contracts/files.d.ts +22 -0
  165. package/dist/shared/contracts/files.d.ts.map +1 -1
  166. package/package.json +7 -6
  167. package/dist/admin/future/components/BetaNotice.js +0 -72
  168. package/dist/admin/future/components/BetaNotice.js.map +0 -1
  169. package/dist/admin/future/components/BetaNotice.mjs +0 -70
  170. package/dist/admin/future/components/BetaNotice.mjs.map +0 -1
  171. package/dist/admin/src/future/components/BetaNotice.d.ts +0 -11
@@ -0,0 +1,311 @@
1
+ import { errors } from '@strapi/utils';
2
+ import { getService } from '../../utils/index.mjs';
3
+ import { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants.mjs';
4
+ import { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions.mjs';
5
+ import { assertAmbientInstance } from '../ambient-instance.mjs';
6
+ import { assertMediaPermission } from '../permissions.mjs';
7
+ import { sanitizeMediaAsset } from '../sanitizers/sanitize-media.mjs';
8
+ import { MCP_UPDATE_ASSET_NO_FIELDS, MCP_NOT_FOUND_ASSET, MCP_MOVE_ASSETS_ID_FAILED, MCP_DELETE_ASSETS_ID_FAILED, MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND, MCP_MOVE_ASSETS_ID_NOT_FOUND, MCP_MOVE_ASSETS_ID_FORBIDDEN, MCP_DELETE_ASSETS_ID_NOT_FOUND, MCP_DELETE_ASSETS_ID_FORBIDDEN } from './constants.mjs';
9
+ import { ok } from '../utils.mjs';
10
+
11
+ /** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */ const WRITABLE_FIELDS = [
12
+ 'name',
13
+ 'alternativeText',
14
+ 'caption'
15
+ ];
16
+ /**
17
+ * Picks the metadata the caller actually sent.
18
+ *
19
+ * `updateFileInfo` treats nil as "keep the stored value" (`_.isNil`), so an explicit
20
+ * `alternativeText: null` cannot be forwarded as null — it would be read as "unchanged"
21
+ * instead of "clear it". Clearing is expressed as an empty string, which is what the admin
22
+ * panel writes when the field is emptied.
23
+ */ const buildFileInfo = (args)=>{
24
+ const fileInfo = {};
25
+ for (const field of WRITABLE_FIELDS){
26
+ const value = args[field];
27
+ if (value !== undefined) {
28
+ fileInfo[field] = value === null ? '' : value;
29
+ }
30
+ }
31
+ return fileInfo;
32
+ };
33
+ /**
34
+ * `media_update_asset` — edits the writable metadata of one asset.
35
+ *
36
+ * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same
37
+ * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and
38
+ * the same `updatedBy` attribution from the session user.
39
+ *
40
+ * The response carries the updated asset through the read sanitizer, so a client can confirm
41
+ * the write without a second `media_get_asset` round-trip — and so provider fields stay
42
+ * invisible on the write path too.
43
+ */ const createMediaUpdateAssetHandler = (strapi, context)=>{
44
+ assertAmbientInstance(strapi);
45
+ return async ({ args })=>{
46
+ const { id, ...metadata } = args;
47
+ const fileInfo = buildFileInfo(metadata);
48
+ // A patch with no writable field is a caller error, not a no-op success: the schema cannot
49
+ // express "at least one of" without becoming a ZodEffects the registry can't publish.
50
+ if (Object.keys(fileInfo).length === 0) {
51
+ throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);
52
+ }
53
+ // Model-level gate first, so a token without the action is refused before any DB read.
54
+ // `findEntityAndCheckPermissions` only covers the row-level check.
55
+ assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);
56
+ // Row-level check, shared with the admin controller: resolves the creator's roles so an
57
+ // owner-scoped permission condition is evaluated against the same subject the REST API
58
+ // would build, and throws Forbidden when a condition excludes this asset.
59
+ //
60
+ // Its NotFoundError carries no message, which would reach the agent as a bare "Not Found";
61
+ // rethrowing adds the same wording the read tools use.
62
+ try {
63
+ await findEntityAndCheckPermissions(context.userAbility, ACTIONS.update, FILE_MODEL_UID, id, strapi);
64
+ } catch (error) {
65
+ if (error instanceof errors.NotFoundError) {
66
+ throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);
67
+ }
68
+ throw error;
69
+ }
70
+ const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {
71
+ user: context.user
72
+ });
73
+ const updated = await getService('upload', strapi).findOne(id, [
74
+ 'folder'
75
+ ]);
76
+ return ok({
77
+ data: sanitizeMediaAsset(updated ?? written)
78
+ });
79
+ };
80
+ };
81
+ /**
82
+ * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.
83
+ *
84
+ * `null` is the media library root and always valid. A named folder is looked up so a typo — or a
85
+ * folder id passed where the agent meant an asset — is refused before anything is moved, matching
86
+ * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.
87
+ *
88
+ * A bad destination is the one failure that rejects the whole call rather than being reported per
89
+ * id: it is a property of the request, not of any single asset, so every id would fail for the
90
+ * same reason and a per-id report would say nothing the agent could act on.
91
+ */ const resolveDestinationFolder = async (strapi, folder)=>{
92
+ if (folder === null) {
93
+ return null;
94
+ }
95
+ const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({
96
+ select: [
97
+ 'id',
98
+ 'name'
99
+ ],
100
+ where: {
101
+ id: folder
102
+ }
103
+ });
104
+ if (destination === null || destination === undefined) {
105
+ throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);
106
+ }
107
+ return {
108
+ id: Number(destination.id),
109
+ name: String(destination.name ?? '')
110
+ };
111
+ };
112
+ /**
113
+ * `media_move_assets` — moves assets between folders in bulk.
114
+ *
115
+ * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves
116
+ * many assets at once, and one bad id among good ones must not discard the valid moves. Each
117
+ * asset is moved on its own, so a failure is confined to its own id and the response says exactly
118
+ * which ids to retry.
119
+ *
120
+ * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single
121
+ * transaction is all-or-nothing by construction (and which also handles folders, out of scope
122
+ * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and
123
+ * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private
124
+ * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the
125
+ * controller's raw join-table writes would skip all four.
126
+ *
127
+ * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its
128
+ * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the
129
+ * connection-pool contention of firing them in parallel.
130
+ */ const createMediaMoveAssetsHandler = (strapi, context)=>{
131
+ assertAmbientInstance(strapi);
132
+ return async ({ args })=>{
133
+ const { ids, folder } = args;
134
+ // Model-level gate first, so a token without the action is refused before any DB read.
135
+ assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);
136
+ const destinationFolder = await resolveDestinationFolder(strapi, folder);
137
+ const moved = [];
138
+ const failed = [];
139
+ // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than
140
+ // reporting the same asset twice for a request that moved it once.
141
+ for (const id of new Set(ids)){
142
+ try {
143
+ // Row-level check, shared with the admin controller: an owner-scoped permission
144
+ // condition is evaluated against the same subject the REST API would build.
145
+ await findEntityAndCheckPermissions(context.userAbility, ACTIONS.update, FILE_MODEL_UID, id, strapi);
146
+ const updated = await getService('upload', strapi).updateFileInfo(id, // `updateFileInfo` reads `undefined` as "keep the stored folder" and null as the root,
147
+ // so the destination is forwarded as-is.
148
+ {
149
+ folder
150
+ }, {
151
+ user: context.user
152
+ });
153
+ // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so
154
+ // the destination is attached here instead of costing a read-back per asset.
155
+ moved.push(sanitizeMediaAsset({
156
+ ...updated,
157
+ folder: destinationFolder
158
+ }));
159
+ } catch (error) {
160
+ if (error instanceof errors.NotFoundError) {
161
+ failed.push({
162
+ id,
163
+ reason: MCP_MOVE_ASSETS_ID_NOT_FOUND
164
+ });
165
+ continue;
166
+ }
167
+ if (error instanceof errors.ForbiddenError) {
168
+ failed.push({
169
+ id,
170
+ reason: MCP_MOVE_ASSETS_ID_FORBIDDEN
171
+ });
172
+ continue;
173
+ }
174
+ /**
175
+ * Any other failure — a DB error, a provider fault — is reported against this id too,
176
+ * rather than thrown.
177
+ *
178
+ * Throwing here would be the one way to lose committed work silently: a tool error
179
+ * reaches the client as `{ content: [text], isError: true }` with NO
180
+ * `structuredContent` (see `tool-registry.ts`), so an error on the third id would
181
+ * discard the report saying the first two had already moved. The agent would be left
182
+ * unable to tell which ids to retry — exactly the recoverability this tool exists to
183
+ * provide.
184
+ *
185
+ * The message is carried through verbatim so the real fault is still legible, and the
186
+ * loop continues: one broken asset must not strand the rest of a reorganisation.
187
+ */ failed.push({
188
+ id,
189
+ reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
190
+ });
191
+ }
192
+ }
193
+ /**
194
+ * A request where nothing moved still returns the per-id report.
195
+ *
196
+ * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing
197
+ * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool
198
+ * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a
199
+ * machine-readable entry, for the same class of mistake.
200
+ *
201
+ * The only failure that still rejects the whole call is an invalid destination folder, which
202
+ * is checked before the loop: nothing has moved, so there is no report to preserve.
203
+ */ return ok({
204
+ destinationFolder,
205
+ moved,
206
+ failed
207
+ });
208
+ };
209
+ };
210
+ /**
211
+ * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.
212
+ *
213
+ * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the
214
+ * deletion must agree on what would be destroyed, and splitting them across tools would both let
215
+ * the two drift apart and let an agent reach the destructive one without ever seeing a preview.
216
+ * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting
217
+ * takes an explicit `dryRun: false`.
218
+ *
219
+ * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete
220
+ * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown
221
+ * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and
222
+ * the requirement is explicit: a bad id among good ones must not discard the valid deletions.
223
+ * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*
224
+ * anything is destroyed rather than after.
225
+ *
226
+ * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper
227
+ * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report
228
+ * of everything already deleted — unrecoverable for an operation with no undo. The removals are
229
+ * driven one at a time here instead, through the same `upload.remove` service, which deletes the
230
+ * provider file and every generated format, emits `media.delete`, and then deletes the row.
231
+ *
232
+ * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch
233
+ * (100 ids max, per the input schema) is not worth firing at a provider in parallel.
234
+ */ const createMediaDeleteAssetsHandler = (strapi, context)=>{
235
+ assertAmbientInstance(strapi);
236
+ return async ({ args })=>{
237
+ const { ids, dryRun = true } = args;
238
+ // Model-level gate first, so a token without the action is refused before any DB read.
239
+ // The preview takes the same gate: it reveals which assets exist and what they are.
240
+ assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);
241
+ const deleted = [];
242
+ const failed = [];
243
+ // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the
244
+ // second occurrence of an already-deleted asset from being reported as a missing one.
245
+ for (const id of new Set(ids)){
246
+ let file;
247
+ try {
248
+ // Row-level check, shared with the admin controller: an owner-scoped permission
249
+ // condition is evaluated against the same subject the REST API would build. Run on the
250
+ // dry run too — a preview must not list an asset the executing call would refuse.
251
+ ({ file } = await findEntityAndCheckPermissions(context.userAbility, ACTIONS.update, FILE_MODEL_UID, id, strapi));
252
+ } catch (error) {
253
+ if (error instanceof errors.NotFoundError) {
254
+ failed.push({
255
+ id,
256
+ reason: MCP_DELETE_ASSETS_ID_NOT_FOUND
257
+ });
258
+ continue;
259
+ }
260
+ if (error instanceof errors.ForbiddenError) {
261
+ failed.push({
262
+ id,
263
+ reason: MCP_DELETE_ASSETS_ID_FORBIDDEN
264
+ });
265
+ continue;
266
+ }
267
+ failed.push({
268
+ id,
269
+ reason: MCP_DELETE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
270
+ });
271
+ continue;
272
+ }
273
+ // The preview stops here: the asset resolved and this token may delete it, which is
274
+ // everything the agent needs to confirm — reported in the same shape the real run uses.
275
+ if (dryRun) {
276
+ deleted.push(sanitizeMediaAsset(file));
277
+ continue;
278
+ }
279
+ try {
280
+ await getService('upload', strapi).remove(file);
281
+ // Reported from the row read before the delete: the asset no longer exists, so this is
282
+ // the only description of it the agent will ever get.
283
+ deleted.push(sanitizeMediaAsset(file));
284
+ } catch (error) {
285
+ /**
286
+ * A failed removal is reported against its own id rather than thrown, for the reason
287
+ * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing
288
+ * on the third id would discard the report saying the first two are gone. Here that
289
+ * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.
290
+ */ failed.push({
291
+ id,
292
+ reason: MCP_DELETE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error))
293
+ });
294
+ }
295
+ }
296
+ /**
297
+ * A request where nothing was deleted still returns the per-id report, on both branches:
298
+ * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.
299
+ * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose
300
+ * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.
301
+ */ return ok({
302
+ dryRun,
303
+ deleted,
304
+ failed,
305
+ totalFileNumber: deleted.length
306
+ });
307
+ };
308
+ };
309
+
310
+ export { createMediaDeleteAssetsHandler, createMediaMoveAssetsHandler, createMediaUpdateAssetHandler };
311
+ //# sourceMappingURL=write-handlers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-handlers.mjs","sources":["../../../../server/src/mcp/handlers/write-handlers.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\nimport { getService } from '../../utils';\nimport { ACTIONS, FILE_MODEL_UID, FOLDER_MODEL_UID } from '../../constants';\nimport { findEntityAndCheckPermissions } from '../../controllers/utils/find-entity-and-check-permissions';\nimport { assertAmbientInstance } from '../ambient-instance';\nimport { assertMediaPermission } from '../permissions';\nimport { sanitizeMediaAsset } from '../sanitizers/sanitize-media';\nimport {\n MCP_NOT_FOUND_ASSET,\n MCP_UPDATE_ASSET_NO_FIELDS,\n MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_NOT_FOUND,\n MCP_MOVE_ASSETS_ID_FORBIDDEN,\n MCP_MOVE_ASSETS_ID_FAILED,\n MCP_DELETE_ASSETS_ID_NOT_FOUND,\n MCP_DELETE_ASSETS_ID_FORBIDDEN,\n MCP_DELETE_ASSETS_ID_FAILED,\n} from './constants';\nimport { ok } from '../utils';\n\n// Type-level only: the MCP SDK validates `args` against the tool's strict Zod input schema\n// before the handler runs, so unknown keys never reach here.\ntype MediaUpdateAssetArgs = {\n id: number;\n name?: string;\n alternativeText?: string | null;\n caption?: string | null;\n};\n\ntype MediaMoveAssetsArgs = {\n ids: number[];\n folder: number | null;\n};\n\ntype MediaDeleteAssetsArgs = {\n ids: number[];\n dryRun?: boolean;\n};\n\n/** The metadata keys `media_update_asset` may write. Everything else is rejected by the schema. */\nconst WRITABLE_FIELDS = ['name', 'alternativeText', 'caption'] as const;\n\n/**\n * Picks the metadata the caller actually sent.\n *\n * `updateFileInfo` treats nil as \"keep the stored value\" (`_.isNil`), so an explicit\n * `alternativeText: null` cannot be forwarded as null — it would be read as \"unchanged\"\n * instead of \"clear it\". Clearing is expressed as an empty string, which is what the admin\n * panel writes when the field is emptied.\n */\nconst buildFileInfo = (args: MediaUpdateAssetArgs): Record<string, string> => {\n const fileInfo: Record<string, string> = {};\n\n for (const field of WRITABLE_FIELDS) {\n const value = args[field];\n\n if (value !== undefined) {\n fileInfo[field] = value === null ? '' : value;\n }\n }\n\n return fileInfo;\n};\n\n/**\n * `media_update_asset` — edits the writable metadata of one asset.\n *\n * Gated on `plugin::upload.assets.update` and mirrors `PUT /upload/files/:id`: the same\n * `findEntityAndCheckPermissions` row-level check, the same `updateFileInfo` service call, and\n * the same `updatedBy` attribution from the session user.\n *\n * The response carries the updated asset through the read sanitizer, so a client can confirm\n * the write without a second `media_get_asset` round-trip — and so provider fields stay\n * invisible on the write path too.\n */\nexport const createMediaUpdateAssetHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { id, ...metadata } = args as MediaUpdateAssetArgs;\n const fileInfo = buildFileInfo(metadata as MediaUpdateAssetArgs);\n\n // A patch with no writable field is a caller error, not a no-op success: the schema cannot\n // express \"at least one of\" without becoming a ZodEffects the registry can't publish.\n if (Object.keys(fileInfo).length === 0) {\n throw new errors.ValidationError(MCP_UPDATE_ASSET_NO_FIELDS);\n }\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // `findEntityAndCheckPermissions` only covers the row-level check.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n // Row-level check, shared with the admin controller: resolves the creator's roles so an\n // owner-scoped permission condition is evaluated against the same subject the REST API\n // would build, and throws Forbidden when a condition excludes this asset.\n //\n // Its NotFoundError carries no message, which would reach the agent as a bare \"Not Found\";\n // rethrowing adds the same wording the read tools use.\n try {\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n throw new errors.NotFoundError(MCP_NOT_FOUND_ASSET);\n }\n\n throw error;\n }\n\n const written = await getService('upload', strapi).updateFileInfo(id, fileInfo, {\n user: context.user,\n });\n\n const updated = await getService('upload', strapi).findOne(id, ['folder']);\n\n return ok({ data: sanitizeMediaAsset(updated ?? written) });\n };\n};\n\n/**\n * Resolves the destination folder for `media_move_assets`, and rejects one that does not exist.\n *\n * `null` is the media library root and always valid. A named folder is looked up so a typo — or a\n * folder id passed where the agent meant an asset — is refused before anything is moved, matching\n * the `folder-exists` test the admin bulk-move validation applies to `destinationFolderId`.\n *\n * A bad destination is the one failure that rejects the whole call rather than being reported per\n * id: it is a property of the request, not of any single asset, so every id would fail for the\n * same reason and a per-id report would say nothing the agent could act on.\n */\nconst resolveDestinationFolder = async (strapi: Core.Strapi, folder: number | null) => {\n if (folder === null) {\n return null;\n }\n\n const destination = await strapi.db.query(FOLDER_MODEL_UID).findOne({\n select: ['id', 'name'],\n where: { id: folder },\n });\n\n if (destination === null || destination === undefined) {\n throw new errors.ValidationError(MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND);\n }\n\n return { id: Number(destination.id), name: String(destination.name ?? '') };\n};\n\n/**\n * `media_move_assets` — moves assets between folders in bulk.\n *\n * Bulk with a per-id report, rather than all-or-nothing: an agent reorganising a library moves\n * many assets at once, and one bad id among good ones must not discard the valid moves. Each\n * asset is moved on its own, so a failure is confined to its own id and the response says exactly\n * which ids to retry.\n *\n * That is why this does not reuse the admin `/actions/bulk-move` controller path, whose single\n * transaction is all-or-nothing by construction (and which also handles folders, out of scope\n * here). It goes through `updateFileInfo` instead — the same service `media_update_asset` and\n * `PUT /upload/files/:id` use — which sets the `folder` relation, recomputes the private\n * `folderPath`, attributes `updatedBy`, and emits `media.update` per asset. Replicating the\n * controller's raw join-table writes would skip all four.\n *\n * The moves are sequential on purpose. Each one writes the join table and `folderPath` for its\n * asset, and a bounded reorganisation (100 ids max, per the input schema) is not worth the\n * connection-pool contention of firing them in parallel.\n */\nexport const createMediaMoveAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, folder } = args as MediaMoveAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const destinationFolder = await resolveDestinationFolder(strapi, folder);\n\n const moved: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id rather than\n // reporting the same asset twice for a request that moved it once.\n for (const id of new Set(ids)) {\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build.\n await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n );\n const updated = await getService('upload', strapi).updateFileInfo(\n id,\n // `updateFileInfo` reads `undefined` as \"keep the stored folder\" and null as the root,\n // so the destination is forwarded as-is.\n { folder },\n { user: context.user }\n );\n\n // `updateFileInfo` resolves the row it wrote without the `folder` relation populated, so\n // the destination is attached here instead of costing a read-back per asset.\n moved.push(sanitizeMediaAsset({ ...updated, folder: destinationFolder }));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_MOVE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n /**\n * Any other failure — a DB error, a provider fault — is reported against this id too,\n * rather than thrown.\n *\n * Throwing here would be the one way to lose committed work silently: a tool error\n * reaches the client as `{ content: [text], isError: true }` with NO\n * `structuredContent` (see `tool-registry.ts`), so an error on the third id would\n * discard the report saying the first two had already moved. The agent would be left\n * unable to tell which ids to retry — exactly the recoverability this tool exists to\n * provide.\n *\n * The message is carried through verbatim so the real fault is still legible, and the\n * loop continues: one broken asset must not strand the rest of a reorganisation.\n */\n failed.push({\n id,\n reason: MCP_MOVE_ASSETS_ID_FAILED(error instanceof Error ? error.message : String(error)),\n });\n }\n }\n\n /**\n * A request where nothing moved still returns the per-id report.\n *\n * `moved: []` alongside a populated `failed` is not ambiguous — it says plainly that nothing\n * moved and why, per id. Throwing instead would drop `structuredContent` entirely (a tool\n * error carries text only), so `ids: [999]` would get prose while `ids: [1, 999]` got a\n * machine-readable entry, for the same class of mistake.\n *\n * The only failure that still rejects the whole call is an invalid destination folder, which\n * is checked before the loop: nothing has moved, so there is no report to preserve.\n */\n return ok({ destinationFolder, moved, failed });\n };\n};\n\n/**\n * `media_delete_assets` — previews or performs the permanent deletion of assets, in bulk.\n *\n * Two branches behind one tool, for the same reason as `media_delete_folder`: the preview and the\n * deletion must agree on what would be destroyed, and splitting them across tools would both let\n * the two drift apart and let an agent reach the destructive one without ever seeing a preview.\n * `dryRun` defaults to true (see the input schema), so omitting the flag previews and deleting\n * takes an explicit `dryRun: false`.\n *\n * Per-id, not all-or-nothing — the opposite of `media_delete_folder`, on purpose. A folder delete\n * cascades, so a mixed id list there is refused outright rather than half-applied over an unknown\n * amount of content. Here each id is exactly one asset, the blast radius of a bad one is nil, and\n * the requirement is explicit: a bad id among good ones must not discard the valid deletions.\n * The dry run reports the same per-id split, so an agent sees which ids will not resolve *before*\n * anything is destroyed rather than after.\n *\n * This does not reuse `file.deleteByIds` (the admin `/actions/bulk-delete` path). That helper\n * fires `upload.remove` under a single `Promise.all`, so the first rejection discards the report\n * of everything already deleted — unrecoverable for an operation with no undo. The removals are\n * driven one at a time here instead, through the same `upload.remove` service, which deletes the\n * provider file and every generated format, emits `media.delete`, and then deletes the row.\n *\n * The deletions are sequential on purpose: each one performs provider I/O, and a bounded batch\n * (100 ids max, per the input schema) is not worth firing at a provider in parallel.\n */\nexport const createMediaDeleteAssetsHandler = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext\n) => {\n assertAmbientInstance(strapi);\n\n return async ({\n args,\n }: {\n args: Record<string, unknown>;\n }): Promise<Modules.MCP.McpToolHandlerReturn> => {\n const { ids, dryRun = true } = args as MediaDeleteAssetsArgs;\n\n // Model-level gate first, so a token without the action is refused before any DB read.\n // The preview takes the same gate: it reveals which assets exist and what they are.\n assertMediaPermission(strapi, context, ACTIONS.update, FILE_MODEL_UID);\n\n const deleted: ReturnType<typeof sanitizeMediaAsset>[] = [];\n const failed: { id: number; reason: string }[] = [];\n\n // `ids` can repeat an id; de-duplicating keeps the report one entry per id, and stops the\n // second occurrence of an already-deleted asset from being reported as a missing one.\n for (const id of new Set(ids)) {\n let file;\n\n try {\n // Row-level check, shared with the admin controller: an owner-scoped permission\n // condition is evaluated against the same subject the REST API would build. Run on the\n // dry run too — a preview must not list an asset the executing call would refuse.\n ({ file } = await findEntityAndCheckPermissions(\n context.userAbility,\n ACTIONS.update,\n FILE_MODEL_UID,\n id,\n strapi\n ));\n } catch (error) {\n if (error instanceof errors.NotFoundError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_NOT_FOUND });\n continue;\n }\n\n if (error instanceof errors.ForbiddenError) {\n failed.push({ id, reason: MCP_DELETE_ASSETS_ID_FORBIDDEN });\n continue;\n }\n\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n continue;\n }\n\n // The preview stops here: the asset resolved and this token may delete it, which is\n // everything the agent needs to confirm — reported in the same shape the real run uses.\n if (dryRun) {\n deleted.push(sanitizeMediaAsset(file));\n continue;\n }\n\n try {\n await getService('upload', strapi).remove(file);\n\n // Reported from the row read before the delete: the asset no longer exists, so this is\n // the only description of it the agent will ever get.\n deleted.push(sanitizeMediaAsset(file));\n } catch (error) {\n /**\n * A failed removal is reported against its own id rather than thrown, for the reason\n * spelled out on `media_move_assets` — a tool error carries no `structuredContent`, so throwing\n * on the third id would discard the report saying the first two are gone. Here that\n * matters more: the earlier deletions cannot be re-read, undone or discovered afterwards.\n */\n failed.push({\n id,\n reason: MCP_DELETE_ASSETS_ID_FAILED(\n error instanceof Error ? error.message : String(error)\n ),\n });\n }\n }\n\n /**\n * A request where nothing was deleted still returns the per-id report, on both branches:\n * `deleted: []` with every id in `failed` says plainly that nothing happened and why, per id.\n * Throwing instead would drop `structuredContent` entirely, so `ids: [999]` would get prose\n * where `ids: [1, 999]` gets a machine-readable entry, for the same class of mistake.\n */\n return ok({ dryRun, deleted, failed, totalFileNumber: deleted.length });\n };\n};\n"],"names":["WRITABLE_FIELDS","buildFileInfo","args","fileInfo","field","value","undefined","createMediaUpdateAssetHandler","strapi","context","assertAmbientInstance","id","metadata","Object","keys","length","errors","ValidationError","MCP_UPDATE_ASSET_NO_FIELDS","assertMediaPermission","ACTIONS","update","FILE_MODEL_UID","findEntityAndCheckPermissions","userAbility","error","NotFoundError","MCP_NOT_FOUND_ASSET","written","getService","updateFileInfo","user","updated","findOne","ok","data","sanitizeMediaAsset","resolveDestinationFolder","folder","destination","db","query","FOLDER_MODEL_UID","select","where","MCP_MOVE_ASSETS_DESTINATION_NOT_FOUND","Number","name","String","createMediaMoveAssetsHandler","ids","destinationFolder","moved","failed","Set","push","reason","MCP_MOVE_ASSETS_ID_NOT_FOUND","ForbiddenError","MCP_MOVE_ASSETS_ID_FORBIDDEN","MCP_MOVE_ASSETS_ID_FAILED","Error","message","createMediaDeleteAssetsHandler","dryRun","deleted","file","MCP_DELETE_ASSETS_ID_NOT_FOUND","MCP_DELETE_ASSETS_ID_FORBIDDEN","MCP_DELETE_ASSETS_ID_FAILED","remove","totalFileNumber"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ var utils = require('@strapi/utils');
4
+
5
+ /**
6
+ * Builds an admin permissions manager for a media model bound to the MCP session's ability.
7
+ *
8
+ * MCP tool handlers have no Koa context, so they cannot rely on route policies: each handler must
9
+ * re-check permissions itself, exactly as the admin controllers do via `ctx.state.userAbility`.
10
+ * The declarative `auth.policies` on a tool definition already gates registration and invocation;
11
+ * this second check is what keeps a handler safe if it is ever called from another entry point.
12
+ *
13
+ * It is also the only check bound to a model. `plugin::upload.read` is registered in the
14
+ * `plugins` section with no subject (see the upload plugin bootstrap), so a tool's policies
15
+ * carry an action only — a subject-less grant registers as CASL `subject: 'all'`. Passing a
16
+ * model UID as the policy subject is rejected by the admin-token validation, so the file /
17
+ * folder distinction is enforced here, where the permissions manager is bound to the UID.
18
+ */ const createMediaPermissionsManager = (strapi, context, action, model)=>strapi.service('admin::permission').createPermissionsManager({
19
+ ability: context.userAbility,
20
+ action,
21
+ model
22
+ });
23
+ /**
24
+ * Throws `ForbiddenError` unless the session's ability permits `action` on `model`.
25
+ * Mirrors the `if (!pm.isAllowed) return ctx.forbidden()` guard in the admin controllers.
26
+ */ const assertMediaPermission = (strapi, context, action, model)=>{
27
+ const pm = createMediaPermissionsManager(strapi, context, action, model);
28
+ if (!pm.isAllowed) {
29
+ throw new utils.errors.ForbiddenError();
30
+ }
31
+ return pm;
32
+ };
33
+
34
+ exports.assertMediaPermission = assertMediaPermission;
35
+ exports.createMediaPermissionsManager = createMediaPermissionsManager;
36
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sources":["../../../server/src/mcp/permissions.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\n/**\n * Builds an admin permissions manager for a media model bound to the MCP session's ability.\n *\n * MCP tool handlers have no Koa context, so they cannot rely on route policies: each handler must\n * re-check permissions itself, exactly as the admin controllers do via `ctx.state.userAbility`.\n * The declarative `auth.policies` on a tool definition already gates registration and invocation;\n * this second check is what keeps a handler safe if it is ever called from another entry point.\n *\n * It is also the only check bound to a model. `plugin::upload.read` is registered in the\n * `plugins` section with no subject (see the upload plugin bootstrap), so a tool's policies\n * carry an action only — a subject-less grant registers as CASL `subject: 'all'`. Passing a\n * model UID as the policy subject is rejected by the admin-token validation, so the file /\n * folder distinction is enforced here, where the permissions manager is bound to the UID.\n */\nexport const createMediaPermissionsManager = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext,\n action: string,\n model: string\n) =>\n strapi.service('admin::permission').createPermissionsManager({\n ability: context.userAbility,\n action,\n model,\n });\n\n/**\n * Throws `ForbiddenError` unless the session's ability permits `action` on `model`.\n * Mirrors the `if (!pm.isAllowed) return ctx.forbidden()` guard in the admin controllers.\n */\nexport const assertMediaPermission = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext,\n action: string,\n model: string\n) => {\n const pm = createMediaPermissionsManager(strapi, context, action, model);\n\n if (!pm.isAllowed) {\n throw new errors.ForbiddenError();\n }\n\n return pm;\n};\n"],"names":["createMediaPermissionsManager","strapi","context","action","model","service","createPermissionsManager","ability","userAbility","assertMediaPermission","pm","isAllowed","errors","ForbiddenError"],"mappings":";;;;AAGA;;;;;;;;;;;;;AAaC,IACM,MAAMA,6BAAAA,GAAgC,CAC3CC,MAAAA,EACAC,OAAAA,EACAC,MAAAA,EACAC,KAAAA,GAEAH,MAAAA,CAAOI,OAAO,CAAC,mBAAA,CAAA,CAAqBC,wBAAwB,CAAC;AAC3DC,QAAAA,OAAAA,EAASL,QAAQM,WAAW;AAC5BL,QAAAA,MAAAA;AACAC,QAAAA;KACF;AAEF;;;AAGC,IACM,MAAMK,qBAAAA,GAAwB,CACnCR,MAAAA,EACAC,SACAC,MAAAA,EACAC,KAAAA,GAAAA;AAEA,IAAA,MAAMM,EAAAA,GAAKV,6BAAAA,CAA8BC,MAAAA,EAAQC,OAAAA,EAASC,MAAAA,EAAQC,KAAAA,CAAAA;IAElE,IAAI,CAACM,EAAAA,CAAGC,SAAS,EAAE;QACjB,MAAM,IAAIC,aAAOC,cAAc,EAAA;AACjC,IAAA;IAEA,OAAOH,EAAAA;AACT;;;;;"}
@@ -0,0 +1,33 @@
1
+ import { errors } from '@strapi/utils';
2
+
3
+ /**
4
+ * Builds an admin permissions manager for a media model bound to the MCP session's ability.
5
+ *
6
+ * MCP tool handlers have no Koa context, so they cannot rely on route policies: each handler must
7
+ * re-check permissions itself, exactly as the admin controllers do via `ctx.state.userAbility`.
8
+ * The declarative `auth.policies` on a tool definition already gates registration and invocation;
9
+ * this second check is what keeps a handler safe if it is ever called from another entry point.
10
+ *
11
+ * It is also the only check bound to a model. `plugin::upload.read` is registered in the
12
+ * `plugins` section with no subject (see the upload plugin bootstrap), so a tool's policies
13
+ * carry an action only — a subject-less grant registers as CASL `subject: 'all'`. Passing a
14
+ * model UID as the policy subject is rejected by the admin-token validation, so the file /
15
+ * folder distinction is enforced here, where the permissions manager is bound to the UID.
16
+ */ const createMediaPermissionsManager = (strapi, context, action, model)=>strapi.service('admin::permission').createPermissionsManager({
17
+ ability: context.userAbility,
18
+ action,
19
+ model
20
+ });
21
+ /**
22
+ * Throws `ForbiddenError` unless the session's ability permits `action` on `model`.
23
+ * Mirrors the `if (!pm.isAllowed) return ctx.forbidden()` guard in the admin controllers.
24
+ */ const assertMediaPermission = (strapi, context, action, model)=>{
25
+ const pm = createMediaPermissionsManager(strapi, context, action, model);
26
+ if (!pm.isAllowed) {
27
+ throw new errors.ForbiddenError();
28
+ }
29
+ return pm;
30
+ };
31
+
32
+ export { assertMediaPermission, createMediaPermissionsManager };
33
+ //# sourceMappingURL=permissions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.mjs","sources":["../../../server/src/mcp/permissions.ts"],"sourcesContent":["import { errors } from '@strapi/utils';\nimport type { Core, Modules } from '@strapi/types';\n\n/**\n * Builds an admin permissions manager for a media model bound to the MCP session's ability.\n *\n * MCP tool handlers have no Koa context, so they cannot rely on route policies: each handler must\n * re-check permissions itself, exactly as the admin controllers do via `ctx.state.userAbility`.\n * The declarative `auth.policies` on a tool definition already gates registration and invocation;\n * this second check is what keeps a handler safe if it is ever called from another entry point.\n *\n * It is also the only check bound to a model. `plugin::upload.read` is registered in the\n * `plugins` section with no subject (see the upload plugin bootstrap), so a tool's policies\n * carry an action only — a subject-less grant registers as CASL `subject: 'all'`. Passing a\n * model UID as the policy subject is rejected by the admin-token validation, so the file /\n * folder distinction is enforced here, where the permissions manager is bound to the UID.\n */\nexport const createMediaPermissionsManager = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext,\n action: string,\n model: string\n) =>\n strapi.service('admin::permission').createPermissionsManager({\n ability: context.userAbility,\n action,\n model,\n });\n\n/**\n * Throws `ForbiddenError` unless the session's ability permits `action` on `model`.\n * Mirrors the `if (!pm.isAllowed) return ctx.forbidden()` guard in the admin controllers.\n */\nexport const assertMediaPermission = (\n strapi: Core.Strapi,\n context: Modules.MCP.McpHandlerContext,\n action: string,\n model: string\n) => {\n const pm = createMediaPermissionsManager(strapi, context, action, model);\n\n if (!pm.isAllowed) {\n throw new errors.ForbiddenError();\n }\n\n return pm;\n};\n"],"names":["createMediaPermissionsManager","strapi","context","action","model","service","createPermissionsManager","ability","userAbility","assertMediaPermission","pm","isAllowed","errors","ForbiddenError"],"mappings":";;AAGA;;;;;;;;;;;;;AAaC,IACM,MAAMA,6BAAAA,GAAgC,CAC3CC,MAAAA,EACAC,OAAAA,EACAC,MAAAA,EACAC,KAAAA,GAEAH,MAAAA,CAAOI,OAAO,CAAC,mBAAA,CAAA,CAAqBC,wBAAwB,CAAC;AAC3DC,QAAAA,OAAAA,EAASL,QAAQM,WAAW;AAC5BL,QAAAA,MAAAA;AACAC,QAAAA;KACF;AAEF;;;AAGC,IACM,MAAMK,qBAAAA,GAAwB,CACnCR,MAAAA,EACAC,SACAC,MAAAA,EACAC,KAAAA,GAAAA;AAEA,IAAA,MAAMM,EAAAA,GAAKV,6BAAAA,CAA8BC,MAAAA,EAAQC,OAAAA,EAASC,MAAAA,EAAQC,KAAAA,CAAAA;IAElE,IAAI,CAACM,EAAAA,CAAGC,SAAS,EAAE;QACjB,MAAM,IAAIC,OAAOC,cAAc,EAAA;AACjC,IAAA;IAEA,OAAOH,EAAAA;AACT;;;;"}