@abinnovision/payloadcms-mcpx 2.0.0-beta.9 → 2.0.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 (76) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +56 -36
  3. package/dist/admin/capability-matrix.d.mts +5 -3
  4. package/dist/admin/capability-matrix.mjs +374 -272
  5. package/dist/admin/capability-toggles.mjs +79 -42
  6. package/dist/admin/confirmations-panel.mjs +232 -0
  7. package/dist/admin/confirmations.d.mts +15 -0
  8. package/dist/admin/confirmations.mjs +71 -0
  9. package/dist/admin/endpoint-url.mjs +7 -0
  10. package/dist/admin/index.d.mts +2 -1
  11. package/dist/admin/index.mjs +2 -1
  12. package/dist/admin/setup-guide.d.mts +3 -3
  13. package/dist/admin/setup-guide.mjs +51 -33
  14. package/dist/api-keys/capability-matrix.d.mts +7 -9
  15. package/dist/api-keys/capability-matrix.mjs +15 -37
  16. package/dist/api-keys/collection.mjs +16 -18
  17. package/dist/api-keys/confirmation-view.mjs +7 -0
  18. package/dist/api-keys/fields.mjs +50 -57
  19. package/dist/auth/resolve.mjs +69 -40
  20. package/dist/capabilities.d.mts +2 -0
  21. package/dist/capabilities.mjs +64 -16
  22. package/dist/define-tool.d.mts +4 -4
  23. package/dist/define-tool.mjs +8 -1
  24. package/dist/endpoint/confirmations.mjs +124 -0
  25. package/dist/endpoint/handler.mjs +25 -27
  26. package/dist/endpoint/http.mjs +27 -0
  27. package/dist/endpoint/server.mjs +48 -29
  28. package/dist/endpoint/upload.mjs +104 -0
  29. package/dist/grants/confirmation.mjs +75 -0
  30. package/dist/grants/grant.mjs +137 -0
  31. package/dist/guards.mjs +8 -1
  32. package/dist/index.d.mts +2 -2
  33. package/dist/index.mjs +1 -1
  34. package/dist/options.mjs +68 -61
  35. package/dist/plugin.d.mts +3 -2
  36. package/dist/plugin.mjs +19 -4
  37. package/dist/request.mjs +5 -1
  38. package/dist/result.mjs +11 -1
  39. package/dist/schema/admin-hidden.mjs +4 -4
  40. package/dist/schema/describe.mjs +12 -18
  41. package/dist/schema/index.mjs +5 -6
  42. package/dist/schema/lexical-pointer.mjs +8 -8
  43. package/dist/schema/lexical.mjs +15 -13
  44. package/dist/schema/outline.mjs +4 -5
  45. package/dist/schema/path.mjs +4 -1
  46. package/dist/schema/pointer.mjs +11 -17
  47. package/dist/schema/shape.mjs +38 -50
  48. package/dist/schema/walk.mjs +57 -18
  49. package/dist/tools/builtin.mjs +7 -1
  50. package/dist/tools/create-document.mjs +38 -26
  51. package/dist/tools/delete-document.mjs +154 -0
  52. package/dist/tools/describe-schema.mjs +7 -5
  53. package/dist/tools/document.mjs +131 -45
  54. package/dist/tools/duplicate-document.mjs +50 -0
  55. package/dist/tools/find-documents.mjs +15 -20
  56. package/dist/tools/find-versions.mjs +11 -15
  57. package/dist/tools/get-document.mjs +31 -15
  58. package/dist/tools/links.mjs +53 -0
  59. package/dist/tools/list-capabilities.mjs +17 -7
  60. package/dist/tools/patch-document.mjs +57 -53
  61. package/dist/tools/publish-document.mjs +17 -29
  62. package/dist/tools/query-paths.mjs +1 -1
  63. package/dist/tools/run-confirmed.mjs +114 -0
  64. package/dist/tools/shared.mjs +53 -46
  65. package/dist/tools/validate-document.mjs +6 -8
  66. package/dist/tools/versions.mjs +11 -6
  67. package/dist/types.d.mts +158 -43
  68. package/dist/upload/file.mjs +177 -0
  69. package/dist/version.mjs +4 -2
  70. package/dist/write/draft-guard.mjs +35 -28
  71. package/dist/write/patch.mjs +4 -4
  72. package/dist/write/publish-intent.mjs +23 -11
  73. package/dist/write/row-ids.mjs +3 -1
  74. package/dist/write/write-data.mjs +18 -31
  75. package/package.json +14 -14
  76. package/dist/builtin-tool-names.mjs +0 -16
@@ -0,0 +1,154 @@
1
+ import { defineMcpxTool } from "../define-tool.mjs";
2
+ import { errorResult, jsonResult } from "../result.mjs";
3
+ import { translateStatic } from "../i18n.mjs";
4
+ import { expectedUpdatedAtShape, idSchema, slugsFor, stringEnum } from "./shared.mjs";
5
+ import { readDraft, resolveCollection, staleReadResult } from "./document.mjs";
6
+ import { withTrashIntent } from "../write/publish-intent.mjs";
7
+ import { withTransaction } from "../write/transaction.mjs";
8
+ import { z } from "zod";
9
+ import { Forbidden, combineQueries, executeAccess } from "payload";
10
+ import { formatAdminURL } from "payload/shared";
11
+ //#region src/tools/delete-document.ts
12
+ const DESCRIPTION = `Asks to delete one document. This call deletes nothing: it returns a "confirmation" with an "id" and a "url". Give the url to the user, who approves or rejects in the admin panel, then pass the id to runConfirmed, which deletes. Refused when the document is unknown, already in trash, changed since "expectedUpdatedAt" or not deletable by the user.`;
13
+ const permanenceSentence = (scope) => {
14
+ const permanent = slugsFor(scope, "delete").collections.filter((slug) => scope.req.payload.collections[slug]?.config.trash !== true);
15
+ if (permanent.length === 0) return " Documents move to trash.";
16
+ const sentence = ` Deletes in ${permanent.join(", ")} are permanent.`;
17
+ return permanent.length === slugsFor(scope, "delete").collections.length ? sentence : `${sentence} Other documents move to trash.`;
18
+ };
19
+ const unattendedSentence = (scope) => {
20
+ const unattended = slugsFor(scope, "delete").collections.filter((slug) => scope.capabilities.collections[slug]?.deleteUnattended === true);
21
+ return unattended.length === 0 ? "" : ` Exception: in ${unattended.join(", ")} this call moves the document to trash at once and returns the delete result, with no confirmation.`;
22
+ };
23
+ const mayDelete = async (req, target) => {
24
+ const { config } = target;
25
+ const data = config.trash ? { deletedAt: (/* @__PURE__ */ new Date()).toISOString() } : {};
26
+ const rules = config.trash ? [config.access.delete, config.access.update] : [config.access.delete];
27
+ let where = { id: { equals: target.id } };
28
+ for (const rule of rules) {
29
+ const result = await executeAccess({
30
+ id: target.id,
31
+ data,
32
+ disableErrors: true,
33
+ req
34
+ }, rule);
35
+ if (result === false) return false;
36
+ if (typeof result === "object") where = combineQueries(where, result);
37
+ }
38
+ const { totalDocs } = await req.payload.count({
39
+ collection: target.slug,
40
+ where,
41
+ overrideAccess: true,
42
+ trash: true,
43
+ req
44
+ });
45
+ return totalDocs > 0;
46
+ };
47
+ /**
48
+ * Deletes one collection document once the key's user approved the call. The
49
+ * first call only checks and stores it; `runConfirmed` runs it again with the
50
+ * stored arguments, pinned to the `updatedAt` read here. Available where the
51
+ * config exposes `delete` and the key has the `delete` checkbox.
52
+ */ const deleteDocument = defineMcpxTool({
53
+ name: "deleteDocument",
54
+ description: (scope) => `${DESCRIPTION}${permanenceSentence(scope)}${unattendedSentence(scope)}`,
55
+ annotations: {
56
+ destructiveHint: true,
57
+ openWorldHint: false
58
+ },
59
+ isEnabled: (scope) => scope.uploads && scope.collections.deletable.length > 0,
60
+ inputSchema: (scope) => ({
61
+ collection: stringEnum(scope.collections.deletable),
62
+ id: idSchema,
63
+ ...expectedUpdatedAtShape,
64
+ reason: z.string().max(500).optional().describe("Why, shown to the user who approves.")
65
+ }),
66
+ confirm: {
67
+ label: "Delete documents",
68
+ summarize: async ({ args }, req) => {
69
+ const slug = String(args["collection"]);
70
+ const id = args["id"];
71
+ const config = req.payload.collections[slug]?.config;
72
+ const titleField = config?.admin.useAsTitle;
73
+ const read = async (draft) => config ? await req.payload.findByID({
74
+ collection: slug,
75
+ id,
76
+ depth: 0,
77
+ draft,
78
+ overrideAccess: false,
79
+ disableErrors: true,
80
+ req
81
+ }) : null;
82
+ const titleOf = (doc) => {
83
+ const value = titleField === void 0 ? void 0 : doc?.[titleField];
84
+ return typeof value === "string" && value !== "" ? value : void 0;
85
+ };
86
+ const published = await read(false);
87
+ const live = titleOf(published);
88
+ const latest = titleOf(await read(true));
89
+ const title = live === void 0 ? latest : latest !== void 0 && latest !== live ? `${live} (draft: ${latest})` : live;
90
+ const status = published?.["_status"];
91
+ return {
92
+ label: (typeof config?.labels.singular === "function" ? void 0 : translateStatic(config?.labels.singular, req.i18n)) ?? slug,
93
+ ...title === void 0 ? {} : { title },
94
+ ...typeof status === "string" ? { status } : {},
95
+ id: String(id),
96
+ href: formatAdminURL({
97
+ adminRoute: req.payload.config.routes.admin,
98
+ path: `/collections/${slug}/${String(id)}`
99
+ }),
100
+ permanent: config?.trash !== true,
101
+ ...typeof args["reason"] === "string" ? { reason: args["reason"] } : {}
102
+ };
103
+ }
104
+ },
105
+ handler: async ({ args, scope, confirmation }) => {
106
+ const target = {
107
+ ...resolveCollection(scope, args.collection, "delete"),
108
+ id: args.id
109
+ };
110
+ const { req } = scope;
111
+ const { payload } = req;
112
+ const trash = target.config.trash ?? false;
113
+ return await withTransaction(req, async () => {
114
+ const doc = await readDraft(scope, {
115
+ target,
116
+ locale: void 0,
117
+ trash: true
118
+ });
119
+ if (doc["deletedAt"]) return errorResult("The document is already in the trash. Emptying the trash is not available.");
120
+ const stale = staleReadResult(doc, args.expectedUpdatedAt, "The document changed since you read it. Read it again before deleting.");
121
+ if (stale) return stale;
122
+ if (!await mayDelete(req, target)) throw new Forbidden(req.t);
123
+ const unattended = trash && scope.capabilities.collections[target.slug]?.deleteUnattended === true;
124
+ if (!confirmation.confirmed && !unattended) return await confirmation.request({
125
+ ...args,
126
+ expectedUpdatedAt: new Date(doc["updatedAt"]).toISOString()
127
+ });
128
+ if (trash) await payload.update({
129
+ collection: target.slug,
130
+ id: target.id,
131
+ data: withTrashIntent({ deletedAt: (/* @__PURE__ */ new Date()).toISOString() }),
132
+ depth: 0,
133
+ overrideAccess: false,
134
+ overrideLock: false,
135
+ req
136
+ });
137
+ else await payload.delete({
138
+ collection: target.slug,
139
+ id: target.id,
140
+ depth: 0,
141
+ overrideAccess: false,
142
+ overrideLock: false,
143
+ req
144
+ });
145
+ return jsonResult({
146
+ collection: target.slug,
147
+ id: target.id,
148
+ movedToTrash: trash
149
+ });
150
+ });
151
+ }
152
+ });
153
+ //#endregion
154
+ export { deleteDocument };
@@ -1,11 +1,11 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
2
  import { jsonResult } from "../result.mjs";
3
+ import { SchemaError } from "../schema/errors.mjs";
3
4
  import { nodePropertiesFor } from "../schema/lexical.mjs";
4
5
  import { translatorFor } from "../i18n.mjs";
5
- import { SchemaError } from "../schema/errors.mjs";
6
6
  import { nodeDescriber, reachableSchemaPaths } from "../schema/describe.mjs";
7
7
  import "../schema/index.mjs";
8
- import { entityShape } from "./shared.mjs";
8
+ import { entityShape, reaches } from "./shared.mjs";
9
9
  import { resolveEntity } from "./document.mjs";
10
10
  import { z } from "zod";
11
11
  //#region src/tools/describe-schema.ts
@@ -30,7 +30,7 @@ const PATHS_LIMIT = 400;
30
30
  readOnlyHint: true,
31
31
  openWorldHint: false
32
32
  },
33
- isEnabled: (scope) => scope.collections.readable.length + scope.globals.readable.length > 0,
33
+ isEnabled: (scope) => reaches(scope, "read"),
34
34
  inputSchema: (scope) => ({
35
35
  ...entityShape(scope, "read", "Instead of \"collection\"."),
36
36
  paths: z.array(z.string()).max(PATHS_LIMIT).optional().describe("Schema paths from \"next\". Omit for the top-level node."),
@@ -43,9 +43,12 @@ const PATHS_LIMIT = 400;
43
43
  const expanded = args.expand === true ? reachableSchemaPaths(config, ref) : void 0;
44
44
  const requested = expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""]);
45
45
  const failures = [];
46
+ const nodeTypes = [];
46
47
  const nodes = requested.map((schemaPath) => {
47
48
  try {
48
- return describeNode(config, ref, schemaPath);
49
+ const node = describeNode(config, ref, schemaPath);
50
+ nodeTypes.push(...node.fields.flatMap((field) => field.nodes ?? []));
51
+ return node;
49
52
  } catch (error) {
50
53
  if (error instanceof SchemaError) return {
51
54
  error: error.message,
@@ -62,7 +65,6 @@ const PATHS_LIMIT = 400;
62
65
  err: failures[0],
63
66
  msg: `[payloadcms-mcpx] Describing ${String(failures.length)} schema paths failed.`
64
67
  });
65
- const nodeTypes = nodes.flatMap((node) => (node.fields ?? []).flatMap((field) => field.nodes ?? []));
66
68
  if (nodeTypes.length > 0) nodes.push({ nodeProperties: nodePropertiesFor(nodeTypes) });
67
69
  if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
68
70
  return jsonResult(nodes);
@@ -1,34 +1,44 @@
1
- import { errorResult } from "../result.mjs";
2
- import { slugsFor } from "./shared.mjs";
1
+ import { definedProps } from "../guards.mjs";
2
+ import { errorResult, jsonResult } from "../result.mjs";
3
+ import { documentLinks } from "./links.mjs";
3
4
  import { collectPublishBlockers } from "../write/publish-blockers.mjs";
5
+ import { slugsFor } from "./shared.mjs";
4
6
  import { APIError, Forbidden, NotFound } from "payload";
5
7
  //#region src/tools/document.ts
6
8
  /**
9
+ * `payload.globals` is `{ config: SanitizedGlobalConfig[] }`, an array, not the
10
+ * slug-keyed map `payload.collections` is.
11
+ */ const globalConfig = (payload, slug) => payload.globals.config.find((candidate) => candidate.slug === slug);
12
+ /**
13
+ * For a tool that takes a collection and no global, so the result needs no
14
+ * check for one.
15
+ */ const resolveCollection = (scope, slug, operation) => {
16
+ const found = scope.req.payload.collections[slug]?.config;
17
+ if (!slugsFor(scope, operation).collections.includes(slug) || !found) throw new Forbidden(scope.req.t);
18
+ return {
19
+ kind: "collection",
20
+ slug,
21
+ config: found
22
+ };
23
+ };
24
+ /**
7
25
  * A raw input shape leaves no top-level `.refine` to express "exactly one of
8
26
  * collection and global", so the rule is enforced here, with a message naming
9
27
  * the offending arguments.
10
28
  */ const resolveEntity = (scope, args, operation) => {
11
29
  const { collection, global } = args;
12
- const allowedSlugs = slugsFor(scope, operation);
13
30
  if (collection !== void 0 && global !== void 0) throw new APIError("Pass either \"collection\" or \"global\", not both.", 400);
14
- if (collection === void 0 && global === void 0) throw new APIError("One of \"collection\" or \"global\" is required. Call listCapabilities to see which slugs are available.", 400);
15
- if (collection !== void 0) {
16
- const found = scope.req.payload.collections[collection];
17
- if (!allowedSlugs.collections.includes(collection) || !found) throw new Forbidden(scope.req.t);
31
+ if (collection !== void 0) return resolveCollection(scope, collection, operation);
32
+ if (global !== void 0) {
33
+ const found = globalConfig(scope.req.payload, global);
34
+ if (!slugsFor(scope, operation).globals.includes(global) || !found) throw new Forbidden(scope.req.t);
18
35
  return {
19
- kind: "collection",
20
- slug: collection,
21
- config: found.config
36
+ kind: "global",
37
+ slug: global,
38
+ config: found
22
39
  };
23
40
  }
24
- const slug = global;
25
- const found = scope.req.payload.globals.config.find((candidate) => candidate.slug === slug);
26
- if (!allowedSlugs.globals.includes(slug) || !found) throw new Forbidden(scope.req.t);
27
- return {
28
- kind: "global",
29
- slug,
30
- config: found
31
- };
41
+ throw new APIError("One of \"collection\" or \"global\" is required. Call listCapabilities to see which slugs are available.", 400);
32
42
  };
33
43
  /**
34
44
  * Resolves the entity and checks `id` against it. A collection document needs
@@ -48,7 +58,45 @@ import { APIError, Forbidden, NotFound } from "payload";
48
58
  };
49
59
  /**
50
60
  * What a response names its subject by: a document's id, or a global's slug.
51
- */ const identityOf = (target, id) => target.kind === "collection" ? { id } : { global: target.slug };
61
+ */ const identityOf = (target) => target.kind === "collection" ? { id: target.id } : { global: target.slug };
62
+ /**
63
+ * The fields every write result starts with, read from the saved `doc`.
64
+ */ const documentSummary = (target, doc) => ({
65
+ ...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
66
+ status: doc["_status"],
67
+ updatedAt: doc["updatedAt"]
68
+ });
69
+ /**
70
+ * Names the blockers of a check, and whether the check itself failed.
71
+ */ const blockerFields = (result, key) => ({
72
+ ...result.blockers.length > 0 ? { [key]: result.blockers } : {},
73
+ ...result.unavailable ? { [`${key}Unavailable`]: true } : {}
74
+ });
75
+ /**
76
+ * What a tool that wrote a document returns: the document, re-read privileged
77
+ * in `locale`, with its links and publish blockers. `extra` follows them.
78
+ */ const writtenResult = async (scope, args) => {
79
+ const { target, locale } = args;
80
+ const saved = await readDraft(scope, {
81
+ target,
82
+ locale,
83
+ privileged: true
84
+ });
85
+ const validation = await collectPublishBlockers(scope.req, {
86
+ doc: saved,
87
+ entity: target
88
+ });
89
+ return jsonResult({
90
+ ...documentSummary(target, saved),
91
+ ...await documentLinks(scope.req, {
92
+ target,
93
+ doc: saved,
94
+ locale
95
+ }),
96
+ ...blockerFields(validation, "publishBlockers"),
97
+ ...args.extra
98
+ });
99
+ };
52
100
  const sameInstant = (left, right) => (typeof left === "string" || left instanceof Date) && new Date(left).getTime() === new Date(right).getTime();
53
101
  /**
54
102
  * Refuses a write when the document changed since the client read it.
@@ -75,7 +123,8 @@ const sameInstant = (left, right) => (typeof left === "string" || left instanceo
75
123
  ...shared,
76
124
  collection: args.target.slug,
77
125
  id: args.target.id,
78
- disableErrors: true
126
+ disableErrors: true,
127
+ ...args.trash ? { trash: true } : {}
79
128
  });
80
129
  if (!doc) throw new NotFound(scope.req.t);
81
130
  return doc;
@@ -86,39 +135,76 @@ const sameInstant = (left, right) => (typeof left === "string" || left instanceo
86
135
  });
87
136
  };
88
137
  /**
138
+ * Updates a collection document or a global with no fallback locale, so a value
139
+ * missing in the written locale is not backfilled from another and persisted.
140
+ */ const updateTarget = async (scope, target, write) => {
141
+ const { payload } = scope.req;
142
+ const { data, draft, file, ...optional } = write;
143
+ const shared = {
144
+ data,
145
+ depth: 0,
146
+ draft,
147
+ fallbackLocale: false,
148
+ overrideAccess: false,
149
+ req: scope.req,
150
+ ...definedProps(optional)
151
+ };
152
+ if (target.kind === "collection") await payload.update({
153
+ ...shared,
154
+ ...definedProps({ file }),
155
+ collection: target.slug,
156
+ id: target.id
157
+ });
158
+ else await payload.updateGlobal({
159
+ ...shared,
160
+ slug: target.slug
161
+ });
162
+ };
163
+ /**
164
+ * Runs `run`, then puts back the request's locale and fallback locale, which
165
+ * Payload's local API assigns in place.
166
+ */ const withRequestLocale = async (req, run) => {
167
+ const { fallbackLocale, locale } = req;
168
+ try {
169
+ return await run();
170
+ } finally {
171
+ if (locale !== void 0) req.locale = locale;
172
+ if (fallbackLocale !== void 0) req.fallbackLocale = fallbackLocale;
173
+ }
174
+ };
175
+ /**
89
176
  * {@link collectPublishBlockers} over the draft of each locale, each blocker
90
177
  * tagged with its locale. Locales run in turn because hooks share `req`, and
91
178
  * the request's locale and fallback locale are restored afterwards.
92
179
  */ const collectLocaleBlockers = async (scope, target, locales) => {
93
180
  const { req } = scope;
94
- const { fallbackLocale, locale: requestLocale } = req;
95
181
  const blockers = [];
96
- let unavailable = false;
97
- for (const locale of locales) try {
98
- const doc = await readDraft(scope, {
99
- target,
100
- locale,
101
- privileged: true
102
- });
103
- const result = await collectPublishBlockers(req, {
104
- doc,
105
- entity: target
106
- });
107
- unavailable ||= result.unavailable === true;
108
- blockers.push(...result.blockers.map((entry) => ({
109
- ...entry,
110
- locale
111
- })));
112
- } catch (error) {
113
- req.payload.logger.warn(`[payloadcms-mcpx] Could not read the ${target.slug} draft in ${locale}: ${error instanceof Error ? error.message : "unknown error"}`);
114
- unavailable = true;
115
- }
116
- if (requestLocale !== void 0) req.locale = requestLocale;
117
- if (fallbackLocale !== void 0) req.fallbackLocale = fallbackLocale;
118
182
  return {
119
183
  blockers,
120
- ...unavailable ? { unavailable: true } : {}
184
+ ...await withRequestLocale(req, async () => {
185
+ let failed = false;
186
+ for (const locale of locales) try {
187
+ const doc = await readDraft(scope, {
188
+ target,
189
+ locale,
190
+ privileged: true
191
+ });
192
+ const result = await collectPublishBlockers(req, {
193
+ doc,
194
+ entity: target
195
+ });
196
+ failed ||= result.unavailable === true;
197
+ blockers.push(...result.blockers.map((entry) => ({
198
+ ...entry,
199
+ locale
200
+ })));
201
+ } catch (error) {
202
+ req.payload.logger.warn(`[payloadcms-mcpx] Could not read the ${target.slug} draft in ${locale}: ${error instanceof Error ? error.message : "unknown error"}`);
203
+ failed = true;
204
+ }
205
+ return failed;
206
+ }) ? { unavailable: true } : {}
121
207
  };
122
208
  };
123
209
  //#endregion
124
- export { collectLocaleBlockers, identityOf, readDraft, resolveDocument, resolveEntity, staleReadResult };
210
+ export { blockerFields, collectLocaleBlockers, documentSummary, globalConfig, identityOf, readDraft, resolveCollection, resolveDocument, resolveEntity, staleReadResult, updateTarget, withRequestLocale, writtenResult };
@@ -0,0 +1,50 @@
1
+ import { defineMcpxTool } from "../define-tool.mjs";
2
+ import { idSchema, liveWriteSentence, localeOf, slugsFor, stringEnum } from "./shared.mjs";
3
+ import { resolveCollection, writtenResult } from "./document.mjs";
4
+ import { withDuplicateIntent } from "../write/publish-intent.mjs";
5
+ import { Forbidden } from "payload";
6
+ //#region src/tools/duplicate-document.ts
7
+ const duplicableSlugs = (scope) => slugsFor(scope, "create").collections.filter((slug) => scope.req.payload.collections[slug]?.config.disableDuplicate !== true && !scope.exposure.collections.some((entity) => entity.slug === slug && entity.isUpload));
8
+ const DESCRIPTION = (scope) => `Copies a document into a new one in the same collection. Use it instead of getDocument and createDocument to copy a document. It copies the latest version, the draft if there is one, in every locale. Unique text fields get " - Copy" appended. Returns the same fields as createDocument. To rename the copy, call patchDocument with the returned "updatedAt".
9
+
10
+ ${liveWriteSentence(scope, "create")}`;
11
+ /**
12
+ * Wraps Payload's duplicate, which checks read access on the source and create
13
+ * access on the copy, and runs the fields' `beforeDuplicate` hooks.
14
+ */ const duplicateDocument = defineMcpxTool({
15
+ name: "duplicateDocument",
16
+ description: DESCRIPTION,
17
+ annotations: {
18
+ readOnlyHint: false,
19
+ destructiveHint: false,
20
+ idempotentHint: false,
21
+ openWorldHint: false
22
+ },
23
+ isEnabled: (scope) => duplicableSlugs(scope).length > 0,
24
+ inputSchema: (scope) => ({
25
+ collection: stringEnum(duplicableSlugs(scope)),
26
+ id: idSchema
27
+ }),
28
+ handler: async ({ args, scope }) => {
29
+ const target = resolveCollection(scope, args.collection, "create");
30
+ if (!duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
31
+ const created = await scope.req.payload.duplicate({
32
+ collection: target.slug,
33
+ id: args.id,
34
+ data: withDuplicateIntent({}),
35
+ depth: 0,
36
+ draft: true,
37
+ overrideAccess: false,
38
+ req: scope.req
39
+ });
40
+ return await writtenResult(scope, {
41
+ target: {
42
+ ...target,
43
+ id: created["id"]
44
+ },
45
+ locale: localeOf(scope, void 0)
46
+ });
47
+ }
48
+ });
49
+ //#endregion
50
+ export { duplicateDocument };
@@ -1,9 +1,10 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
+ import { definedProps } from "../guards.mjs";
2
3
  import { jsonResult } from "../result.mjs";
3
4
  import { stripAdminHidden } from "../schema/admin-hidden.mjs";
4
5
  import "../schema/index.mjs";
5
- import { READ_LOCALE_DESCRIPTION, depthShape, localeOf, localeShape, slugEnum } from "./shared.mjs";
6
- import { resolveEntity } from "./document.mjs";
6
+ import { READ_LOCALE_DESCRIPTION, depthShape, localeOf, localeShape, pageFields, pageShape, stringEnum } from "./shared.mjs";
7
+ import { resolveCollection } from "./document.mjs";
7
8
  import { assertQueryable } from "./query-paths.mjs";
8
9
  import { mcpxReadRequest } from "./read-request.mjs";
9
10
  import { z } from "zod";
@@ -23,11 +24,10 @@ import { z } from "zod";
23
24
  },
24
25
  isEnabled: (scope) => scope.collections.readable.length > 0,
25
26
  inputSchema: (scope) => ({
26
- collection: slugEnum(scope.collections.readable),
27
+ collection: stringEnum(scope.collections.readable),
27
28
  where: z.record(z.string(), z.unknown()).optional(),
28
29
  sort: z.string().optional().describe("e.g. \"-updatedAt\" for descending."),
29
- limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe("Default 10."),
30
- page: z.number().int().min(1).optional(),
30
+ ...pageShape(scope),
31
31
  ...depthShape(scope),
32
32
  select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
33
33
  ...localeShape(scope, {
@@ -37,7 +37,7 @@ import { z } from "zod";
37
37
  draft: z.boolean().optional().describe("Default true: latest drafts.")
38
38
  }),
39
39
  handler: async ({ args, scope }) => {
40
- resolveEntity(scope, { collection: args.collection }, "read");
40
+ const target = resolveCollection(scope, args.collection, "read");
41
41
  const locale = localeOf(scope, args.locale);
42
42
  assertQueryable(scope, {
43
43
  collection: args.collection,
@@ -52,22 +52,17 @@ import { z } from "zod";
52
52
  limit: args.limit ?? 10,
53
53
  overrideAccess: false,
54
54
  req: mcpxReadRequest(scope),
55
- ...args.page === void 0 ? {} : { page: args.page },
56
- ...args.sort === void 0 ? {} : { sort: args.sort },
57
- ...args.where === void 0 ? {} : { where: args.where },
58
- ...args.select === void 0 ? {} : { select: args.select },
59
- ...locale === void 0 ? {} : { locale }
55
+ ...definedProps({
56
+ page: args.page,
57
+ sort: args.sort,
58
+ where: args.where,
59
+ select: args.select,
60
+ locale
61
+ })
60
62
  });
61
63
  return jsonResult({
62
- docs: result.docs.map((doc) => stripAdminHidden(scope.req.payload.config, {
63
- kind: "collection",
64
- slug: args.collection
65
- }, doc)),
66
- totalDocs: result.totalDocs,
67
- page: result.page,
68
- totalPages: result.totalPages,
69
- limit: result.limit,
70
- hasNextPage: result.hasNextPage
64
+ docs: result.docs.map((doc) => stripAdminHidden(scope.req.payload.config, target, doc)),
65
+ ...pageFields(result)
71
66
  });
72
67
  }
73
68
  });
@@ -1,15 +1,16 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
+ import { definedProps } from "../guards.mjs";
2
3
  import { jsonResult } from "../result.mjs";
3
- import { draftVersionSlugs, entityShape, idShape, localeOf, localeShape, slugsFor, widen } from "./shared.mjs";
4
+ import { entityShape, idShape, localeOf, localeShape, pageFields, pageShape, reaches, slugsFor, slugsWhere, widen } from "./shared.mjs";
4
5
  import { resolveDocument } from "./document.mjs";
5
6
  import { queryVersions } from "./versions.mjs";
7
+ import { z } from "zod";
6
8
  import { APIError } from "payload";
7
9
  import { hasDraftsEnabled } from "payload/shared";
8
- import { z } from "zod";
9
10
  //#region src/tools/find-versions.ts
10
11
  const DESCRIPTION = `Lists the versions of one document or global, newest first, autosaves included. Returns per version "versionId", "createdAt", "updatedAt", "status", "latest", "autosave" and, where set, "publishedLocale", without the content. Read a version with getDocument "versionId", or compare it with "diffFrom".`;
11
12
  const statusShape = (scope) => {
12
- const { collections, globals } = draftVersionSlugs(scope);
13
+ const { collections, globals } = slugsWhere(scope, (entity) => entity.hasDrafts, slugsFor(scope, "versions"));
13
14
  if (collections.length + globals.length === 0) return widen({});
14
15
  return widen({ status: z.enum(["published", "draft"]).optional() });
15
16
  };
@@ -25,15 +26,11 @@ const statusShape = (scope) => {
25
26
  readOnlyHint: true,
26
27
  openWorldHint: false
27
28
  },
28
- isEnabled: (scope) => {
29
- const { collections, globals } = slugsFor(scope, "versions");
30
- return collections.length + globals.length > 0;
31
- },
29
+ isEnabled: (scope) => reaches(scope, "versions"),
32
30
  inputSchema: (scope) => ({
33
31
  ...entityShape(scope, "versions"),
34
32
  ...idShape(scope, "versions"),
35
- limit: z.number().int().min(1).max(scope.limits.maxLimit).optional().describe("Default 10."),
36
- page: z.number().int().min(1).optional(),
33
+ ...pageShape(scope),
37
34
  ...statusShape(scope),
38
35
  ...localeShape(scope, {
39
36
  required: false,
@@ -49,8 +46,10 @@ const statusShape = (scope) => {
49
46
  locale: localeOf(scope, args.locale)
50
47
  }, {
51
48
  limit: args.limit ?? 10,
52
- ...args.page === void 0 ? {} : { page: args.page },
53
- ...args.status === void 0 ? {} : { status: args.status }
49
+ ...definedProps({
50
+ page: args.page,
51
+ status: args.status
52
+ })
54
53
  });
55
54
  const versions = result.docs.map((doc) => ({
56
55
  versionId: doc.id,
@@ -63,10 +62,7 @@ const statusShape = (scope) => {
63
62
  }));
64
63
  return jsonResult({
65
64
  versions,
66
- totalDocs: result.totalDocs,
67
- page: result.page,
68
- totalPages: result.totalPages,
69
- hasNextPage: result.hasNextPage
65
+ ...pageFields(result)
70
66
  });
71
67
  }
72
68
  });