@abinnovision/payloadcms-mcpx 2.0.0-beta.18 → 2.0.0-beta.20

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 (65) hide show
  1. package/README.md +2 -2
  2. package/dist/admin/capability-matrix.d.mts +2 -1
  3. package/dist/admin/capability-matrix.mjs +291 -340
  4. package/dist/admin/capability-toggles.mjs +24 -31
  5. package/dist/admin/confirmations-panel.mjs +61 -71
  6. package/dist/admin/confirmations.mjs +2 -1
  7. package/dist/admin/endpoint-url.mjs +7 -0
  8. package/dist/admin/setup-guide.mjs +2 -1
  9. package/dist/api-keys/capability-matrix.d.mts +4 -7
  10. package/dist/api-keys/capability-matrix.mjs +12 -51
  11. package/dist/api-keys/collection.mjs +10 -17
  12. package/dist/api-keys/confirmation-view.mjs +2 -1
  13. package/dist/api-keys/fields.mjs +53 -69
  14. package/dist/auth/resolve.mjs +29 -10
  15. package/dist/capabilities.d.mts +2 -0
  16. package/dist/capabilities.mjs +62 -19
  17. package/dist/endpoint/confirmations.mjs +14 -14
  18. package/dist/endpoint/handler.mjs +6 -22
  19. package/dist/endpoint/http.mjs +27 -0
  20. package/dist/endpoint/upload.mjs +11 -43
  21. package/dist/grants/confirmation.mjs +11 -42
  22. package/dist/grants/grant.mjs +58 -40
  23. package/dist/guards.mjs +8 -1
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.mjs +1 -1
  26. package/dist/options.mjs +34 -54
  27. package/dist/plugin.mjs +2 -1
  28. package/dist/request.mjs +5 -1
  29. package/dist/result.mjs +11 -1
  30. package/dist/schema/admin-hidden.mjs +4 -4
  31. package/dist/schema/describe.mjs +12 -18
  32. package/dist/schema/index.mjs +5 -6
  33. package/dist/schema/lexical-pointer.mjs +8 -8
  34. package/dist/schema/lexical.mjs +15 -13
  35. package/dist/schema/outline.mjs +4 -5
  36. package/dist/schema/path.mjs +4 -1
  37. package/dist/schema/pointer.mjs +11 -17
  38. package/dist/schema/shape.mjs +38 -50
  39. package/dist/schema/walk.mjs +57 -18
  40. package/dist/tools/create-document.mjs +16 -44
  41. package/dist/tools/delete-document.mjs +14 -17
  42. package/dist/tools/describe-schema.mjs +7 -5
  43. package/dist/tools/document.mjs +130 -44
  44. package/dist/tools/duplicate-document.mjs +11 -10
  45. package/dist/tools/find-documents.mjs +15 -20
  46. package/dist/tools/find-versions.mjs +10 -14
  47. package/dist/tools/get-document.mjs +8 -12
  48. package/dist/tools/links.mjs +2 -1
  49. package/dist/tools/list-capabilities.mjs +2 -1
  50. package/dist/tools/patch-document.mjs +12 -44
  51. package/dist/tools/publish-document.mjs +9 -27
  52. package/dist/tools/query-paths.mjs +1 -1
  53. package/dist/tools/run-confirmed.mjs +38 -50
  54. package/dist/tools/shared.mjs +43 -41
  55. package/dist/tools/validate-document.mjs +4 -6
  56. package/dist/tools/versions.mjs +11 -6
  57. package/dist/types.d.mts +12 -26
  58. package/dist/upload/file.mjs +10 -18
  59. package/dist/write/draft-guard.mjs +20 -20
  60. package/dist/write/patch.mjs +4 -4
  61. package/dist/write/publish-intent.mjs +22 -28
  62. package/dist/write/row-ids.mjs +3 -1
  63. package/dist/write/write-data.mjs +18 -31
  64. package/package.json +5 -5
  65. package/dist/builtin-tool-names.mjs +0 -19
@@ -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";
1
+ import { definedProps } from "../guards.mjs";
2
+ import { errorResult, jsonResult } from "../result.mjs";
3
+ import { documentLinks } from "./links.mjs";
2
4
  import { collectPublishBlockers } from "../write/publish-blockers.mjs";
3
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 };
@@ -1,11 +1,10 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
- import { idSchema, liveWriteSentence, localeOf, slugEnum, slugsFor } from "./shared.mjs";
3
- import { resolveEntity } from "./document.mjs";
4
- import { createdResult } from "./create-document.mjs";
2
+ import { idSchema, liveWriteSentence, localeOf, slugsFor, stringEnum } from "./shared.mjs";
3
+ import { resolveCollection, writtenResult } from "./document.mjs";
5
4
  import { withDuplicateIntent } from "../write/publish-intent.mjs";
6
5
  import { Forbidden } from "payload";
7
6
  //#region src/tools/duplicate-document.ts
8
- const duplicableSlugs = (scope) => slugsFor(scope, "create").collections.filter((slug) => scope.req.payload.config.collections.find((collection) => collection.slug === slug)?.disableDuplicate !== true && !scope.exposure.collections.some((entity) => entity.slug === slug && entity.isUpload));
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));
9
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".
10
9
 
11
10
  ${liveWriteSentence(scope, "create")}`;
@@ -23,12 +22,12 @@ ${liveWriteSentence(scope, "create")}`;
23
22
  },
24
23
  isEnabled: (scope) => duplicableSlugs(scope).length > 0,
25
24
  inputSchema: (scope) => ({
26
- collection: slugEnum(duplicableSlugs(scope)),
25
+ collection: stringEnum(duplicableSlugs(scope)),
27
26
  id: idSchema
28
27
  }),
29
28
  handler: async ({ args, scope }) => {
30
- const target = resolveEntity(scope, { collection: args.collection }, "create");
31
- if (target.kind === "global" || !duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
29
+ const target = resolveCollection(scope, args.collection, "create");
30
+ if (!duplicableSlugs(scope).includes(target.slug)) throw new Forbidden(scope.req.t);
32
31
  const created = await scope.req.payload.duplicate({
33
32
  collection: target.slug,
34
33
  id: args.id,
@@ -38,9 +37,11 @@ ${liveWriteSentence(scope, "create")}`;
38
37
  overrideAccess: false,
39
38
  req: scope.req
40
39
  });
41
- return await createdResult(scope, {
42
- target,
43
- id: created["id"],
40
+ return await writtenResult(scope, {
41
+ target: {
42
+ ...target,
43
+ id: created["id"]
44
+ },
44
45
  locale: localeOf(scope, void 0)
45
46
  });
46
47
  }
@@ -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,6 +1,7 @@
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";
6
7
  import { z } from "zod";
@@ -9,7 +10,7 @@ import { hasDraftsEnabled } from "payload/shared";
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
  });
@@ -1,13 +1,13 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
2
  import { errorResult, jsonResult } from "../result.mjs";
3
- import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
4
- import { findRichTextField } from "../schema/walk.mjs";
5
3
  import { SchemaError } from "../schema/errors.mjs";
4
+ import { JSON_POINTER_PATTERN, prototypeSegmentProblem, splitPath } from "../schema/path.mjs";
5
+ import { findFieldAt } from "../schema/walk.mjs";
6
6
  import { lexicalOutline } from "../schema/outline.mjs";
7
7
  import { resolveDataPointer } from "../schema/pointer.mjs";
8
8
  import "../schema/index.mjs";
9
9
  import { downloadHandoff, downloadSlugs } from "../upload/file.mjs";
10
- import { READ_LOCALE_DESCRIPTION, depthShape, entityShape, idSchema, idShape, localeOf, localeShape, slugsFor, widen } from "./shared.mjs";
10
+ import { READ_LOCALE_DESCRIPTION, depthShape, entityShape, idSchema, idShape, localeOf, localeShape, reaches, slugsFor, widen } from "./shared.mjs";
11
11
  import { identityOf, resolveDocument } from "./document.mjs";
12
12
  import { diffDocuments, loadPublished, loadVersion, readLive } from "./versions.mjs";
13
13
  import { z } from "zod";
@@ -19,10 +19,6 @@ const VERSION_PARAGRAPH = `
19
19
 
20
20
  "versionId" and "diffFrom" work where findVersions does. To revert, call getDocument with the old version as "versionId", the version findVersions marks "latest" as "diffFrom" and the locale you will write, then apply the returned "patch" with patchDocument.`;
21
21
  const DESCRIPTION = `Reads one document or global, or with "path" only the value at that pointer. Returns the latest draft by default, with the "updatedAt" a write takes as "expectedUpdatedAt".`;
22
- const exposesVersions = (scope) => {
23
- const { collections, globals } = slugsFor(scope, "versions");
24
- return collections.length + globals.length > 0;
25
- };
26
22
  const assertVersionArgs = (scope, target, args) => {
27
23
  if (args.versionId === void 0 && args.diffFrom === void 0) return;
28
24
  const { collections, globals } = slugsFor(scope, "versions");
@@ -41,7 +37,7 @@ const diffResult = async (scope, read, diff, doc) => {
41
37
  });
42
38
  };
43
39
  const versionShape = (scope) => {
44
- if (!exposesVersions(scope)) return widen({});
40
+ if (!reaches(scope, "versions")) return widen({});
45
41
  return widen({
46
42
  versionId: idSchema.optional().describe("From findVersions. Not with \"draft\"."),
47
43
  diffFrom: idSchema.optional().describe("Version id, or \"published\" for the newest version published in any locale. Returns {from, to, patch}, the JSON Patch from that version to what is read.")
@@ -65,12 +61,12 @@ const withDownload = async (scope, doc, target) => typeof doc["filename"] === "s
65
61
  * the `expectedUpdatedAt` timestamp without a second call.
66
62
  */ const getDocument = defineMcpxTool({
67
63
  name: "getDocument",
68
- description: (scope) => exposesVersions(scope) ? DESCRIPTION + VERSION_PARAGRAPH : DESCRIPTION,
64
+ description: (scope) => reaches(scope, "versions") ? DESCRIPTION + VERSION_PARAGRAPH : DESCRIPTION,
69
65
  annotations: {
70
66
  readOnlyHint: true,
71
67
  openWorldHint: false
72
68
  },
73
- isEnabled: (scope) => scope.collections.readable.length + scope.globals.readable.length > 0,
69
+ isEnabled: (scope) => reaches(scope, "read"),
74
70
  inputSchema: (scope) => ({
75
71
  ...entityShape(scope, "read"),
76
72
  ...idShape(scope, "read"),
@@ -121,7 +117,7 @@ const withDownload = async (scope, doc, target) => typeof doc["filename"] === "s
121
117
  const path = pointer.toString();
122
118
  const value = pointer.get(doc);
123
119
  const envelope = {
124
- ...identityOf(target, args.id),
120
+ ...identityOf(target),
125
121
  status: doc["_status"],
126
122
  updatedAt: doc["updatedAt"],
127
123
  path
@@ -141,7 +137,7 @@ const withDownload = async (scope, doc, target) => typeof doc["filename"] === "s
141
137
  if (!(error instanceof SchemaError)) throw error;
142
138
  return errorResult(error.message);
143
139
  }
144
- const field = resolution.descriptor?.type === "richText" && !resolution.lexical ? findRichTextField(resolution.fields, splitPath(resolution.descriptor.path)) : void 0;
140
+ const field = resolution.descriptor?.type === "richText" && !resolution.lexical ? findFieldAt(resolution.fields, splitPath(resolution.descriptor.path), "richText") : void 0;
145
141
  if (!field) return errorResult(OUTLINE_ERROR);
146
142
  return jsonResult({
147
143
  ...envelope,
@@ -1,3 +1,4 @@
1
+ import { publicOrigin } from "../request.mjs";
1
2
  import { formatAdminURL } from "payload/shared";
2
3
  //#region src/tools/links.ts
3
4
  const livePreviewOf = (req, target) => {
@@ -35,7 +36,7 @@ const previewOf = async (req, args) => {
35
36
  */ const documentLinks = async (req, args) => {
36
37
  const { target, doc, locale } = args;
37
38
  const { config } = req.payload;
38
- const origin = config.serverURL || new URL(req.url ?? "").origin;
39
+ const origin = publicOrigin(req);
39
40
  const path = target.kind === "collection" ? `/collections/${target.slug}/${String(doc["id"])}` : `/globals/${target.slug}`;
40
41
  const adminUrl = formatAdminURL({
41
42
  adminRoute: config.routes.admin,
@@ -2,6 +2,7 @@ import { defineMcpxTool } from "../define-tool.mjs";
2
2
  import { jsonResult } from "../result.mjs";
3
3
  import { translateStatic, translatorFor } from "../i18n.mjs";
4
4
  import { slugsFor } from "./shared.mjs";
5
+ import { globalConfig } from "./document.mjs";
5
6
  import { MCPX_ISSUES_URL, MCPX_REPOSITORY_URL } from "../version.mjs";
6
7
  import { hasDraftValidationEnabled } from "payload/shared";
7
8
  //#region src/tools/list-capabilities.ts
@@ -59,7 +60,7 @@ const translateLabel = (scope, label, fallback) => {
59
60
  });
60
61
  const globals = scope.exposure.globals.flatMap((entry) => {
61
62
  const capability = scope.capabilities.globals[entry.slug];
62
- const config = payload.globals.config.find((candidate) => candidate.slug === entry.slug);
63
+ const config = globalConfig(payload, entry.slug);
63
64
  if (!capability || !config || !capability.read) return [];
64
65
  const description = translate(config.admin.description);
65
66
  return [{
@@ -1,13 +1,11 @@
1
1
  import { defineMcpxTool } from "../define-tool.mjs";
2
2
  import { isPlainObject } from "../guards.mjs";
3
- import { errorResult, jsonResult } from "../result.mjs";
3
+ import { errorResult } from "../result.mjs";
4
4
  import { JSON_POINTER_PATTERN } from "../schema/path.mjs";
5
5
  import "../schema/index.mjs";
6
- import { collectPublishBlockers } from "../write/publish-blockers.mjs";
7
6
  import { assertAcceptsFile, fileShape, fileSlugs, fileWriteRefusal, uploadHandoff, uploadedFile } from "../upload/file.mjs";
8
- import { entityShape, idShape, liveWriteSentence, localeOf, localeShape } from "./shared.mjs";
9
- import { identityOf, readDraft, resolveDocument, staleReadResult } from "./document.mjs";
10
- import { documentLinks } from "./links.mjs";
7
+ import { entityShape, expectedUpdatedAtShape, idShape, liveWriteSentence, localeOf, localeShape, reaches } from "./shared.mjs";
8
+ import { readDraft, resolveDocument, staleReadResult, updateTarget, writtenResult } from "./document.mjs";
11
9
  import { withTransaction } from "../write/transaction.mjs";
12
10
  import { applyPatchOperations, isElementPointer } from "../write/patch.mjs";
13
11
  import { buildWriteData } from "../write/write-data.mjs";
@@ -88,7 +86,7 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
88
86
  idempotentHint: false,
89
87
  openWorldHint: false
90
88
  },
91
- isEnabled: (scope) => scope.collections.writable.length + scope.globals.writable.length > 0,
89
+ isEnabled: (scope) => reaches(scope, "write"),
92
90
  inputSchema: (scope) => ({
93
91
  ...entityShape(scope, "write"),
94
92
  ...idShape(scope, "write"),
@@ -97,7 +95,7 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
97
95
  description: "Localized fields are written in this locale only."
98
96
  }),
99
97
  patches: fileSlugs(scope).length === 0 ? z.array(PATCH_OPERATION_SCHEMA).min(1).max(PATCHES_LIMIT) : z.array(PATCH_OPERATION_SCHEMA).max(PATCHES_LIMIT).describe("May be empty when \"file\" is given."),
100
- expectedUpdatedAt: z.string().optional().describe("\"updatedAt\" from your last read. Refused if the document changed since."),
98
+ ...expectedUpdatedAtShape,
101
99
  ...fileShape(scope, (slugs) => `Replaces the file of a document in ${slugs}. The call returns an "upload" for the bytes instead of writing.`)
102
100
  }),
103
101
  handler: async ({ args, scope }) => {
@@ -110,11 +108,6 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
110
108
  if (file !== void 0) assertAcceptsFile(scope, target, file);
111
109
  if (patches.length === 0 && file === void 0) throw new APIError("\"patches\" may be empty only when \"file\" is given.", 400);
112
110
  const report = async (next) => {
113
- const saved = await readDraft(scope, {
114
- target,
115
- locale,
116
- privileged: true
117
- });
118
111
  const readable = await readDraft(scope, {
119
112
  target,
120
113
  locale
@@ -123,22 +116,10 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
123
116
  throw error;
124
117
  });
125
118
  const notApplied = readable ? notAppliedPointers(patches, next, readable) : [];
126
- const validation = await collectPublishBlockers(scope.req, {
127
- doc: saved,
128
- entity: target
129
- });
130
- return jsonResult({
131
- ...identityOf(target, saved["id"]),
132
- status: saved["_status"],
133
- updatedAt: saved["updatedAt"],
134
- ...await documentLinks(scope.req, {
135
- target,
136
- doc: saved,
137
- locale
138
- }),
139
- ...validation.blockers.length > 0 ? { publishBlockers: validation.blockers } : {},
140
- ...validation.unavailable ? { publishBlockersUnavailable: true } : {},
141
- ...notApplied.length > 0 ? { notApplied } : {}
119
+ return await writtenResult(scope, {
120
+ target,
121
+ locale,
122
+ extra: notApplied.length > 0 ? { notApplied } : {}
142
123
  });
143
124
  };
144
125
  const outcome = await withTransaction(scope.req, async () => {
@@ -167,24 +148,11 @@ const notAppliedPointers = (patches, intended, saved) => patches.flatMap((operat
167
148
  expectedUpdatedAt: doc["updatedAt"]
168
149
  } };
169
150
  }
170
- const write = {
151
+ await updateTarget(scope, target, {
171
152
  data: buildWriteData(payload.config, target.config, applied.next),
172
- depth: 0,
173
153
  draft: true,
174
- overrideAccess: false,
175
- req: scope.req,
176
- ...locale === void 0 ? {} : { locale }
177
- };
178
- if (target.kind === "collection") await payload.update({
179
- ...write,
180
- collection: target.slug,
181
- id: target.id,
182
- ...bytes === void 0 ? {} : { file: bytes }
183
- });
184
- else await payload.updateGlobal({
185
- ...write,
186
- fallbackLocale: false,
187
- slug: target.slug
154
+ locale,
155
+ file: bytes
188
156
  });
189
157
  return bytes === void 0 ? { done: await report(applied.next) } : { written: applied.next };
190
158
  });