@abinnovision/payloadcms-mcpx 1.0.0-beta.3 → 1.0.0-beta.4

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.
package/README.md CHANGED
@@ -134,19 +134,22 @@ reflects the key: write tools disappear for read-only keys, and every
134
134
 
135
135
  Rules the tools enforce and explain in their own descriptions:
136
136
 
137
- - `describeSchema` paths are dotted and stop at blocks fields, which list the
138
- block slugs they accept; every node carries `next`, the ready-to-use paths
139
- for those blocks (`layout.sections.sectionWrapper`), so pass an entry of
140
- `next` as a `paths` element to descend. A block is described as it exists at
141
- that position.
137
+ - `describeSchema` paths stop at blocks fields, which list the block slugs they
138
+ accept; every node carries `next`, the ready-to-use paths for those blocks
139
+ (`/layout/sections/sectionWrapper`), so pass an entry of `next` as a `paths`
140
+ element to descend. A block is described as it exists at that position.
142
141
  - Field and collection `admin.description` values are included in
143
142
  `describeSchema` and `listCapabilities`, so intent written for the admin
144
143
  panel reaches the client. Strings and locale-keyed records pass through;
145
144
  functions and components are dropped.
146
145
  - Builtin tools reject unknown arguments by name instead of silently ignoring
147
146
  them.
148
- - A schema path becomes a patch pointer by replacing `.` with `/`, adding a
149
- leading `/`, and replacing each `[]` with a 0-based index.
147
+ - Every path this plugin accepts or reports is a JSON Pointer. A schema path
148
+ and a pointer into a document differ only in what stands in an element
149
+ position: a schema path writes `*` for an array element and names a block by
150
+ its slug, where a pointer carries a 0-based index. So `/items/*/title` is
151
+ written at `/items/0/title`, and `/layout/sections/hero` at
152
+ `/layout/sections/0`.
150
153
  - Adding a block requires `blockType` on the value; append with `/-`.
151
154
  - Clearing is `replace` with `null`; a list is emptied with `[]` and refuses
152
155
  `null`. `remove` is only valid on list elements, because Payload keeps
@@ -175,7 +178,8 @@ validated; field `beforeChange` hooks run again during the check, so they must
175
178
  be pure; and the check runs privileged, so blocker paths and messages may name
176
179
  fields the key's user cannot read (values are never included).
177
180
  Collections with `versions.drafts.validate: true` refuse invalid drafts
178
- outright; those failures come back as `validationErrors`.
181
+ outright; those failures come back as `validationErrors`. Both carry pointers,
182
+ restated from the dotted paths Payload reports internally.
179
183
 
180
184
  Writes also report `notApplied`: pointers whose value Payload kept unchanged,
181
185
  which happens when field-level access denies the update.
@@ -1,3 +1,4 @@
1
+ import { pointerFromPayloadPath } from "../schema/walk.mjs";
1
2
  import { APIError, ValidationError } from "payload";
2
3
  //#region src/endpoint/result.ts
3
4
  /**
@@ -41,12 +42,17 @@ import { APIError, ValidationError } from "payload";
41
42
  * Maps an exception thrown by a tool to a result the client can read.
42
43
  *
43
44
  * Payload's public errors keep their message and status; a `ValidationError`
44
- * also surfaces its per-field detail. Anything else is logged and reported as
45
- * an internal error so no stack or driver message leaks to the client.
45
+ * also surfaces its per-field detail, with each field's path restated as a
46
+ * JSON Pointer so it reads like every other path this plugin reports. Anything
47
+ * else is logged and reported as an internal error so no stack or driver
48
+ * message leaks to the client.
46
49
  */ const toToolError = (error, logger) => {
47
50
  if (error instanceof ValidationError) return errorResult(error.message, {
48
51
  status: error.status,
49
- validationErrors: error.data.errors
52
+ validationErrors: error.data.errors.map((entry) => ({
53
+ ...entry,
54
+ path: pointerFromPayloadPath(entry.path)
55
+ }))
50
56
  });
51
57
  if (error instanceof APIError && error.isPublic) return errorResult(error.message, { status: error.status });
52
58
  logger.error({
package/dist/options.mjs CHANGED
@@ -99,7 +99,7 @@ const normalizeLimits = (limits) => {
99
99
  auth: options.auth,
100
100
  serverInfo: {
101
101
  name: options.serverInfo?.name ?? "payloadcms-mcpx",
102
- version: options.serverInfo?.version ?? "1.0.0-beta.3"
102
+ version: options.serverInfo?.version ?? "0.0.0"
103
103
  }
104
104
  };
105
105
  };
@@ -5,21 +5,22 @@ const blocksDescriptors = (fields) => describeFields(fields).filter((descriptor)
5
5
  * Walks a schema path to the field list it addresses.
6
6
  *
7
7
  * A schema path alternates a blocks field's own path with the slug of one of
8
- * the blocks it accepts, so `layout.sections.sectionWrapper.modules.hero`
9
- * reaches `hero` as it exists under `pages` specifically.
8
+ * the blocks it accepts, so `/layout/sections/sectionWrapper/modules/hero`
9
+ * reaches `hero` as it exists under `pages` specifically. The slug sits where
10
+ * a pointer into a document would carry the element's index.
10
11
  */ const fieldsAtSchemaPath = (config, collection, schemaPath) => {
11
12
  let fields = collection.flattenedFields;
12
13
  let blockType;
13
- let remaining = splitPath(schemaPath).filter(Boolean);
14
+ let remaining = splitPath(schemaPath);
14
15
  while (remaining.length > 0) {
15
16
  /**
16
17
  * A blocks field's own path may span several segments
17
- * (`layout.sections`), so the longest matching one is taken.
18
+ * (`/layout/sections`), so the longest matching one is taken.
18
19
  */ const match = blocksDescriptors(fields).map((descriptor) => splitPath(descriptor.path)).filter((parts) => parts.every((part, offset) => part === remaining[offset])).sort((left, right) => right.length - left.length)[0];
19
20
  if (!match) throw new Error(`"${joinPath(remaining)}" does not address a blocks field. Blocks fields here: ${blocksDescriptors(fields).map((descriptor) => descriptor.path).join(", ") || "none"}`);
20
21
  const slug = remaining.at(match.length);
21
22
  const field = findBlocksField(fields, match);
22
- if (!field) throw new Error(`"${match.join(".")}" could not be resolved.`);
23
+ if (!field) throw new Error(`"${joinPath(match)}" could not be resolved.`);
23
24
  if (slug === void 0) throw new Error(`"${joinPath(match)}" is a blocks field; append one of: ${blockSlugsOf(field).join(", ")}`);
24
25
  const block = blockOf(config, field, slug);
25
26
  if (!block) throw new Error(`"${slug}" is not allowed at "${joinPath(match)}". Allowed: ${blockSlugsOf(field).join(", ")}`);
@@ -37,11 +38,7 @@ const blocksDescriptors = (fields) => describeFields(fields).filter((descriptor)
37
38
  */ const describeNode = (config, collection, schemaPath = "") => {
38
39
  const { blockType, fields } = fieldsAtSchemaPath(config, collectionOf(config, collection), schemaPath);
39
40
  const descriptors = describeFields(fields);
40
- const next = descriptors.flatMap((descriptor) => (descriptor.blocks ?? []).map((slug) => [
41
- schemaPath,
42
- descriptor.path,
43
- slug
44
- ].filter(Boolean).join(".")));
41
+ const next = descriptors.flatMap((descriptor) => (descriptor.blocks ?? []).map((slug) => `${schemaPath}${descriptor.path}/${slug}`));
45
42
  return {
46
43
  ...blockType === void 0 ? {} : { blockType },
47
44
  collection,
@@ -65,11 +62,7 @@ const blocksDescriptors = (fields) => describeFields(fields).filter((descriptor)
65
62
  seen.push(schemaPath);
66
63
  for (const descriptor of describeNode(config, collection, schemaPath).fields) for (const slug of descriptor.blocks ?? []) {
67
64
  if (visited.includes(slug)) continue;
68
- walk([
69
- schemaPath,
70
- descriptor.path,
71
- slug
72
- ].filter(Boolean).join("."), [...visited, slug]);
65
+ walk(`${schemaPath}${descriptor.path}/${slug}`, [...visited, slug]);
73
66
  }
74
67
  };
75
68
  walk("", []);
@@ -1,10 +1,7 @@
1
1
  import { blockOf, blockSlugsOf, collectionOf, describeFields, findBlocksField, joinPath, splitPath } from "./walk.mjs";
2
2
  //#region src/schema/pointer.ts
3
3
  const isIndexSegment = (segment) => segment === "-" || /^\d+$/.test(segment);
4
- /**
5
- * Decodes a JSON Pointer into its segments, unescaping `~1` and `~0`.
6
- */ const pointerSegments = (pointer) => pointer.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
7
- const partMatches = (part, segment) => segment !== void 0 && (part === "[]" ? isIndexSegment(segment) : part === segment);
4
+ const partMatches = (part, segment) => segment !== void 0 && (part === "*" ? isIndexSegment(segment) : part === segment);
8
5
  /**
9
6
  * Longest descriptor whose path is fully consumed by the leading segments.
10
7
  */ const longestMatch = (descriptors, segments) => descriptors.map((descriptor) => ({
@@ -38,7 +35,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "[]" ? is
38
35
  let fields = collectionOf(config, target.collection).flattenedFields;
39
36
  let data = target.doc;
40
37
  let blockType;
41
- let segments = pointerSegments(target.pointer);
38
+ let segments = splitPath(target.pointer);
42
39
  while (segments.length > 0) {
43
40
  const descriptors = describeFields(fields);
44
41
  const match = longestMatch(descriptors, segments);
@@ -46,7 +43,7 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "[]" ? is
46
43
  if (isSubtreePrefix(descriptors, segments)) return {
47
44
  ...blockType === void 0 ? {} : { blockType },
48
45
  fields,
49
- prefix: joinPath(segments)
46
+ prefix: segments
50
47
  };
51
48
  throw new Error(`"${joinPath(segments)}" is not a field here. Available: ${descriptors.map((descriptor) => descriptor.path).join(", ")}`);
52
49
  }
@@ -55,9 +52,9 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "[]" ? is
55
52
  ...blockType === void 0 ? {} : { blockType },
56
53
  descriptor: match.descriptor,
57
54
  fields,
58
- prefix: ""
55
+ prefix: []
59
56
  };
60
- if (match.descriptor.type !== "blocks") throw new Error(`"${match.descriptor.path}" is a ${match.descriptor.type} field and has no "${rest.join("/")}" beneath it.`);
57
+ if (match.descriptor.type !== "blocks") throw new Error(`"${match.descriptor.path}" is a ${match.descriptor.type} field and has no "${joinPath(rest)}" beneath it.`);
61
58
  const [index, ...remaining] = rest;
62
59
  if (!isIndexSegment(index)) throw new Error(`"${match.descriptor.path}" is an array; "${index}" is not an index.`);
63
60
  const parts = splitPath(match.descriptor.path);
@@ -76,8 +73,8 @@ const partMatches = (part, segment) => segment !== void 0 && (part === "[]" ? is
76
73
  return {
77
74
  ...blockType === void 0 ? {} : { blockType },
78
75
  fields,
79
- prefix: ""
76
+ prefix: []
80
77
  };
81
78
  };
82
79
  //#endregion
83
- export { pointerSegments, resolveDataPointer };
80
+ export { resolveDataPointer };
@@ -1,4 +1,4 @@
1
- import { blockOf, blockSlugsOf, describeFields, findBlocksField, joinPath, splitPath } from "./walk.mjs";
1
+ import { blockOf, blockSlugsOf, describeFields, findBlocksField, splitPath } from "./walk.mjs";
2
2
  //#region src/schema/shape.ts
3
3
  /**
4
4
  * Keys Payload manages on a row that a client may echo back harmlessly.
@@ -36,7 +36,7 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
36
36
  };
37
37
  const checkLeafValue = (scope, descriptor, value) => {
38
38
  if (descriptor.readOnly) {
39
- scope.problems.push(`${scope.pointer}: "${descriptor.path}" is read-only and cannot be written.`);
39
+ scope.problems.push(`${scope.pointer}: this field is read-only and cannot be written.`);
40
40
  return;
41
41
  }
42
42
  if (descriptor.type === "richText") {
@@ -61,7 +61,7 @@ const checkLeafValue = (scope, descriptor, value) => {
61
61
  ...scope,
62
62
  fields: block.flattenedFields,
63
63
  pointer: `${scope.pointer}/${String(index)}`,
64
- prefix: ""
64
+ prefix: []
65
65
  }, row);
66
66
  });
67
67
  };
@@ -76,7 +76,7 @@ const checkLeafValue = (scope, descriptor, value) => {
76
76
  * block would be stripped in silence.
77
77
  */ const checkValue = (scope, value) => {
78
78
  if (!isPlainObject(value)) return;
79
- const prefixParts = scope.prefix ? splitPath(scope.prefix) : [];
79
+ const prefixParts = scope.prefix;
80
80
  const relative = describeFields(scope.fields).flatMap((descriptor) => {
81
81
  const parts = splitPath(descriptor.path);
82
82
  return prefixParts.every((part, offset) => part === parts[offset]) ? [{
@@ -100,7 +100,7 @@ const checkLeafValue = (scope, descriptor, value) => {
100
100
  }, exact.descriptor, entry);
101
101
  continue;
102
102
  }
103
- if (candidates.some(({ parts }) => parts[1] === "[]")) {
103
+ if (candidates.some(({ parts }) => parts[1] === "*")) {
104
104
  if (!Array.isArray(entry)) {
105
105
  scope.problems.push(`${pointer}: expected an array.`);
106
106
  continue;
@@ -109,11 +109,11 @@ const checkLeafValue = (scope, descriptor, value) => {
109
109
  checkValue({
110
110
  ...scope,
111
111
  pointer: `${pointer}/${String(index)}`,
112
- prefix: joinPath([
112
+ prefix: [
113
113
  ...prefixParts,
114
114
  key,
115
- "[]"
116
- ])
115
+ "*"
116
+ ]
117
117
  }, row);
118
118
  });
119
119
  continue;
@@ -121,7 +121,7 @@ const checkLeafValue = (scope, descriptor, value) => {
121
121
  checkValue({
122
122
  ...scope,
123
123
  pointer,
124
- prefix: joinPath([...prefixParts, key])
124
+ prefix: [...prefixParts, key]
125
125
  }, entry);
126
126
  }
127
127
  };
@@ -11,13 +11,22 @@ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
11
11
  "updatedAt"
12
12
  ]);
13
13
  /**
14
- * Joins path segments, attaching the array marker to its field rather than
15
- * separating it, so an array subfield reads `items[].title`.
16
- */ const joinPath = (parts) => parts.reduce((path, part) => part === "[]" ? `${path}[]` : path ? `${path}.${part}` : part, "");
14
+ * Shape a JSON Pointer must have to be parseable at all.
15
+ */ const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
17
16
  /**
18
- * Splits a descriptor path back into pointer-comparable segments, with the
19
- * array marker as a segment of its own.
20
- */ const splitPath = (path) => path.split(".").flatMap((part) => part.endsWith("[]") ? [part.slice(0, -2), "[]"] : [part]);
17
+ * Joins segments into a JSON Pointer, so the segments `items`, `*`, `title`
18
+ * read as one path to a subfield of every element of `items`. No segments is
19
+ * the root pointer, `""`.
20
+ */ const joinPath = (parts) => parts.map((part) => `/${part.replace(/~/g, "~0").replace(/\//g, "~1")}`).join("");
21
+ /**
22
+ * Splits a JSON Pointer into its segments, unescaping `~1` and `~0`. The root
23
+ * pointer yields no segments.
24
+ */ const splitPath = (path) => path.split("/").slice(1).map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
25
+ /**
26
+ * Restates a path Payload reports on a validation error (`layout.0.title`) as
27
+ * a JSON Pointer, so everything this plugin hands back addresses documents the
28
+ * same way. Payload's path already carries real indices, so it maps directly.
29
+ */ const pointerFromPayloadPath = (path) => path ? joinPath(path.split(".")) : "";
21
30
  /**
22
31
  * Blocks a blocks field accepts, by slug. On a flattened field, whichever of
23
32
  * `blockReferences` and `blocks` was declared carries the definitions.
@@ -83,7 +92,7 @@ const withRows = (descriptor, field) => ({
83
92
  const readOnly = parentReadOnly || isReadOnly(field);
84
93
  const path = [...prefix, field.name];
85
94
  if (field.type === "tab" || field.type === "group") return describeFields(field.flattenedFields, path, readOnly);
86
- if (field.type === "array") return describeFields(field.flattenedFields, [...path, "[]"], readOnly);
95
+ if (field.type === "array") return describeFields(field.flattenedFields, [...path, "*"], readOnly);
87
96
  if (field.type === "blocks") return [withRows({
88
97
  ...describeBase(field, joinPath(path), readOnly),
89
98
  blocks: blockSlugsOf(field)
@@ -97,7 +106,7 @@ const withRows = (descriptor, field) => ({
97
106
  if (!("name" in field) || field.name !== path[0]) continue;
98
107
  if (field.type === "blocks" && path.length === 1) return field;
99
108
  if (field.type === "tab" || field.type === "group") return findBlocksField(field.flattenedFields, path.slice(1));
100
- if (field.type === "array" && path[1] === "[]") return findBlocksField(field.flattenedFields, path.slice(2));
109
+ if (field.type === "array" && path[1] === "*") return findBlocksField(field.flattenedFields, path.slice(2));
101
110
  }
102
111
  };
103
112
  const collectionOf = (config, collection) => {
@@ -106,4 +115,4 @@ const collectionOf = (config, collection) => {
106
115
  return found;
107
116
  };
108
117
  //#endregion
109
- export { RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, collectionOf, describeFields, findBlocksField, joinPath, splitPath, staticDescription };
118
+ export { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, collectionOf, describeFields, findBlocksField, joinPath, pointerFromPayloadPath, splitPath, staticDescription };
@@ -32,7 +32,7 @@ const createDocument = {
32
32
  pointer: "",
33
33
  resolution: {
34
34
  fields: collection.flattenedFields,
35
- prefix: ""
35
+ prefix: []
36
36
  }
37
37
  }, seed);
38
38
  if (problems.length > 0) return errorResult("Nothing was created.", { problems });
@@ -7,9 +7,9 @@ const describeSchema = {
7
7
  name: "describeSchema",
8
8
  description: `Describes the writable shape of a document, one node at a time.
9
9
 
10
- Call it with no "paths" to get a collection's own fields. Every "blocks" field stops there and lists the block slugs it accepts instead of nesting them; each node's "next" lists the ready-to-use paths for those blocks, so pass any entry of "next" as a "paths" element to descend, e.g. "layout.sections.sectionWrapper" and then "layout.sections.sectionWrapper.modules.hero". A block is described as it exists at that position, because the same block can accept different children elsewhere.
10
+ Call it with no "paths" to get a collection's own fields. Every "blocks" field stops there and lists the block slugs it accepts instead of nesting them; each node's "next" lists the ready-to-use paths for those blocks, so pass any entry of "next" as a "paths" element to descend, e.g. "/layout/sections/sectionWrapper" and then "/layout/sections/sectionWrapper/modules/hero". A block is described as it exists at that position, because the same block can accept different children elsewhere.
11
11
 
12
- Field paths are dotted and already resolved through anything that does not nest in the stored document. To turn one into a patchDocument pointer, replace each "." with "/", add a leading "/", and replace each "[]" with a 0-based index. Note a path here names a block by its slug where a pointer names it by its index.
12
+ Paths here use the same JSON Pointer syntax as getDocument and patchDocument, and are already resolved through anything that does not nest in the stored document. The difference is only what stands in an element position: a path names an array element "*" and a block by its slug, where a pointer into a document carries a 0-based index. So "/items/*/title" is written at "/items/0/title", and "/layout/sections/hero" at "/layout/sections/0".
13
13
 
14
14
  Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed and cannot be written. Fields marked readOnly are listed but refused on write.`,
15
15
  annotations: {
@@ -19,7 +19,7 @@ Fields Payload maintains (id, _status, createdAt, updatedAt) are never listed an
19
19
  isEnabled: (scope) => scope.readable.length > 0,
20
20
  inputSchema: (scope) => ({
21
21
  collection: collectionEnum(scope.readable).describe("Collection to describe."),
22
- paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"layout.sections.sectionWrapper\". Omit for the collection root."),
22
+ paths: z.array(z.string()).optional().describe("Schema paths to describe, e.g. \"/layout/sections/sectionWrapper\". Omit for the collection root."),
23
23
  expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
24
24
  }),
25
25
  handler: (args, scope) => {
@@ -1,3 +1,4 @@
1
+ import { JSON_POINTER_PATTERN } from "../schema/walk.mjs";
1
2
  import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
3
  import { collectionEnum, depthShape, ensureAllowed, idSchema, localeOf, localeShape } from "./shared.mjs";
3
4
  import { z } from "zod";
@@ -14,7 +15,7 @@ const getDocument = {
14
15
  inputSchema: (scope) => ({
15
16
  collection: collectionEnum(scope.readable).describe("Collection holding the document."),
16
17
  id: idSchema,
17
- path: z.string().optional().describe("JSON pointer to return only a subtree, e.g. \"/layout/sections/0\"."),
18
+ path: z.string().regex(JSON_POINTER_PATTERN).optional().describe("JSON pointer to return only a subtree, e.g. \"/layout/sections/0\"."),
18
19
  ...depthShape(scope),
19
20
  ...localeShape(scope, {
20
21
  required: false,
@@ -1,6 +1,6 @@
1
+ import { staticDescription } from "../schema/walk.mjs";
1
2
  import { jsonResult } from "../endpoint/result.mjs";
2
3
  import { translateLabel } from "./shared.mjs";
3
- import { staticDescription } from "../schema/walk.mjs";
4
4
  import { hasDraftValidationEnabled } from "payload/shared";
5
5
  //#region src/tools/list-capabilities.ts
6
6
  const listCapabilities = {
@@ -10,7 +10,7 @@ const DESCRIPTION = `Applies RFC 6902 JSON Patch operations to one document.
10
10
 
11
11
  The write always lands as a draft and is never published, whatever it contains; publishing stays a human action in the admin panel.
12
12
 
13
- Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. Use describeSchema to find a field's path, then turn it into a pointer by replacing "." with "/" and each "[]" with a 0-based index.
13
+ Only the fields describeSchema lists can be addressed. A pointer that does not resolve is refused with the fields that are valid at that point, and nothing is applied unless every operation in the batch validates first. describeSchema reports field paths in this same pointer syntax; a path becomes a pointer into a document by replacing each "*" and each block slug with its 0-based index.
14
14
 
15
15
  Adding a block requires "blockType" on the value. Append with "/-" as the last segment. To clear a field use "replace" with null; an array or blocks field refuses null and is emptied with [] instead. "remove" is only for list elements, because a field left out of a write is kept rather than cleared. Read the document first to learn the indices, and pass its "updatedAt" as expectedUpdatedAt so a concurrent edit is refused rather than overwritten.
16
16
 
package/dist/version.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/version.ts
2
2
  /**
3
3
  * Package version injected at build time; sources under Vitest report "dev".
4
- */ const MCPX_VERSION = "1.0.0-beta.3";
4
+ */ const MCPX_VERSION = "0.0.0";
5
5
  //#endregion
6
6
  export { MCPX_VERSION };
@@ -1,4 +1,4 @@
1
- import { RESERVED_FIELD_NAMES, blockOf, describeFields, findBlocksField, splitPath } from "../schema/walk.mjs";
1
+ import { JSON_POINTER_PATTERN, RESERVED_FIELD_NAMES, blockOf, describeFields, findBlocksField, splitPath } from "../schema/walk.mjs";
2
2
  import { validateWriteValue } from "../schema/shape.mjs";
3
3
  import { resolveDataPointer } from "../schema/pointer.mjs";
4
4
  import { z } from "zod";
@@ -6,8 +6,7 @@ import { Pointer, applyPatch } from "rfc6902";
6
6
  //#region src/write/patch.ts
7
7
  /**
8
8
  * One RFC 6902 operation as accepted by `patchDocument`.
9
- */ const JSON_POINTER_PATTERN = /^(\/([^~/]|~[01])*)*$/;
10
- const PATCH_OPERATION_SCHEMA = z.object({
9
+ */ const PATCH_OPERATION_SCHEMA = z.object({
11
10
  from: z.string().regex(JSON_POINTER_PATTERN).optional(),
12
11
  op: z.enum([
13
12
  "add",
@@ -186,13 +185,13 @@ const isPlainObject = (value) => typeof value === "object" && value !== null &&
186
185
  result[key] = entry;
187
186
  continue;
188
187
  }
189
- if (candidates.some(({ parts }) => parts[1] === "[]")) {
188
+ if (candidates.some(({ parts }) => parts[1] === "*")) {
190
189
  result[key] = Array.isArray(entry) ? entry.map((row) => isPlainObject(row) ? pickDescribed(config, row, {
191
190
  fields,
192
191
  prefix: [
193
192
  ...prefix,
194
193
  key,
195
- "[]"
194
+ "*"
196
195
  ],
197
196
  isRow: true
198
197
  }) : row) : entry;
@@ -7,6 +7,7 @@ interface PublishBlocker {
7
7
  /** Resolved field label path, e.g. "Layout > Block 2 (Hero) > Title". */
8
8
  field?: string;
9
9
  message: string;
10
+ /** JSON Pointer to the offending value, e.g. "/layout/2/title". */
10
11
  path: string;
11
12
  }
12
13
  //#endregion
@@ -1,3 +1,4 @@
1
+ import { pointerFromPayloadPath } from "../schema/walk.mjs";
1
2
  import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payload";
2
3
  //#region src/write/publish-blockers.ts
3
4
  /**
@@ -64,7 +65,7 @@ import { beforeChangeTraverseFields, beforeValidateTraverseFields } from "payloa
64
65
  }
65
66
  return errors.map((error) => ({
66
67
  message: error.message,
67
- path: error.path,
68
+ path: pointerFromPayloadPath(error.path),
68
69
  ...typeof error.label === "string" ? { field: error.label } : {}
69
70
  }));
70
71
  };
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.3",
4
+ "version": "1.0.0-beta.4",
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",