@abinnovision/payloadcms-mcpx 1.0.0-beta.8 → 1.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 (60) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +331 -194
  3. package/dist/api-keys/collection.mjs +3 -3
  4. package/dist/api-keys/fields.mjs +56 -5
  5. package/dist/api-keys/setup-guide.mjs +56 -0
  6. package/dist/auth/resolve.mjs +5 -7
  7. package/dist/capabilities.mjs +23 -4
  8. package/dist/client/index.d.mts +2 -0
  9. package/dist/client/index.mjs +2 -0
  10. package/dist/client/setup-guide.d.mts +14 -0
  11. package/dist/client/setup-guide.mjs +87 -0
  12. package/dist/endpoint/{result.mjs → errors.mjs} +4 -23
  13. package/dist/endpoint/handler.mjs +11 -5
  14. package/dist/endpoint/index.mjs +4 -0
  15. package/dist/endpoint/server.mjs +18 -26
  16. package/dist/i18n.mjs +4 -15
  17. package/dist/index.d.mts +4 -4
  18. package/dist/index.mjs +4 -3
  19. package/dist/options.mjs +31 -26
  20. package/dist/plugin.mjs +1 -0
  21. package/dist/{write/draft-guard.d.mts → request.d.mts} +2 -2
  22. package/dist/request.mjs +8 -0
  23. package/dist/result.d.mts +11 -0
  24. package/dist/result.mjs +20 -0
  25. package/dist/schema/describe.mjs +3 -15
  26. package/dist/schema/index.mjs +8 -0
  27. package/dist/schema/lexical-pointer.mjs +125 -0
  28. package/dist/schema/lexical.mjs +195 -27
  29. package/dist/schema/outline.mjs +67 -0
  30. package/dist/schema/pointer.mjs +77 -30
  31. package/dist/schema/shape.mjs +133 -51
  32. package/dist/schema/walk.mjs +44 -64
  33. package/dist/tools/{index.mjs → builtin.mjs} +8 -5
  34. package/dist/tools/create-document.mjs +34 -15
  35. package/dist/tools/describe-schema.mjs +21 -7
  36. package/dist/tools/find-documents.mjs +13 -6
  37. package/dist/tools/get-document.mjs +45 -11
  38. package/dist/tools/list-capabilities.mjs +19 -9
  39. package/dist/tools/names.mjs +2 -1
  40. package/dist/tools/patch-document.mjs +32 -21
  41. package/dist/tools/publish-document.mjs +79 -0
  42. package/dist/tools/shared.mjs +84 -32
  43. package/dist/tools/target.mjs +7 -11
  44. package/dist/tools/validate-document.mjs +20 -12
  45. package/dist/types.d.mts +115 -42
  46. package/dist/types.mjs +3 -4
  47. package/dist/version.mjs +1 -1
  48. package/dist/write/draft-guard.mjs +47 -44
  49. package/dist/write/patch.mjs +174 -92
  50. package/dist/write/publish-blockers.mjs +13 -12
  51. package/dist/write/publish-intent.mjs +17 -0
  52. package/dist/write/transaction.mjs +8 -3
  53. package/package.json +24 -9
  54. package/dist/i18n.d.mts +0 -1
  55. package/dist/options.d.mts +0 -2
  56. package/dist/schema/lexical.d.mts +0 -1
  57. package/dist/schema/walk.d.mts +0 -3
  58. package/dist/tools/target.d.mts +0 -3
  59. package/dist/tools/types.d.mts +0 -5
  60. package/dist/write/publish-blockers.d.mts +0 -15
@@ -1,49 +1,53 @@
1
- import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, splitPath } from "../schema/walk.mjs";
2
- import { validateWriteValue } from "../schema/shape.mjs";
1
+ import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeAddressableFields, findBlocksField, isIndexSegment, joinPath, splitPath } from "../schema/walk.mjs";
3
2
  import { resolveDataPointer } from "../schema/pointer.mjs";
3
+ import { EMPTY_ROOT, validateWriteValue } from "../schema/shape.mjs";
4
+ import "../schema/index.mjs";
4
5
  import { z } from "zod";
5
6
  import { Pointer, applyPatch } from "rfc6902";
6
7
  //#region src/write/patch.ts
7
- /**
8
- * One RFC 6902 operation as accepted by `patchDocument`.
9
- */ const PATCH_OPERATION_SCHEMA = z.object({
10
- from: z.string().regex(JSON_POINTER_PATTERN).optional(),
11
- op: z.enum([
12
- "add",
13
- "copy",
14
- "move",
15
- "remove",
16
- "replace",
17
- "test"
18
- ]),
19
- path: z.string().regex(JSON_POINTER_PATTERN),
20
- value: z.unknown().optional()
21
- }).describe("An RFC 6902 operation.");
22
- /**
23
- * Whether a pointer touches a field Payload maintains.
24
- */ const isReservedPointer = (pointer) => pointer.split("/").slice(1).some((segment) => RESERVED_FIELD_NAMES.has(segment));
25
- /**
26
- * The pointer an operation removes a value from, if it removes one at all.
27
- */ const droppedPointer = (operation) => {
8
+ const POINTER = z.string().regex(JSON_POINTER_PATTERN);
9
+ /** Discriminated on `op`, so an operation carries only its own members. */ const PATCH_OPERATION_SCHEMA = z.discriminatedUnion("op", [
10
+ z.strictObject({
11
+ op: z.literal("add"),
12
+ path: POINTER,
13
+ value: z.unknown()
14
+ }),
15
+ z.strictObject({
16
+ op: z.literal("remove"),
17
+ path: POINTER
18
+ }),
19
+ z.strictObject({
20
+ op: z.literal("replace"),
21
+ path: POINTER,
22
+ value: z.unknown()
23
+ }),
24
+ z.strictObject({
25
+ from: POINTER,
26
+ op: z.literal("move"),
27
+ path: POINTER
28
+ }),
29
+ z.strictObject({
30
+ from: POINTER,
31
+ op: z.literal("copy"),
32
+ path: POINTER
33
+ }),
34
+ z.strictObject({
35
+ op: z.literal("test"),
36
+ path: POINTER,
37
+ value: z.unknown()
38
+ })
39
+ ]).describe("An RFC 6902 operation.");
40
+ const isReservedPointer = (pointer) => pointer.split("/").slice(1).some((segment) => RESERVED_FIELD_NAMES.has(segment));
41
+ const droppedPointer = (operation) => {
28
42
  if (operation.op === "remove") return operation.path;
29
43
  return operation.op === "move" ? operation.from : void 0;
30
44
  };
31
- /**
32
- * Whether a pointer addresses a list element rather than a field.
33
- */ const isElementPointer = (pointer) => {
34
- const last = pointer.split("/").pop() ?? "";
35
- return last === "-" || /^\d+$/.test(last);
45
+ const isElementPointer = (pointer) => {
46
+ return isIndexSegment(pointer.split("/").pop() ?? "");
36
47
  };
37
48
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
38
- /**
39
- * A Lexical editor state. Its nodes manage their own ids, so it is never
40
- * descended into.
41
- */ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
42
- /**
43
- * Visits every row in a value: a plain object that carries `blockType` or
44
- * sits directly inside an array. Rich text states manage their own nodes and
45
- * are never descended into.
46
- */ const walkRows = (value, visit, isRow = false) => {
49
+ /** Its nodes manage their own ids, so it is never descended into. */ const isRichTextState = (value) => isPlainObject(value["root"]) && Array.isArray(value["root"]["children"]);
50
+ /** A row is a plain object carrying `blockType`, or one sitting in an array. */ const walkRows = (value, visit, isRow = false) => {
47
51
  if (Array.isArray(value)) {
48
52
  for (const entry of value) walkRows(entry, visit, true);
49
53
  return;
@@ -76,10 +80,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
76
80
  delete row["id"];
77
81
  });
78
82
  };
79
- /**
80
- * Drops every row id from a copy of `value`. Used on create, where no stored
81
- * row exists and any incoming id is client-invented.
82
- */ const stripRowIds = (value) => {
83
+ /** For create, where no stored row exists and any incoming id is invented. */ const stripRowIds = (value) => {
83
84
  const next = structuredClone(value);
84
85
  walkRows(next, (row) => {
85
86
  delete row["id"];
@@ -87,71 +88,155 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
87
88
  return next;
88
89
  };
89
90
  /**
90
- * Applies a patch to a deep copy of the document, so a failing operation
91
- * leaves the original untouched and nothing partial is ever written.
92
- */ const applyPatchToCopy = (doc, patches) => {
93
- const next = structuredClone(doc);
94
- const prepared = patches.map((operation) => {
95
- const cloned = "value" in operation ? {
96
- ...operation,
97
- value: structuredClone(operation.value)
98
- } : operation;
99
- if (cloned.op === "replace" && !isElementPointer(cloned.path) && Pointer.fromJSON(cloned.path).get(next) === void 0) return {
100
- ...cloned,
101
- op: "add"
102
- };
103
- return cloned;
104
- });
105
- const problems = applyPatch(next, prepared).flatMap((error, index) => error ? [`patches[${String(index)}]: ${error.message}`] : []);
106
- if (problems.length > 0) return { problems };
107
- reconcileRowIds(next, doc);
108
- return { next };
91
+ * The operation as it is applied: values are cloned so the written document
92
+ * never shares references with the caller's operations, and a `replace` of a
93
+ * field the target locale has no value for becomes an `add`, which is what
94
+ * RFC 6902 requires when nothing is there to replace.
95
+ */ const prepare = (operation, doc) => {
96
+ const cloned = "value" in operation ? {
97
+ ...operation,
98
+ value: structuredClone(operation.value)
99
+ } : operation;
100
+ return cloned.op === "replace" && !isElementPointer(cloned.path) && Pointer.fromJSON(cloned.path).get(doc) === void 0 ? {
101
+ ...cloned,
102
+ op: "add"
103
+ } : cloned;
104
+ };
105
+ /** The one it carries, or the one it takes from `from`. `remove` writes nothing. */ const effectiveValue = (operation, doc) => {
106
+ if ("value" in operation) return operation.value;
107
+ return "from" in operation ? Pointer.fromJSON(operation.from).get(doc) : void 0;
108
+ };
109
+ /** A pointer stopping short addresses a subtree; the fields beneath decide. */ const resolvesReadOnly = (resolution) => {
110
+ if (resolution.readOnly) return true;
111
+ if (resolution.descriptor) return resolution.descriptor.readOnly === true;
112
+ const below = describeAddressableFields(resolution.fields).filter((descriptor) => resolution.prefix.every((part, offset) => part === splitPath(descriptor.path)[offset]));
113
+ return below.length > 0 && below.every((descriptor) => descriptor.readOnly);
114
+ };
115
+ /** An element has no descriptor, so its field is read one segment up. */ const isReadOnlyPointer = (config, target) => resolvesReadOnly(resolveDataPointer(config, {
116
+ doc: target.doc,
117
+ pointer: isElementPointer(target.pointer) ? joinPath(splitPath(target.pointer).slice(0, -1)) : target.pointer,
118
+ ref: target.ref
119
+ }));
120
+ /** Unresolvable pointers say nothing here; the caller reports them anyway. */ const resolutionAt = (config, target) => {
121
+ try {
122
+ return resolveDataPointer(config, {
123
+ doc: target.doc,
124
+ pointer: target.pointer,
125
+ ref: target.ref
126
+ });
127
+ } catch {
128
+ return;
129
+ }
109
130
  };
110
131
  /**
111
- * Checks every operation against the schema before any is applied.
112
- *
113
- * A partially applied batch is worse than a refused one, so this returns all
114
- * problems and the caller applies nothing unless the list is empty.
115
- */ const findPatchProblems = (config, target) => target.patches.flatMap((operation, index) => {
116
- const at = `patches[${String(index)}]`;
117
- const pointers = [operation.path, ..."from" in operation && operation.from ? [operation.from] : []];
118
- if (pointers.includes("")) return [`${at}: an empty pointer addresses the whole document. Address a field instead.`];
132
+ * Removing a field does nothing, but removing part of an editor state does
133
+ * something, and something worse: a node written without the properties its
134
+ * class hydrates from throws when the admin editor opens it. Both are refused,
135
+ * and only the reason differs.
136
+ */ const droppedFieldProblem = (config, target) => {
137
+ const resolution = resolutionAt(config, target);
138
+ const lexical = resolution?.lexical;
139
+ if (lexical?.kind === "property") return `"${target.pointer}" is a node property, not a list element. A "${lexical.nodeType}" node needs it, so replace it rather than removing it.`;
140
+ if (lexical ?? resolution?.inLexical) return `"${target.pointer}" sits inside an editor state, which is written whole, so removing it would take effect and leave a node the admin editor cannot open. Replace it instead, or remove the node that holds it.`;
141
+ return `"${target.pointer}" is a field, not a list element, and removing it would do nothing. The patched document is written whole, and Payload keeps any field absent from a write rather than clearing it. Use "replace" with null to clear a field, or with [] to empty a list.`;
142
+ };
143
+ /**
144
+ * Dropping the only node under a root empties the state, which Lexical refuses
145
+ * to hydrate. Checked here rather than on the written value, because a
146
+ * `remove` carries none.
147
+ */ const emptiesTheRoot = (config, target) => {
148
+ const list = joinPath(splitPath(target.pointer).slice(0, -1));
149
+ const owner = resolutionAt(config, {
150
+ ...target,
151
+ pointer: list
152
+ })?.lexical;
153
+ if (owner?.kind !== "nodes" || owner.isRoot !== true) return false;
154
+ const nodes = Pointer.fromJSON(list).get(target.doc);
155
+ return Array.isArray(nodes) && nodes.length === 1;
156
+ };
157
+ /**
158
+ * Checked against the document as it stands when this operation runs: both
159
+ * pointers must resolve, the written value must pass write validation, and what
160
+ * it drops must not sit in a read-only field.
161
+ */ const findOperationProblems = (config, target) => {
162
+ const { doc, operation, ref } = target;
163
+ const pointers = [operation.path, ..."from" in operation ? [operation.from] : []];
164
+ if (pointers.includes("")) return ["an empty pointer addresses the whole document. Address a field instead."];
119
165
  const reserved = pointers.find(isReservedPointer);
120
- if (reserved !== void 0) return [`${at}: "${reserved}" addresses a field Payload maintains. Drafts are the only thing this tool writes, and id, _status, createdAt and updatedAt are not writable.`];
166
+ if (reserved !== void 0) return [`"${reserved}" addresses a field Payload maintains. This tool only ever writes drafts, and id, _status, createdAt and updatedAt are not writable; use publishDocument to publish.`];
121
167
  const dropped = droppedPointer(operation);
122
- if (dropped !== void 0 && !isElementPointer(dropped)) return [`${at}: "${dropped}" is a field, not a list element, and removing it would do nothing. The patched document is written whole, and Payload keeps any field absent from a write rather than clearing it. Use "replace" with null to clear a field, or with [] to empty a list.`];
123
- const value = "value" in operation ? operation.value : void 0;
168
+ if (dropped !== void 0 && !isElementPointer(dropped)) return [droppedFieldProblem(config, {
169
+ doc,
170
+ pointer: dropped,
171
+ ref
172
+ })];
124
173
  try {
125
- const moved = "from" in operation && operation.from ? Pointer.fromJSON(operation.from).get(target.doc) : void 0;
174
+ const value = effectiveValue(operation, doc);
175
+ if (value !== void 0 && operation.op !== "test" && isReadOnlyPointer(config, {
176
+ doc,
177
+ pointer: operation.path,
178
+ ref
179
+ })) return [`"${operation.path}" is read-only and cannot be written.`];
180
+ if (dropped !== void 0 && isReadOnlyPointer(config, {
181
+ doc,
182
+ pointer: dropped,
183
+ ref
184
+ })) return [`"${dropped}" sits in a read-only field and cannot be removed.`];
185
+ if (dropped !== void 0 && emptiesTheRoot(config, {
186
+ doc,
187
+ pointer: dropped,
188
+ ref
189
+ })) return [`"${dropped}": ${EMPTY_ROOT}`];
126
190
  for (const pointer of pointers) {
127
191
  const resolution = resolveDataPointer(config, {
128
- addedValue: value ?? moved,
129
- doc: target.doc,
192
+ addedValue: value,
193
+ doc,
130
194
  pointer,
131
- ref: target.ref
195
+ ref
132
196
  });
133
- if (pointer === operation.path && value !== void 0) return validateWriteValue(config, {
134
- pointer,
135
- resolution
136
- }, value).map((problem) => `${at}: ${problem}`);
197
+ if (pointer === operation.path && value !== void 0) {
198
+ const problems = validateWriteValue(config, {
199
+ pointer,
200
+ resolution
201
+ }, value);
202
+ if (problems.length > 0) return problems;
203
+ }
137
204
  }
138
205
  return [];
139
206
  } catch (error) {
140
- return [`${at}: ${error instanceof Error ? error.message : "invalid"}`];
207
+ return [error instanceof Error ? error.message : "invalid"];
141
208
  }
142
- });
209
+ };
143
210
  /**
144
- * Keys Payload manages on a row that travel back into the write unchanged.
145
- */ const ROW_KEYS = /* @__PURE__ */ new Set([
211
+ * One evolving copy, so an operation depending on an earlier one resolves
212
+ * against the shape it actually modifies, and a failure leaves the original
213
+ * untouched. The caller writes nothing unless the whole batch came back
214
+ * applied, so a partial batch is never persisted.
215
+ */ const applyPatchOperations = (config, target) => {
216
+ const next = structuredClone(target.doc);
217
+ for (const [index, operation] of target.patches.entries()) {
218
+ const at = `patches[${String(index)}]`;
219
+ const problems = findOperationProblems(config, {
220
+ doc: next,
221
+ operation,
222
+ ref: target.ref
223
+ });
224
+ if (problems.length > 0) return { problems: problems.map((problem) => `${at}: ${problem}`) };
225
+ const [error] = applyPatch(next, [prepare(operation, next)]);
226
+ if (error) return { problems: [`${at}: ${error.message}`] };
227
+ }
228
+ reconcileRowIds(next, target.doc);
229
+ return { next };
230
+ };
231
+ const ROW_KEYS = /* @__PURE__ */ new Set([
146
232
  "blockName",
147
233
  "blockType",
148
234
  "id"
149
235
  ]);
150
236
  /**
151
- * Picks the keys the schema walker describes out of `value`, descending into
152
- * groups, named tabs, arrays and blocks. Everything Payload maintains or
153
- * derives (`_status`, timestamps, join and virtual fields, upload base fields)
154
- * is left out, so the write-back carries only what a client could have set.
237
+ * Everything Payload maintains or derives (`_status`, timestamps, join and
238
+ * virtual fields, upload base fields) is left out, so the write-back carries
239
+ * only what a client could have set.
155
240
  */ const pickDescribed = (config, value, at) => {
156
241
  const { fields, prefix, isRow } = at;
157
242
  const relative = describeAddressableFields(fields).flatMap((descriptor) => {
@@ -205,10 +290,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
205
290
  }
206
291
  return result;
207
292
  };
208
- /**
209
- * The data handed to `payload.update` after a patch: the patched document
210
- * reduced to the fields the client may write, plus row identity keys.
211
- */ const buildWriteData = (config, target, doc) => {
293
+ /** The patched document reduced to writable fields, plus row identity keys. */ const buildWriteData = (config, target, doc) => {
212
294
  return pickDescribed(config, doc, {
213
295
  fields: target.flattenedFields,
214
296
  prefix: [],
@@ -216,4 +298,4 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
216
298
  });
217
299
  };
218
300
  //#endregion
219
- export { PATCH_OPERATION_SCHEMA, applyPatchToCopy, buildWriteData, droppedPointer, findPatchProblems, isElementPointer, isReservedPointer, stripRowIds };
301
+ export { PATCH_OPERATION_SCHEMA, applyPatchOperations, buildWriteData, droppedPointer, isElementPointer, isReservedPointer, stripRowIds };
@@ -1,23 +1,21 @@
1
1
  import { pointerFromPayloadPath } from "../schema/walk.mjs";
2
+ import "../schema/index.mjs";
2
3
  import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payload";
3
4
  //#region src/write/publish-blockers.ts
4
5
  /**
5
6
  * Runs Payload's own field validation over a draft without saving anything.
6
7
  *
7
- * Draft saves skip validation unless `versions.drafts.validate` is set, so an
8
- * agent building a document incrementally gets no signal until a human presses
9
- * Publish. This is the same traversal a real save runs, exported from
10
- * `payload`, called with `skipValidation: false` so it collects into `errors`
11
- * instead of throwing. The `beforeValidate` pass runs first because some field
12
- * hooks (Lexical's) prepare state in `context` that their `beforeChange`
13
- * counterpart depends on.
8
+ * The same traversal a real save runs, exported from `payload` and called with
9
+ * `skipValidation: false` so it collects into `errors` instead of throwing. The
10
+ * `beforeValidate` pass runs first because some field hooks (Lexical's) prepare
11
+ * state in `context` that their `beforeChange` counterpart depends on.
14
12
  *
15
13
  * Nothing is written: `data` is a copy, the context is a scratch copy and the
16
14
  * locale merge actions are discarded. `overrideAccess` is true because the
17
15
  * question is "could this be published", not "may this client write it".
18
16
  *
19
- * Limits: only the locale the doc was read in is checked, and field-level
20
- * `beforeChange` hooks run again, which is safe only for pure ones.
17
+ * `unavailable` marks a traversal that threw, which is not the same answer as a
18
+ * document with nothing wrong with it.
21
19
  */ const collectPublishBlockers = async (req, target) => {
22
20
  const { doc, entity } = target;
23
21
  const id = doc["id"];
@@ -62,13 +60,16 @@ import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payloa
62
60
  });
63
61
  } catch (error) {
64
62
  req.payload.logger.warn(`[payloadcms-mcpx] Could not validate the ${entity.slug} draft: ${error instanceof Error ? error.message : "unknown error"}`);
65
- return [];
63
+ return {
64
+ blockers: [],
65
+ unavailable: true
66
+ };
66
67
  }
67
- return errors.map((error) => ({
68
+ return { blockers: errors.map((error) => ({
68
69
  message: error.message,
69
70
  path: pointerFromPayloadPath(error.path),
70
71
  ...typeof error.label === "string" ? { field: error.label } : {}
71
- }));
72
+ })) };
72
73
  };
73
74
  //#endregion
74
75
  export { collectPublishBlockers };
@@ -0,0 +1,17 @@
1
+ import { randomUUID } from "node:crypto";
2
+ //#region src/write/publish-intent.ts
3
+ const PUBLISH_INTENT = "__mcpxPublishIntent";
4
+ const TOKEN = randomUUID();
5
+ const withPublishIntent = (data) => ({
6
+ ...data,
7
+ [PUBLISH_INTENT]: TOKEN
8
+ });
9
+ const carries = (data) => typeof data === "object" && data !== null && data[PUBLISH_INTENT] === TOKEN;
10
+ const hasPublishIntent = (data) => carries(data);
11
+ /** Asked by the last hook that needs it, so it takes the marker off. */ const takePublishIntent = (data) => {
12
+ if (!carries(data)) return false;
13
+ delete data[PUBLISH_INTENT];
14
+ return true;
15
+ };
16
+ //#endregion
17
+ export { hasPublishIntent, takePublishIntent, withPublishIntent };
@@ -1,9 +1,14 @@
1
1
  import { commitTransaction, initTransaction, killTransaction } from "payload";
2
2
  //#region src/write/transaction.ts
3
3
  /**
4
- * Runs `fn` inside one database transaction on `req`, so a read followed by a
5
- * write cannot interleave with another writer. Adapters without transaction
6
- * support, or a request that already owns one, run `fn` as is.
4
+ * Adapters without transaction support, or a request that already owns one, run
5
+ * `fn` as is.
6
+ *
7
+ * Atomicity, not isolation: neither SQLite nor Postgres at read committed locks
8
+ * the row on the read, so an `expectedUpdatedAt` check remains best effort. Nor
9
+ * is this safe across the tool calls of one JSON-RPC batch, which share a
10
+ * request: the second caller joins the first's transaction, so one tool's
11
+ * rollback takes the other's work with it.
7
12
  */ const withTransaction = async (req, fn) => {
8
13
  if (!await initTransaction(req)) return await fn();
9
14
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@abinnovision/payloadcms-mcpx",
4
- "version": "1.0.0-beta.8",
4
+ "version": "1.0.0",
5
5
  "description": "Payload CMS plugin exposing a fixed, schema-aware MCP tool surface with draft-only writes and per-API-key capabilities.",
6
6
  "keywords": [
7
7
  "payload",
@@ -26,6 +26,10 @@
26
26
  ".": {
27
27
  "types": "./dist/index.d.mts",
28
28
  "default": "./dist/index.mjs"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/client/index.d.mts",
32
+ "default": "./dist/client/index.mjs"
29
33
  }
30
34
  },
31
35
  "files": [
@@ -35,17 +39,17 @@
35
39
  ],
36
40
  "scripts": {
37
41
  "build": "tsdown",
38
- "format:check": "prettier --check 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
39
- "format:fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
40
- "lint:check": "eslint 'src/**/*.ts' 'test/**/*.ts'",
41
- "lint:fix": "eslint 'src/**/*.ts' 'test/**/*.ts' --fix",
42
+ "format:check": "prettier --check 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
43
+ "format:fix": "prettier --write 'src/**/*.{ts,tsx}' 'test/**/*.ts' '*.{json{,5},md,y{,a}ml}'",
44
+ "lint:check": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts'",
45
+ "lint:fix": "eslint 'src/**/*.{ts,tsx}' 'test/**/*.ts' --fix",
42
46
  "test-integration": "vitest --run --config test/integration/vitest.config.mts",
43
47
  "test-unit": "vitest --run --coverage --config vitest.config.mts",
44
48
  "test-unit:watch": "vitest --config vitest.config.mts",
45
49
  "typecheck": "tsc --noEmit"
46
50
  },
47
51
  "lint-staged": {
48
- "{src,test}/**/*.ts": [
52
+ "{src,test}/**/*.{ts,tsx}": [
49
53
  "eslint --fix",
50
54
  "prettier --write"
51
55
  ],
@@ -63,11 +67,12 @@
63
67
  "@arethetypeswrong/core": "^0.18.5",
64
68
  "@payloadcms/db-sqlite": "3.88.0",
65
69
  "@payloadcms/richtext-lexical": "3.88.0",
70
+ "@payloadcms/ui": "3.88.0",
66
71
  "@swc/core": "^1.16.1",
67
- "@types/node": "^26.2.0",
72
+ "@types/node": "^26.3.0",
68
73
  "@types/react": "^19.2.18",
69
74
  "@vitest/coverage-v8": "^4.1.10",
70
- "eslint": "^10.7.0",
75
+ "eslint": "^10.9.1",
71
76
  "payload": "3.88.0",
72
77
  "prettier": "^3.9.5",
73
78
  "publint": "^0.3.24",
@@ -79,7 +84,17 @@
79
84
  "vitest": "^4.1.10"
80
85
  },
81
86
  "peerDependencies": {
82
- "payload": ">=3.88.0 <4"
87
+ "@payloadcms/ui": ">=3.88.0 <4",
88
+ "payload": ">=3.88.0 <4",
89
+ "react": "^19"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "@payloadcms/ui": {
93
+ "optional": true
94
+ },
95
+ "react": {
96
+ "optional": true
97
+ }
83
98
  },
84
99
  "publishConfig": {
85
100
  "ghpr": true,
package/dist/i18n.d.mts DELETED
@@ -1 +0,0 @@
1
- import { PayloadRequest } from "payload";
@@ -1,2 +0,0 @@
1
- import "./types.mjs";
2
- import "payload";
@@ -1 +0,0 @@
1
- import "payload";
@@ -1,3 +0,0 @@
1
- import "./lexical.mjs";
2
- import "../i18n.mjs";
3
- import "payload";
@@ -1,3 +0,0 @@
1
- import "./types.mjs";
2
- import "../schema/walk.mjs";
3
- import "payload";
@@ -1,5 +0,0 @@
1
- import "../types.mjs";
2
- import "../options.mjs";
3
- import { PayloadRequest } from "payload";
4
- import { z } from "zod";
5
- import { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
@@ -1,15 +0,0 @@
1
- import "../tools/target.mjs";
2
- import { PayloadRequest } from "payload";
3
- //#region src/write/publish-blockers.d.ts
4
- /**
5
- * One reason a human could not publish the draft as it stands.
6
- */
7
- interface PublishBlocker {
8
- /** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
9
- field?: string;
10
- message: string;
11
- /** JSON Pointer to the offending value, e.g. "/layout/2/title". */
12
- path: string;
13
- }
14
- //#endregion
15
- export type { PublishBlocker };