@abinnovision/payloadcms-mcpx 1.1.0-beta.6 → 2.0.0-beta.7

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 (69) hide show
  1. package/README.md +141 -470
  2. package/dist/{client → admin}/capability-matrix.d.mts +3 -4
  3. package/dist/admin/capability-matrix.mjs +354 -0
  4. package/dist/admin/capability-toggles.mjs +73 -0
  5. package/dist/{client → admin}/setup-guide.d.mts +1 -1
  6. package/dist/{client → admin}/setup-guide.mjs +3 -10
  7. package/dist/api-keys/capability-matrix.d.mts +6 -6
  8. package/dist/api-keys/capability-matrix.mjs +15 -85
  9. package/dist/api-keys/fields.mjs +15 -24
  10. package/dist/auth/resolve.mjs +23 -5
  11. package/dist/{tools/names.mjs → builtin-tool-names.mjs} +1 -1
  12. package/dist/capabilities.mjs +34 -42
  13. package/dist/define-tool.d.mts +19 -0
  14. package/dist/{types.mjs → define-tool.mjs} +1 -1
  15. package/dist/endpoint/errors.mjs +5 -6
  16. package/dist/endpoint/handler.mjs +49 -19
  17. package/dist/endpoint/server.mjs +65 -19
  18. package/dist/entity.d.mts +9 -0
  19. package/dist/guards.mjs +5 -0
  20. package/dist/i18n.mjs +8 -8
  21. package/dist/index.d.mts +5 -2
  22. package/dist/index.mjs +3 -2
  23. package/dist/options.mjs +51 -45
  24. package/dist/plugin.mjs +0 -1
  25. package/dist/request.d.mts +2 -3
  26. package/dist/request.mjs +2 -3
  27. package/dist/result.d.mts +2 -3
  28. package/dist/result.mjs +3 -3
  29. package/dist/schema/admin-hidden.mjs +91 -0
  30. package/dist/schema/describe.mjs +17 -33
  31. package/dist/schema/errors.mjs +8 -0
  32. package/dist/schema/index.mjs +5 -2
  33. package/dist/schema/lexical-pointer.mjs +18 -20
  34. package/dist/schema/lexical.mjs +39 -92
  35. package/dist/schema/outline.mjs +5 -15
  36. package/dist/schema/path.mjs +22 -0
  37. package/dist/schema/pointer.mjs +18 -25
  38. package/dist/schema/shape.mjs +25 -71
  39. package/dist/schema/walk.mjs +35 -45
  40. package/dist/tools/builtin.mjs +4 -4
  41. package/dist/tools/create-document.mjs +26 -23
  42. package/dist/tools/describe-schema.mjs +38 -34
  43. package/dist/tools/document.mjs +84 -0
  44. package/dist/tools/find-documents.mjs +32 -19
  45. package/dist/tools/find-versions.mjs +22 -18
  46. package/dist/tools/get-document.mjs +39 -32
  47. package/dist/tools/list-capabilities.mjs +23 -16
  48. package/dist/tools/patch-document.mjs +74 -50
  49. package/dist/tools/publish-document.mjs +17 -28
  50. package/dist/tools/query-paths.mjs +55 -0
  51. package/dist/tools/read-request.d.mts +10 -0
  52. package/dist/tools/read-request.mjs +43 -0
  53. package/dist/tools/shared.mjs +58 -99
  54. package/dist/tools/validate-document.mjs +16 -29
  55. package/dist/tools/versions.mjs +25 -17
  56. package/dist/types.d.mts +54 -56
  57. package/dist/write/draft-guard.mjs +17 -37
  58. package/dist/write/patch.mjs +27 -183
  59. package/dist/write/publish-blockers.mjs +7 -14
  60. package/dist/write/publish-intent.mjs +4 -4
  61. package/dist/write/row-ids.mjs +44 -0
  62. package/dist/write/transaction.mjs +5 -8
  63. package/dist/write/write-data.mjs +64 -0
  64. package/package.json +4 -4
  65. package/dist/client/capability-matrix.mjs +0 -349
  66. package/dist/endpoint/index.mjs +0 -4
  67. package/dist/tools/target.mjs +0 -45
  68. /package/dist/{client → admin}/index.d.mts +0 -0
  69. /package/dist/{client → admin}/index.mjs +0 -0
@@ -1,159 +1,118 @@
1
- import { canCreate, canPublish, isLiveWrite } from "../capabilities.mjs";
2
- import { translateStatic } from "../i18n.mjs";
3
- import { NotFound } from "payload";
1
+ import { canCreate, isLiveWrite } from "../capabilities.mjs";
4
2
  import { z } from "zod";
5
3
  //#region src/tools/shared.ts
6
4
  /**
7
5
  * An out-of-scope slug fails schema validation before a handler runs, so a
8
6
  * client only ever sees what its key may touch.
9
7
  */ const slugEnum = (slugs) => z.enum(slugs);
10
- /** Payload's id type follows the adapter, so both forms are handed on as read. */ const idSchema = z.union([z.string(), z.number()]).describe("Document id.");
8
+ /** Payload's id type follows the adapter, so both forms are handed on as read. */ const idSchema = z.union([z.string(), z.number()]);
9
+ const slugsOf = (scope, key) => ({
10
+ collections: scope.collections[key],
11
+ globals: scope.globals[key]
12
+ });
11
13
  const slugsWhere = (scope, predicate, allowed) => {
12
14
  const pick = (entities, slugs) => entities.filter((entity) => slugs.includes(entity.slug) && predicate(entity)).map((entity) => entity.slug);
13
15
  return [...pick(scope.exposure.collections, allowed.collections), ...pick(scope.exposure.globals, allowed.globals)];
14
16
  };
15
17
  /**
16
- * Slugs this key may write whose writes land live rather than as a draft. An
17
- * entity without versions has no draft to land on, so `write: "live"` there
18
- * makes every write a live one. Empty for every key that can only write drafts.
19
- */ const liveWriteSlugs = (scope) => slugsWhere(scope, isLiveWrite, {
20
- collections: scope.writable,
21
- globals: scope.writableGlobals
22
- });
23
- /**
24
18
  * Slugs this key may write but never create in, because their documents are
25
19
  * files. Collection-only, since nothing creates a global either way.
26
20
  */ const patchOnlySlugs = (scope) => slugsWhere(scope, (entity) => !canCreate(entity), {
27
- collections: scope.writable,
21
+ collections: scope.collections.writable,
28
22
  globals: []
29
23
  });
30
- /** Slugs this key may write and, separately, publish. */ const publishableWriteSlugs = (scope) => slugsWhere(scope, canPublish, {
31
- collections: scope.publishable,
32
- globals: scope.publishableGlobals
33
- });
34
- /**
35
- * What a write actually does for this key, and what it takes to make it public.
36
- * A live-write slug has no draft and no publish step; a publishable one has
37
- * both. Stated per key so a client is never told its writes are drafts while
38
- * they are not, nor that publishing is out of reach when it is not.
39
- */ const draftSentence = (scope) => {
40
- const live = liveWriteSlugs(scope);
41
- const publishable = publishableWriteSlugs(scope);
42
- return `${live.length === 0 ? "Every write lands as a draft." : `Writes land as drafts, except for ${live.join(", ")}, which have no drafts: a write there changes the live document immediately.`} ${publishable.length === 0 ? "Nothing this key writes is ever published; publishing stays a human action in the admin panel." : `Publish a draft with publishDocument, which this key may do for ${publishable.join(", ")}. Publishing anything else stays a human action in the admin panel.`}`;
43
- };
44
- /** The value a client read back is a string; what it meets may be a Date. */ const sameInstant = (left, right) => typeof left === "string" && new Date(left).getTime() === new Date(right).getTime();
45
- /** Unchecked, because the runtime shape really does vary; `Branch` guards it. */ const widen = (branch) => branch;
46
- /** The one list the shape helpers and {@link resolveTarget} both read. */ const slugsFor = (scope, operation) => {
24
+ const widen = (branch) => branch;
25
+ /** The one list the shape helpers and {@link resolveEntity} both read. */ const slugsFor = (scope, operation) => {
47
26
  switch (operation) {
48
27
  case "create": return {
49
28
  collections: slugsWhere(scope, canCreate, {
50
- collections: scope.writable,
29
+ collections: scope.collections.writable,
51
30
  globals: []
52
31
  }),
53
32
  globals: []
54
33
  };
55
- case "publish": return {
56
- collections: scope.publishable,
57
- globals: scope.publishableGlobals
58
- };
59
- case "read": return {
60
- collections: scope.readable,
61
- globals: scope.readableGlobals
62
- };
34
+ case "publish": return slugsOf(scope, "publishable");
35
+ case "read": return slugsOf(scope, "readable");
63
36
  case "versions": return {
64
37
  collections: slugsWhere(scope, (entity) => entity.hasVersions, {
65
- collections: scope.readable,
38
+ collections: scope.collections.readable,
66
39
  globals: []
67
40
  }),
68
41
  globals: slugsWhere(scope, (entity) => entity.hasVersions, {
69
42
  collections: [],
70
- globals: scope.readableGlobals
43
+ globals: scope.globals.readable
71
44
  })
72
45
  };
73
- case "write": return {
74
- collections: scope.writable,
75
- globals: scope.writableGlobals
76
- };
46
+ case "write": return slugsOf(scope, "writable");
77
47
  }
78
48
  };
79
49
  /**
50
+ * The slugs of {@link slugsFor} `"versions"` that also have drafts. Only they
51
+ * have `_status` on their versions, so only they can be queried by status.
52
+ */ const draftVersionSlugs = (scope) => {
53
+ const { collections, globals } = slugsFor(scope, "versions");
54
+ return {
55
+ collections: slugsWhere(scope, (entity) => entity.hasDrafts, {
56
+ collections,
57
+ globals: []
58
+ }),
59
+ globals: slugsWhere(scope, (entity) => entity.hasDrafts, {
60
+ collections: [],
61
+ globals
62
+ })
63
+ };
64
+ };
65
+ /** Slugs reachable by `operation` whose writes go live, because they have no draft to write. */ const liveWriteSlugs = (scope, operation) => slugsWhere(scope, isLiveWrite, slugsFor(scope, operation));
66
+ /**
67
+ * Stated on the tools that write, since a client calling them must know before
68
+ * the call which writes are public.
69
+ */ const liveWriteSentence = (scope, operation) => {
70
+ const live = liveWriteSlugs(scope, operation);
71
+ return live.length === 0 ? "Every write is saved as a draft." : `Writes to ${live.join(", ")} go live immediately. Every other write is saved as a draft.`;
72
+ };
73
+ /**
80
74
  * With no reachable global, `global` is left out and `collection` stays
81
75
  * required, so a deployment without globals sees an unchanged schema. Only the
82
76
  * mixed case makes either optional, and the handler enforces exclusivity there.
83
- */ const targetShape = (scope, operation, descriptions) => {
77
+ * `globalRule` states that rule on `global` for a tool without an `id`.
78
+ */ const entityShape = (scope, operation, globalRule) => {
84
79
  const { collections, globals } = slugsFor(scope, operation);
85
- if (globals.length === 0) return widen({ collection: slugEnum(collections).describe(descriptions.collection) });
86
- if (collections.length === 0) return widen({ global: slugEnum(globals).describe(descriptions.global) });
80
+ if (globals.length === 0) return widen({ collection: slugEnum(collections) });
81
+ if (collections.length === 0) return widen({ global: slugEnum(globals) });
82
+ const global = slugEnum(globals).optional();
87
83
  return widen({
88
- collection: slugEnum(collections).optional().describe(descriptions.collection),
89
- global: slugEnum(globals).optional().describe(descriptions.global)
84
+ collection: slugEnum(collections).optional(),
85
+ global: globalRule === void 0 ? global : global.describe(globalRule)
90
86
  });
91
87
  };
92
88
  /**
93
89
  * Only a collection document has one. Optional in the mixed case, where
94
- * `requireIdFor` enforces the dependency.
90
+ * `resolveDocument` enforces the dependency and the description states it.
95
91
  */ const idShape = (scope, operation) => {
96
92
  const { collections, globals } = slugsFor(scope, operation);
97
93
  if (collections.length === 0) return widen({});
98
94
  if (globals.length === 0) return widen({ id: idSchema });
99
- return widen({ id: idSchema.optional().describe("Document id. Required with \"collection\"; must be omitted with \"global\".") });
95
+ return widen({ id: idSchema.optional().describe("Required with \"collection\", omitted with \"global\".") });
100
96
  };
97
+ /** For the reads that fall back to the default locale, unlike the writes. */ const READ_LOCALE_DESCRIPTION = "Default: the default locale. A value missing in this locale is shown from the default locale.";
101
98
  const localeShape = (scope, options) => {
102
- if (!scope.locales) return widen({});
103
- const locale = z.enum(scope.locales);
104
- return widen({ locale: (options.required ? locale : locale.optional()).describe(options.description) });
99
+ if (!scope.localization) return widen({});
100
+ const enumerated = z.enum(scope.localization.locales);
101
+ const locale = options.required ? enumerated : enumerated.optional();
102
+ return widen({ locale: options.description === void 0 ? locale : locale.describe(options.description) });
105
103
  };
106
104
  /**
107
105
  * Defaults to 0 rather than Payload's own default: a client usually wants ids
108
106
  * it can write back, and populating a relation costs a query.
109
- */ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.limits.maxDepth).optional().describe(`Relationship population depth. Default 0, at most ${String(scope.limits.maxDepth)}.`) });
107
+ */ const depthShape = (scope) => ({ depth: z.number().int().min(0).max(scope.limits.maxDepth).optional().describe("Default 0. A relationship into a collection this key cannot read stays an id.") });
110
108
  /**
111
109
  * The locale to operate on: the explicit argument, else the request's, else
112
110
  * the default. `undefined` when localization is off.
113
111
  */ const localeOf = (scope, locale) => {
114
- if (!scope.locales) return;
112
+ if (!scope.localization) return;
113
+ const { locales, defaultLocale } = scope.localization;
115
114
  const requested = locale ?? scope.req.locale;
116
- return (requested && scope.locales.includes(requested) ? requested : scope.defaultLocale) ?? void 0;
117
- };
118
- /**
119
- * Reads the current draft in a fixed locale with no fallback, which is the
120
- * shape that may be written back or validated without mixing locales.
121
- */ const readTarget = async (scope, args) => {
122
- const { payload } = scope.req;
123
- const privileged = args.privileged === true;
124
- const shared = {
125
- depth: 0,
126
- draft: true,
127
- ...args.locale === void 0 ? {} : {
128
- locale: args.locale,
129
- fallbackLocale: false
130
- },
131
- overrideAccess: privileged,
132
- showHiddenFields: privileged,
133
- req: scope.req
134
- };
135
- if (args.target.kind === "collection") {
136
- const doc = await payload.findByID({
137
- ...shared,
138
- collection: args.target.slug,
139
- id: args.id,
140
- disableErrors: true
141
- });
142
- if (!doc) throw new NotFound(scope.req.t);
143
- return doc;
144
- }
145
- return await payload.findGlobal({
146
- ...shared,
147
- slug: args.target.slug
148
- });
149
- };
150
- const translateLabel = (scope, label, fallback) => {
151
- const { i18n, t } = scope.req;
152
- const resolved = typeof label === "function" ? label({
153
- i18n,
154
- t
155
- }) : label;
156
- return translateStatic(resolved, i18n) ?? fallback;
115
+ return requested && locales.includes(requested) ? requested : defaultLocale;
157
116
  };
158
117
  //#endregion
159
- export { depthShape, draftSentence, idSchema, idShape, localeOf, localeShape, patchOnlySlugs, readTarget, sameInstant, slugEnum, slugsFor, targetShape, translateLabel };
118
+ export { READ_LOCALE_DESCRIPTION, depthShape, draftVersionSlugs, entityShape, idSchema, idShape, liveWriteSentence, liveWriteSlugs, localeOf, localeShape, patchOnlySlugs, slugEnum, slugsFor, widen };
@@ -1,48 +1,35 @@
1
+ import { defineMcpxTool } from "../define-tool.mjs";
1
2
  import { jsonResult } from "../result.mjs";
2
- import { idShape, localeOf, localeShape, readTarget, targetShape } from "./shared.mjs";
3
- import { requireIdFor, resolveTarget } from "./target.mjs";
4
- import { defineMcpxTool } from "../types.mjs";
3
+ import { entityShape, idShape, localeOf, localeShape } from "./shared.mjs";
4
+ import { identityOf, readDraft, resolveDocument } from "./document.mjs";
5
5
  import { collectPublishBlockers } from "../write/publish-blockers.mjs";
6
6
  /**
7
- * Gated on write rather than read, because publish blockers only mean
8
- * something to a caller who can act on them.
7
+ * Gated on write rather than read, because publish blockers only mean something
8
+ * to a caller who can act on them.
9
9
  *
10
10
  * It reads the document twice on purpose: once under the key's own access to
11
- * refuse a caller who may not see it, then privileged, so the check runs over
12
- * every field rather than the subset the user can read. It carries no
13
- * `readOnlyHint`, because the traversal fires field hooks.
11
+ * refuse a caller who may not see it, then privileged, so the check covers every
12
+ * field and not only those the user can read. It has no `readOnlyHint` because
13
+ * the traversal fires field hooks.
14
14
  */ const validateDocument = defineMcpxTool({
15
15
  name: "validateDocument",
16
- description: `Reports what still prevents a human from publishing the draft, without writing anything. The same list patchDocument returns after a write; use it to check work or to answer "is this ready".
17
-
18
- Pass exactly one of "collection" and "global". "id" is required with "collection" and must be omitted with "global", because a global is a singleton.
19
-
20
- Nothing is written, but the check runs the same field-level beforeValidate and beforeChange hooks a save would, so a hook with side effects fires. "publishBlockersUnavailable" means the check itself failed, so the empty list says nothing.`,
16
+ description: `Lists what still blocks publishing one document or global, without saving. Returns "publishBlockers", each with a pointer and a message. An empty list means it can be published, unless "publishBlockersUnavailable" is true: then the check itself failed. The check runs the field hooks a save runs.`,
21
17
  annotations: { openWorldHint: false },
22
- isEnabled: (scope) => scope.writable.length + scope.writableGlobals.length > 0,
18
+ isEnabled: (scope) => scope.collections.writable.length + scope.globals.writable.length > 0,
23
19
  inputSchema: (scope) => ({
24
- ...targetShape(scope, "write", {
25
- collection: "Collection holding the document.",
26
- global: "Global to validate."
27
- }),
20
+ ...entityShape(scope, "write"),
28
21
  ...idShape(scope, "write"),
29
- ...localeShape(scope, {
30
- required: true,
31
- description: "Locale to validate."
32
- })
22
+ ...localeShape(scope, { required: true })
33
23
  }),
34
24
  handler: async ({ args, scope }) => {
35
- const target = resolveTarget(scope, args, "write");
36
- const id = requireIdFor(target, args.id);
25
+ const target = resolveDocument(scope, args, "write");
37
26
  const locale = localeOf(scope, args.locale);
38
- await readTarget(scope, {
27
+ await readDraft(scope, {
39
28
  target,
40
- id,
41
29
  locale
42
30
  });
43
- const doc = await readTarget(scope, {
31
+ const doc = await readDraft(scope, {
44
32
  target,
45
- id,
46
33
  locale,
47
34
  privileged: true
48
35
  });
@@ -51,7 +38,7 @@ Nothing is written, but the check runs the same field-level beforeValidate and b
51
38
  entity: target
52
39
  });
53
40
  return jsonResult({
54
- ...target.kind === "collection" ? { id: doc["id"] } : { global: target.slug },
41
+ ...identityOf(target, doc["id"]),
55
42
  status: doc["_status"],
56
43
  updatedAt: doc["updatedAt"],
57
44
  publishBlockers: validation.blockers,
@@ -1,35 +1,41 @@
1
+ import { stripAdminHidden } from "../schema/admin-hidden.mjs";
2
+ import "../schema/index.mjs";
3
+ import { mcpxReadRequest } from "./read-request.mjs";
4
+ import { hasDraftsEnabled } from "payload/shared";
1
5
  import { Pointer, createPatch } from "rfc6902";
2
6
  //#region src/tools/versions.ts
3
- /** Bookkeeping that differs between saves without saying anything about content. */ const NOISE = [
7
+ const NOISE = [
4
8
  "id",
5
9
  "globalType",
6
10
  "createdAt",
7
11
  "updatedAt",
8
12
  "_status"
9
13
  ];
10
- /** Newest first, with the id breaking ties between saves in the same instant. */ const NEWEST_FIRST = ["-updatedAt", "-id"];
11
- /** The document or global as the key's user sees it. */ const readLive = async (scope, read, options) => {
14
+ const NEWEST_FIRST = ["-updatedAt", "-id"];
15
+ /** The document or global as the key's user sees it, without `admin.hidden` fields. */ const readLive = async (scope, read, options) => {
12
16
  const shared = {
13
17
  depth: read.depth,
14
18
  draft: options.draft,
15
19
  overrideAccess: false,
16
- req: scope.req,
20
+ req: mcpxReadRequest(scope),
17
21
  ...options.select === void 0 ? {} : { select: options.select },
18
22
  ...read.locale === void 0 ? {} : { locale: read.locale }
19
23
  };
20
- return await (read.target.kind === "collection" ? scope.req.payload.findByID({
24
+ const doc = await (read.target.kind === "collection" ? scope.req.payload.findByID({
21
25
  ...shared,
22
26
  collection: read.target.slug,
23
- id: read.id
27
+ id: read.target.id
24
28
  }) : scope.req.payload.findGlobal({
25
29
  ...shared,
26
30
  slug: read.target.slug
27
31
  }));
32
+ return stripAdminHidden(scope.req.payload.config, read.target, doc);
28
33
  };
29
- /**
30
- * Payload checks only `readVersions` on a version read, never `read`, so the
31
- * document itself is read first and throws when the key's user may not see it.
32
- */ const assertReadable = async (scope, read) => {
34
+ const withoutHidden = (scope, read, stored) => ({
35
+ ...stored,
36
+ version: stripAdminHidden(scope.req.payload.config, read.target, stored.version)
37
+ });
38
+ const assertReadable = async (scope, read) => {
33
39
  await readLive(scope, {
34
40
  ...read,
35
41
  depth: 0
@@ -56,7 +62,7 @@ import { Pointer, createPatch } from "rfc6902";
56
62
  depth: read.depth,
57
63
  disableErrors: true,
58
64
  overrideAccess: false,
59
- req: scope.req,
65
+ req: mcpxReadRequest(scope),
60
66
  ...read.locale === void 0 ? {} : { locale: read.locale }
61
67
  };
62
68
  const version = await (read.target.kind === "collection" ? payload.findVersionByID({
@@ -67,13 +73,13 @@ import { Pointer, createPatch } from "rfc6902";
67
73
  slug: read.target.slug
68
74
  }));
69
75
  if (!version) return null;
70
- if (read.id !== void 0 && !isVersionOf(version, read.id)) return null;
71
- return version;
76
+ if (read.target.kind === "collection" && !isVersionOf(version, read.target.id)) return null;
77
+ return withoutHidden(scope, read, version);
72
78
  };
73
79
  /** One page of a document's or global's history, newest first. */ const queryVersions = async (scope, read, options) => {
74
80
  await assertReadable(scope, read);
75
81
  const where = {
76
- ...read.id === void 0 ? {} : { parent: { equals: read.id } },
82
+ ...read.target.kind === "collection" ? { parent: { equals: read.target.id } } : {},
77
83
  ...options.status === void 0 ? {} : { "version._status": { equals: options.status } }
78
84
  };
79
85
  const shared = {
@@ -81,7 +87,7 @@ import { Pointer, createPatch } from "rfc6902";
81
87
  limit: options.limit,
82
88
  sort: NEWEST_FIRST,
83
89
  overrideAccess: false,
84
- req: scope.req,
90
+ req: mcpxReadRequest(scope),
85
91
  where,
86
92
  ...options.page === void 0 ? {} : { page: options.page },
87
93
  ...read.locale === void 0 ? {} : { locale: read.locale }
@@ -95,10 +101,12 @@ import { Pointer, createPatch } from "rfc6902";
95
101
  }));
96
102
  };
97
103
  /** The newest version whose status is published, or `null` if none is. */ const loadPublished = async (scope, read) => {
98
- return (await queryVersions(scope, read, {
104
+ if (!hasDraftsEnabled(read.target.config)) return null;
105
+ const [newest] = (await queryVersions(scope, read, {
99
106
  status: "published",
100
107
  limit: 1
101
- })).docs[0] ?? null;
108
+ })).docs;
109
+ return newest ? withoutHidden(scope, read, newest) : null;
102
110
  };
103
111
  const strip = (doc) => Object.fromEntries(Object.entries(doc).filter(([key]) => !NOISE.includes(key)));
104
112
  /**
package/dist/types.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DocumentId } from "./entity.mjs";
1
2
  import { CollectionConfig, CollectionSlug, GlobalSlug, PayloadRequest, TypedUser } from "payload";
2
3
  import { z } from "zod";
3
4
  import { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
@@ -17,32 +18,41 @@ declare module "payload" {
17
18
  * - `false`: no write tool touches it.
18
19
  * - `"draft"`: writes land as drafts and nothing MCP does changes what the
19
20
  * public sees. Requires `versions.drafts`.
20
- * - `"live"`: MCP may change live content. On an entity with drafts that means
21
- * `publishDocument` is exposed; on one without, where there is no draft to
22
- * land on, it means the write itself is permitted and lands live.
21
+ * - `"live"`: MCP may change live content. On an entity with drafts that
22
+ * exposes `publishDocument`. On one without, there is no draft, so the write
23
+ * itself is permitted and lands live.
23
24
  */
24
25
  export type McpxWriteMode = "draft" | "live" | false;
25
- /** A key can only enable what the config exposes here. */
26
+ /**
27
+ * What an entity exposes. The config names the entities that are reachable and
28
+ * only takes capabilities away; a key's checkboxes decide per key. `true` is
29
+ * shorthand for `{}`, which exposes everything the entity supports.
30
+ */
26
31
  export interface McpxCollectionOptions {
27
32
  /** Expose `describeSchema`, `findDocuments`, `getDocument`. Default `true`. */
28
33
  read?: boolean;
29
34
  /**
30
35
  * Expose `patchDocument`, `validateDocument` and, unless this is an upload
31
- * collection, `createDocument`, and how far those writes reach. Default
32
- * `false`.
36
+ * collection, `createDocument`. Without drafts a write changes live
37
+ * content. Default `true`.
33
38
  */
34
- write?: McpxWriteMode;
35
- }
36
- /** A singleton, so neither `findDocuments` nor `createDocument` reaches one. */
37
- export interface McpxGlobalOptions {
38
- /** Expose `describeSchema` and `getDocument`. Default `true`. */
39
- read?: boolean;
39
+ write?: boolean;
40
+ /**
41
+ * Expose `publishDocument`, which promotes a draft to live content. Needs
42
+ * `versions.drafts` and `write`. Default `true` where the entity has drafts.
43
+ * `false` keeps writes as drafts and is refused where there are none.
44
+ */
45
+ publish?: boolean;
40
46
  /**
41
- * Expose `patchDocument` and `validateDocument`, and how far those writes
42
- * reach. Default `false`.
47
+ * Expose `findVersions` and the `versionId` and `diffFrom` arguments of
48
+ * `getDocument` to keys that may read the entity. Default `false`: version
49
+ * history has no checkbox of its own and follows the key's read, so the
50
+ * config opts in. `true` needs Payload `versions` and `read`.
43
51
  */
44
- write?: McpxWriteMode;
52
+ versions?: boolean;
45
53
  }
54
+ /** The same options. A singleton, so neither `findDocuments` nor `createDocument` reaches one. */
55
+ export type McpxGlobalOptions = McpxCollectionOptions;
46
56
  export type McpxToolExtra = RequestHandlerExtra<ServerRequest, ServerNotification>;
47
57
  /** What the config exposes, before an API key's checkboxes narrow it. */
48
58
  export interface McpxExposedEntity {
@@ -50,26 +60,33 @@ export interface McpxExposedEntity {
50
60
  read: boolean;
51
61
  write: McpxWriteMode;
52
62
  hasDrafts: boolean;
53
- /** Payload keeps a version history, with or without drafts. */
63
+ /**
64
+ * Payload keeps a version history, with or without drafts, and the config
65
+ * exposes it. Says nothing about drafts; see `hasDrafts`.
66
+ */
54
67
  hasVersions: boolean;
55
68
  /** An upload document is a file, and no tool here can supply one. */
56
69
  isUpload: boolean;
57
70
  /** Name of the capability group on the key document. */
58
71
  fieldName: string;
59
72
  }
73
+ /** The slugs a key may read, write and publish. */
74
+ export interface McpxScopeSlugs {
75
+ readable: string[];
76
+ writable: string[];
77
+ publishable: string[];
78
+ }
60
79
  /** What a tool knows about the current request. */
61
80
  export interface McpxToolScope {
62
81
  req: PayloadRequest;
63
82
  capabilities: McpxResolvedCapabilities;
64
- readable: string[];
65
- writable: string[];
66
- publishable: string[];
67
- readableGlobals: string[];
68
- writableGlobals: string[];
69
- publishableGlobals: string[];
83
+ collections: McpxScopeSlugs;
84
+ globals: McpxScopeSlugs;
70
85
  /** `null` when localization is off. */
71
- locales: null | string[];
72
- defaultLocale: null | string;
86
+ localization: null | {
87
+ locales: string[];
88
+ defaultLocale: string;
89
+ };
73
90
  limits: {
74
91
  maxLimit: number;
75
92
  maxDepth: number;
@@ -87,7 +104,7 @@ export interface McpxToolScope {
87
104
  export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.infer<z.ZodObject<Shape>>> {
88
105
  /** camelCase, unique, not one of the builtin tool names. */
89
106
  name: string;
90
- /** Built per request so it can state what this key's writes actually do. */
107
+ /** Built per request so it can state what this key's writes do. */
91
108
  description: string | ((scope: McpxToolScope) => string);
92
109
  annotations?: ToolAnnotations;
93
110
  /**
@@ -99,7 +116,7 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
99
116
  /**
100
117
  * Built per request so enums can be narrowed to what the key may touch.
101
118
  * Registered strictly either way: an unknown argument is rejected by name
102
- * rather than stripped.
119
+ * instead of stripped.
103
120
  */
104
121
  inputSchema?: Shape | ((scope: McpxToolScope) => z.ZodRawShape);
105
122
  handler(ctx: {
@@ -112,25 +129,10 @@ export interface McpxTool<Shape extends z.ZodRawShape = z.ZodRawShape, Args = z.
112
129
  }
113
130
  /** Argument type erased, so a registry can hold tools of differing shapes. */
114
131
  export type McpxAnyTool = McpxTool<z.ZodRawShape, never>;
115
- /** Fixed shape; arguments inferred from it. */
116
- export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
117
- inputSchema?: Shape;
118
- }): McpxTool<Shape>;
119
- /** Per-request shape returned as an object literal; arguments inferred from it. */
120
- export declare function defineMcpxTool<Shape extends z.ZodRawShape>(tool: McpxTool<Shape> & {
121
- inputSchema: (scope: McpxToolScope) => Shape;
122
- }): McpxAnyTool;
123
- /**
124
- * Per-request shape built from helpers that erase to `z.ZodRawShape`, as the
125
- * builtins do. Nothing to infer from, so state the arguments instead.
126
- */
127
- export declare function defineMcpxTool<Args>(tool: McpxTool<z.ZodRawShape, Args> & {
128
- inputSchema: (scope: McpxToolScope) => z.ZodRawShape;
129
- }): McpxAnyTool;
130
132
  export interface McpxAuthResult {
131
133
  /** Must carry `collection`. */
132
134
  user: TypedUser;
133
- apiKeyId: number | string;
135
+ apiKeyId: DocumentId;
134
136
  /** The `capabilities` group as stored on the key document. */
135
137
  capabilities: unknown;
136
138
  }
@@ -141,9 +143,9 @@ export interface McpxAuthResult {
141
143
  * to `Record<string, unknown>`, which interfaces do not satisfy.
142
144
  */
143
145
  export type McpxPluginOptions = {
144
- /** Allow-list of collections. `true` is shorthand for `{ read: true }`. */
146
+ /** Allow-list of collections. `true` is shorthand for `{}`. */
145
147
  collections: Partial<Record<CollectionSlug, McpxCollectionOptions | true>>;
146
- /** Allow-list of globals. `true` is shorthand for `{ read: true }`. */
148
+ /** Allow-list of globals. `true` is shorthand for `{}`. */
147
149
  globals?: Partial<Record<GlobalSlug, McpxGlobalOptions | true>>;
148
150
  /** Collection the keys act as. Default `config.admin.user`, then `users`. */
149
151
  userCollection?: CollectionSlug;
@@ -170,7 +172,11 @@ export type McpxPluginOptions = {
170
172
  };
171
173
  tools?: McpxAnyTool[];
172
174
  auth?: {
173
- /** Replace or wrap the default key resolution. Return `null` for 401. */
175
+ /**
176
+ * Replace or wrap the default key resolution. Return `null` for 401. A
177
+ * result whose user has no `id` or whose `collection` is not the user
178
+ * collection, or that has no `apiKeyId`, is also answered with 401.
179
+ */
174
180
  resolve?: (args: {
175
181
  req: PayloadRequest;
176
182
  resolveDefault: () => Promise<McpxAuthResult | null>;
@@ -185,10 +191,10 @@ export type McpxPluginOptions = {
185
191
  export interface McpxCollectionCapabilities {
186
192
  read: boolean;
187
193
  write: boolean;
188
- /** Only ever true where the config sets `write: "live"` and drafts exist. */
194
+ /** Only ever true where the config lets writes publish and drafts exist. */
189
195
  publish: boolean;
190
196
  }
191
- /** In force for one request: plugin config AND key checkboxes. */
197
+ /** In force for one request: the plugin config and the key checkboxes together. */
192
198
  export interface McpxResolvedCapabilities {
193
199
  collections: Record<string, McpxCollectionCapabilities>;
194
200
  globals: Record<string, McpxCollectionCapabilities>;
@@ -196,15 +202,7 @@ export interface McpxResolvedCapabilities {
196
202
  }
197
203
  /** Stamped on `req.context.mcpx`; see {@link isMcpxRequest}. */
198
204
  export interface McpxRequestContext {
199
- apiKeyId: number | string;
205
+ apiKeyId: DocumentId;
200
206
  capabilities: McpxResolvedCapabilities;
201
207
  }
202
- /** One reason a human could not publish the draft as it stands. */
203
- export interface PublishBlocker {
204
- /** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
205
- field?: string;
206
- message: string;
207
- /** JSON Pointer to the offending value, e.g. "/layout/2/title". */
208
- path: string;
209
- }
210
208
  //#endregion