@abinnovision/payloadcms-mcpx 1.0.0-beta.3

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 (40) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +258 -0
  3. package/dist/api-keys/collection.mjs +58 -0
  4. package/dist/api-keys/fields.mjs +88 -0
  5. package/dist/api-keys/key.mjs +10 -0
  6. package/dist/auth/resolve.mjs +62 -0
  7. package/dist/capabilities.mjs +30 -0
  8. package/dist/endpoint/handler.mjs +84 -0
  9. package/dist/endpoint/result.mjs +59 -0
  10. package/dist/endpoint/server.mjs +52 -0
  11. package/dist/index.d.mts +5 -0
  12. package/dist/index.mjs +4 -0
  13. package/dist/options.mjs +107 -0
  14. package/dist/plugin.d.mts +9 -0
  15. package/dist/plugin.mjs +42 -0
  16. package/dist/schema/describe.mjs +82 -0
  17. package/dist/schema/lexical.mjs +25 -0
  18. package/dist/schema/pointer.mjs +83 -0
  19. package/dist/schema/shape.mjs +147 -0
  20. package/dist/schema/walk.mjs +109 -0
  21. package/dist/tools/create-document.mjs +67 -0
  22. package/dist/tools/describe-schema.mjs +44 -0
  23. package/dist/tools/find-documents.mjs +54 -0
  24. package/dist/tools/get-document.mjs +54 -0
  25. package/dist/tools/index.mjs +23 -0
  26. package/dist/tools/list-capabilities.mjs +49 -0
  27. package/dist/tools/names.mjs +14 -0
  28. package/dist/tools/patch-document.mjs +113 -0
  29. package/dist/tools/shared.mjs +65 -0
  30. package/dist/tools/validate-document.mjs +48 -0
  31. package/dist/types.d.mts +113 -0
  32. package/dist/types.mjs +7 -0
  33. package/dist/version.mjs +6 -0
  34. package/dist/write/draft-guard.d.mts +10 -0
  35. package/dist/write/draft-guard.mjs +70 -0
  36. package/dist/write/patch.mjs +220 -0
  37. package/dist/write/publish-blockers.d.mts +13 -0
  38. package/dist/write/publish-blockers.mjs +72 -0
  39. package/dist/write/transaction.mjs +19 -0
  40. package/package.json +89 -0
@@ -0,0 +1,147 @@
1
+ import { blockOf, blockSlugsOf, describeFields, findBlocksField, joinPath, splitPath } from "./walk.mjs";
2
+ //#region src/schema/shape.ts
3
+ /**
4
+ * Keys Payload manages on a row that a client may echo back harmlessly.
5
+ */ const TOLERATED_VALUE_KEYS = /* @__PURE__ */ new Set([
6
+ "blockName",
7
+ "blockType",
8
+ "id"
9
+ ]);
10
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
11
+ /**
12
+ * Checks every node type in an editor state against what the field's editor
13
+ * can actually produce.
14
+ *
15
+ * Payload does not: the Lexical validator runs node validations only for the
16
+ * few node types that register one, so a `heading` inside a field whose
17
+ * editor has no heading feature is stored without complaint and only fails
18
+ * later, at render or when the document is reopened in the admin editor.
19
+ */ const checkRichText = (scope, allowed, value) => {
20
+ if (!isPlainObject(value) || !isPlainObject(value["root"])) {
21
+ scope.problems.push(`${scope.pointer}: expected a Lexical editor state with a "root".`);
22
+ return;
23
+ }
24
+ const walk = (nodes) => {
25
+ if (!Array.isArray(nodes)) return;
26
+ for (const node of nodes) {
27
+ if (!isPlainObject(node) || typeof node["type"] !== "string") {
28
+ scope.problems.push(`${scope.pointer}: every node needs a "type".`);
29
+ continue;
30
+ }
31
+ if (!allowed.includes(node["type"])) scope.problems.push(`${scope.pointer}: "${node["type"]}" is not available in this field's editor. Allowed: ${allowed.join(", ")}`);
32
+ walk(node["children"]);
33
+ }
34
+ };
35
+ walk(value["root"]["children"]);
36
+ };
37
+ const checkLeafValue = (scope, descriptor, value) => {
38
+ if (descriptor.readOnly) {
39
+ scope.problems.push(`${scope.pointer}: "${descriptor.path}" is read-only and cannot be written.`);
40
+ return;
41
+ }
42
+ if (descriptor.type === "richText") {
43
+ checkRichText(scope, descriptor.nodes ?? [], value);
44
+ return;
45
+ }
46
+ if (descriptor.type !== "blocks") return;
47
+ if (!Array.isArray(value)) {
48
+ scope.problems.push(`${scope.pointer}: expected an array of blocks.`);
49
+ return;
50
+ }
51
+ const field = findBlocksField(scope.fields, splitPath(descriptor.path));
52
+ if (!field) return;
53
+ value.forEach((row, index) => {
54
+ const slug = isPlainObject(row) ? row["blockType"] : void 0;
55
+ const block = typeof slug === "string" ? blockOf(scope.config, field, slug) : void 0;
56
+ if (!block) {
57
+ scope.problems.push(`${scope.pointer}/${String(index)}: "${String(slug)}" is not allowed here. Allowed: ${blockSlugsOf(field).join(", ")}`);
58
+ return;
59
+ }
60
+ checkValue({
61
+ ...scope,
62
+ fields: block.flattenedFields,
63
+ pointer: `${scope.pointer}/${String(index)}`,
64
+ prefix: ""
65
+ }, row);
66
+ });
67
+ };
68
+ /**
69
+ * Walks an incoming value against the schema, reporting every shape problem
70
+ * rather than the first.
71
+ *
72
+ * Shape only: unknown field names, unknown block slugs, read-only fields and
73
+ * unusable rich text nodes. Required-ness, row counts, enum membership and
74
+ * relationship existence stay with Payload, which already checks them and
75
+ * reports them per field. Without this pass a misspelled field inside a new
76
+ * block would be stripped in silence.
77
+ */ const checkValue = (scope, value) => {
78
+ if (!isPlainObject(value)) return;
79
+ const prefixParts = scope.prefix ? splitPath(scope.prefix) : [];
80
+ const relative = describeFields(scope.fields).flatMap((descriptor) => {
81
+ const parts = splitPath(descriptor.path);
82
+ return prefixParts.every((part, offset) => part === parts[offset]) ? [{
83
+ descriptor,
84
+ parts: parts.slice(prefixParts.length)
85
+ }] : [];
86
+ });
87
+ for (const [key, entry] of Object.entries(value)) {
88
+ if (TOLERATED_VALUE_KEYS.has(key)) continue;
89
+ const candidates = relative.filter(({ parts }) => parts[0] === key);
90
+ const pointer = `${scope.pointer}/${key}`;
91
+ if (candidates.length === 0) {
92
+ scope.problems.push(`${pointer}: no such field. Available: ${[...new Set(relative.map(({ parts }) => parts[0]))].join(", ")}`);
93
+ continue;
94
+ }
95
+ const exact = candidates.find(({ parts }) => parts.length === 1);
96
+ if (exact) {
97
+ checkLeafValue({
98
+ ...scope,
99
+ pointer
100
+ }, exact.descriptor, entry);
101
+ continue;
102
+ }
103
+ if (candidates.some(({ parts }) => parts[1] === "[]")) {
104
+ if (!Array.isArray(entry)) {
105
+ scope.problems.push(`${pointer}: expected an array.`);
106
+ continue;
107
+ }
108
+ entry.forEach((row, index) => {
109
+ checkValue({
110
+ ...scope,
111
+ pointer: `${pointer}/${String(index)}`,
112
+ prefix: joinPath([
113
+ ...prefixParts,
114
+ key,
115
+ "[]"
116
+ ])
117
+ }, row);
118
+ });
119
+ continue;
120
+ }
121
+ checkValue({
122
+ ...scope,
123
+ pointer,
124
+ prefix: joinPath([...prefixParts, key])
125
+ }, entry);
126
+ }
127
+ };
128
+ /**
129
+ * Shape problems with a value about to be written at a resolved pointer.
130
+ */ const validateWriteValue = (config, target, value) => {
131
+ const problems = [];
132
+ const scope = {
133
+ config,
134
+ fields: target.resolution.fields,
135
+ pointer: target.pointer,
136
+ prefix: target.resolution.prefix,
137
+ problems
138
+ };
139
+ if (target.resolution.descriptor) {
140
+ checkLeafValue(scope, target.resolution.descriptor, value);
141
+ return problems;
142
+ }
143
+ checkValue(scope, value);
144
+ return problems;
145
+ };
146
+ //#endregion
147
+ export { validateWriteValue };
@@ -0,0 +1,109 @@
1
+ import { allowedNodeTypes } from "./lexical.mjs";
2
+ import { fieldIsHiddenOrDisabled, fieldIsVirtual } from "payload/shared";
3
+ //#region src/schema/walk.ts
4
+ /**
5
+ * Fields Payload maintains, which a client may neither address nor supply.
6
+ */ const RESERVED_FIELD_NAMES = /* @__PURE__ */ new Set([
7
+ "_status",
8
+ "createdAt",
9
+ "deletedAt",
10
+ "id",
11
+ "updatedAt"
12
+ ]);
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, "");
17
+ /**
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]);
21
+ /**
22
+ * Blocks a blocks field accepts, by slug. On a flattened field, whichever of
23
+ * `blockReferences` and `blocks` was declared carries the definitions.
24
+ */ const blockSlugsOf = (field) => [...new Set((field.blockReferences ?? field.blocks).map((block) => typeof block === "string" ? block : block.slug))];
25
+ /**
26
+ * Resolves one of a blocks field's slugs to its definition.
27
+ *
28
+ * A definition inlined on the field wins over the shared registry. A block's
29
+ * own fields are identical wherever it appears, but the blocks its children
30
+ * accept are not, so an inline definition has to be read at its position.
31
+ * The registry (`config.blocks`) is the fallback for slugs referenced by name.
32
+ */ const blockOf = (config, field, slug) => {
33
+ const declared = field.blockReferences ?? field.blocks;
34
+ const inline = declared.find((block) => typeof block !== "string" && block.slug === slug);
35
+ if (inline) return inline;
36
+ return declared.includes(slug) ? config.blocks?.find((block) => block.slug === slug) : void 0;
37
+ };
38
+ const isSkipped = (field) => !("name" in field) || field.type === "join" || RESERVED_FIELD_NAMES.has(field.name) || fieldIsVirtual(field) || fieldIsHiddenOrDisabled(field);
39
+ const isReadOnly = (field) => "admin" in field && field.admin.readOnly === true;
40
+ /**
41
+ * The `admin.description` of a field or collection, when it is serializable:
42
+ * a string or a locale-keyed record. Functions and components are admin-UI
43
+ * constructs and are dropped.
44
+ */ const staticDescription = (description) => {
45
+ if (typeof description === "string") return description;
46
+ return typeof description === "object" && description !== null && Object.values(description).every((entry) => typeof entry === "string") ? description : void 0;
47
+ };
48
+ const describeBase = (field, path, readOnly) => {
49
+ const description = staticDescription("admin" in field ? field.admin.description : void 0);
50
+ return {
51
+ path,
52
+ type: field.type,
53
+ ...description === void 0 ? {} : { description },
54
+ ..."required" in field && field.required ? { required: true } : {},
55
+ ..."localized" in field && field.localized ? { localized: true } : {},
56
+ ...readOnly ? { readOnly: true } : {}
57
+ };
58
+ };
59
+ const describeLeaf = (field, path, readOnly) => {
60
+ const descriptor = describeBase(field, path, readOnly);
61
+ if (field.type === "select" || field.type === "radio") descriptor.options = field.options.map((option) => typeof option === "string" ? option : option.value);
62
+ if (field.type === "relationship" || field.type === "upload") descriptor.relationTo = field.relationTo;
63
+ if ((field.type === "select" || field.type === "relationship" || field.type === "upload") && field.hasMany === true) descriptor.hasMany = true;
64
+ if (field.type === "richText") descriptor.nodes = allowedNodeTypes(field);
65
+ return descriptor;
66
+ };
67
+ const withRows = (descriptor, field) => ({
68
+ ...descriptor,
69
+ ...field.minRows === void 0 ? {} : { minRows: field.minRows },
70
+ ...field.maxRows === void 0 ? {} : { maxRows: field.maxRows }
71
+ });
72
+ /**
73
+ * Flattens a field list into descriptors addressed relative to the node.
74
+ *
75
+ * The input is Payload's own flattened shape, which has already merged every
76
+ * construct that exists only in the admin UI (unnamed tabs, unnamed groups,
77
+ * `row`, `collapsible`) and dropped `ui` fields. Named tabs, groups and
78
+ * arrays contribute a path segment. The walk stops at every blocks field and
79
+ * names the slugs instead of descending, which keeps a node proportional to
80
+ * the number of blocks it allows rather than to the size of their definitions.
81
+ */ const describeFields = (fields, prefix = [], parentReadOnly = false) => fields.flatMap((field) => {
82
+ if (isSkipped(field)) return [];
83
+ const readOnly = parentReadOnly || isReadOnly(field);
84
+ const path = [...prefix, field.name];
85
+ if (field.type === "tab" || field.type === "group") return describeFields(field.flattenedFields, path, readOnly);
86
+ if (field.type === "array") return describeFields(field.flattenedFields, [...path, "[]"], readOnly);
87
+ if (field.type === "blocks") return [withRows({
88
+ ...describeBase(field, joinPath(path), readOnly),
89
+ blocks: blockSlugsOf(field)
90
+ }, field)];
91
+ return [describeLeaf(field, joinPath(path), readOnly)];
92
+ });
93
+ /**
94
+ * Locates the blocks field that a resolved descriptor path refers to.
95
+ */ const findBlocksField = (fields, path) => {
96
+ for (const field of fields) {
97
+ if (!("name" in field) || field.name !== path[0]) continue;
98
+ if (field.type === "blocks" && path.length === 1) return field;
99
+ 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));
101
+ }
102
+ };
103
+ const collectionOf = (config, collection) => {
104
+ const found = config.collections.find((candidate) => candidate.slug === collection);
105
+ if (!found) throw new Error(`Unknown collection "${collection}".`);
106
+ return found;
107
+ };
108
+ //#endregion
109
+ export { RESERVED_FIELD_NAMES, blockOf, blockSlugsOf, collectionOf, describeFields, findBlocksField, joinPath, splitPath, staticDescription };
@@ -0,0 +1,67 @@
1
+ import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
+ import { collectionEnum, ensureAllowed, localeOf, localeShape, readDraft } from "./shared.mjs";
3
+ import { validateWriteValue } from "../schema/shape.mjs";
4
+ import { stripRowIds } from "../write/patch.mjs";
5
+ import { collectPublishBlockers } from "../write/publish-blockers.mjs";
6
+ import { z } from "zod";
7
+ //#region src/tools/create-document.ts
8
+ const createDocument = {
9
+ name: "createDocument",
10
+ description: `Creates a new document as a draft from a minimal seed. Only the fields describeSchema lists may appear in "data"; unknown keys are refused with the valid siblings. The draft may be incomplete: the response lists "publishBlockers", which patchDocument can then work through. Use this when no document exists yet; prefer patching an existing draft otherwise.`,
11
+ annotations: {
12
+ readOnlyHint: false,
13
+ destructiveHint: false,
14
+ idempotentHint: false,
15
+ openWorldHint: false
16
+ },
17
+ isEnabled: (scope) => scope.writable.length > 0,
18
+ inputSchema: (scope) => ({
19
+ collection: collectionEnum(scope.writable).describe("Collection to create the document in."),
20
+ ...localeShape(scope, {
21
+ required: true,
22
+ description: "Locale the localized fields of the seed belong to."
23
+ }),
24
+ data: z.record(z.string(), z.unknown()).describe("Initial field values, as describeSchema lists them.")
25
+ }),
26
+ handler: async (args, scope) => {
27
+ const collection = ensureAllowed(scope, args.collection, "write");
28
+ const { payload } = scope.req;
29
+ const locale = localeOf(scope, args.locale);
30
+ const { id: _ignored, ...seed } = args.data;
31
+ const problems = validateWriteValue(payload.config, {
32
+ pointer: "",
33
+ resolution: {
34
+ fields: collection.flattenedFields,
35
+ prefix: ""
36
+ }
37
+ }, seed);
38
+ if (problems.length > 0) return errorResult("Nothing was created.", { problems });
39
+ const created = await payload.create({
40
+ collection: args.collection,
41
+ data: stripRowIds(seed),
42
+ depth: 0,
43
+ draft: true,
44
+ overrideAccess: false,
45
+ req: scope.req,
46
+ ...locale === void 0 ? {} : { locale }
47
+ });
48
+ const saved = await readDraft(scope, {
49
+ collection: args.collection,
50
+ id: created["id"],
51
+ locale,
52
+ privileged: true
53
+ });
54
+ const publishBlockers = await collectPublishBlockers(scope.req, {
55
+ collection,
56
+ doc: saved
57
+ });
58
+ return jsonResult({
59
+ id: saved["id"],
60
+ status: saved["_status"],
61
+ updatedAt: saved["updatedAt"],
62
+ ...publishBlockers.length > 0 ? { publishBlockers } : {}
63
+ });
64
+ }
65
+ };
66
+ //#endregion
67
+ export { createDocument };
@@ -0,0 +1,44 @@
1
+ import { jsonResult } from "../endpoint/result.mjs";
2
+ import { collectionEnum, ensureAllowed } from "./shared.mjs";
3
+ import { describeNode, reachableSchemaPaths } from "../schema/describe.mjs";
4
+ import { z } from "zod";
5
+ //#region src/tools/describe-schema.ts
6
+ const describeSchema = {
7
+ name: "describeSchema",
8
+ description: `Describes the writable shape of a document, one node at a time.
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.
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.
13
+
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
+ annotations: {
16
+ readOnlyHint: true,
17
+ openWorldHint: false
18
+ },
19
+ isEnabled: (scope) => scope.readable.length > 0,
20
+ inputSchema: (scope) => ({
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."),
23
+ expand: z.boolean().optional().describe("Return every node reachable from the root in one response. Ignores paths.")
24
+ }),
25
+ handler: (args, scope) => {
26
+ ensureAllowed(scope, args.collection, "read");
27
+ const { config } = scope.req.payload;
28
+ const expanded = args.expand === true ? reachableSchemaPaths(config, args.collection) : void 0;
29
+ const nodes = (expanded?.paths ?? (args.paths && args.paths.length > 0 ? args.paths : [""])).map((schemaPath) => {
30
+ try {
31
+ return describeNode(config, args.collection, schemaPath);
32
+ } catch (error) {
33
+ return {
34
+ error: error instanceof Error ? error.message : "Unknown error",
35
+ schemaPath
36
+ };
37
+ }
38
+ });
39
+ if (expanded?.truncated) nodes.push({ error: `Result truncated after ${String(400)} nodes. Request explicit paths instead.` });
40
+ return Promise.resolve(jsonResult(nodes));
41
+ }
42
+ };
43
+ //#endregion
44
+ export { describeSchema };
@@ -0,0 +1,54 @@
1
+ import { jsonResult } from "../endpoint/result.mjs";
2
+ import { collectionEnum, depthShape, ensureAllowed, localeOf, localeShape } from "./shared.mjs";
3
+ import { z } from "zod";
4
+ //#region src/tools/find-documents.ts
5
+ const findDocuments = {
6
+ name: "findDocuments",
7
+ description: `Finds documents in a collection. "where" is a Payload query object, e.g. {"title":{"contains":"home"}} or {"and":[...]}; "select" picks fields, e.g. {"title":true}. Drafts are included by default so unpublished work is visible. Keep depth at 0 unless populated relationships are needed; ids are enough for writes.`,
8
+ annotations: {
9
+ readOnlyHint: true,
10
+ openWorldHint: false
11
+ },
12
+ isEnabled: (scope) => scope.readable.length > 0,
13
+ inputSchema: (scope) => ({
14
+ collection: collectionEnum(scope.readable).describe("Collection to search."),
15
+ where: z.record(z.string(), z.unknown()).optional().describe("Payload where query."),
16
+ sort: z.string().optional().describe("Sort field, prefix with \"-\" for descending."),
17
+ limit: z.number().int().min(1).max(scope.options.limits.maxLimit).optional().describe(`Documents per page. Default 10, at most ${String(scope.options.limits.maxLimit)}.`),
18
+ page: z.number().int().min(1).optional().describe("Page number, from 1."),
19
+ ...depthShape(scope),
20
+ select: z.record(z.string(), z.unknown()).optional().describe("Fields to return, e.g. {\"title\":true}."),
21
+ ...localeShape(scope, {
22
+ required: false,
23
+ description: "Locale to read. Defaults to the default locale."
24
+ }),
25
+ draft: z.boolean().optional().describe("Include the latest drafts. Default true.")
26
+ }),
27
+ handler: async (args, scope) => {
28
+ ensureAllowed(scope, args.collection, "read");
29
+ const locale = localeOf(scope, args.locale);
30
+ const result = await scope.req.payload.find({
31
+ collection: args.collection,
32
+ depth: args.depth ?? 0,
33
+ draft: args.draft ?? true,
34
+ limit: args.limit ?? 10,
35
+ overrideAccess: false,
36
+ req: scope.req,
37
+ ...args.page === void 0 ? {} : { page: args.page },
38
+ ...args.sort === void 0 ? {} : { sort: args.sort },
39
+ ...args.where === void 0 ? {} : { where: args.where },
40
+ ...args.select === void 0 ? {} : { select: args.select },
41
+ ...locale === void 0 ? {} : { locale }
42
+ });
43
+ return jsonResult({
44
+ docs: result.docs,
45
+ totalDocs: result.totalDocs,
46
+ page: result.page,
47
+ totalPages: result.totalPages,
48
+ limit: result.limit,
49
+ hasNextPage: result.hasNextPage
50
+ });
51
+ }
52
+ };
53
+ //#endregion
54
+ export { findDocuments };
@@ -0,0 +1,54 @@
1
+ import { errorResult, jsonResult } from "../endpoint/result.mjs";
2
+ import { collectionEnum, depthShape, ensureAllowed, idSchema, localeOf, localeShape } from "./shared.mjs";
3
+ import { z } from "zod";
4
+ import { Pointer } from "rfc6902";
5
+ //#region src/tools/get-document.ts
6
+ const getDocument = {
7
+ name: "getDocument",
8
+ description: `Reads one document, or one subtree of it when "path" is given as a JSON pointer such as "/layout/sections/2". Returns the latest draft by default. Read before patching: the response carries "updatedAt" for expectedUpdatedAt and the indices pointers need.`,
9
+ annotations: {
10
+ readOnlyHint: true,
11
+ openWorldHint: false
12
+ },
13
+ isEnabled: (scope) => scope.readable.length > 0,
14
+ inputSchema: (scope) => ({
15
+ collection: collectionEnum(scope.readable).describe("Collection holding the document."),
16
+ id: idSchema,
17
+ path: z.string().optional().describe("JSON pointer to return only a subtree, e.g. \"/layout/sections/0\"."),
18
+ ...depthShape(scope),
19
+ ...localeShape(scope, {
20
+ required: false,
21
+ description: "Locale to read. Defaults to the default locale."
22
+ }),
23
+ draft: z.boolean().optional().describe("Return the latest draft. Default true.")
24
+ }),
25
+ handler: async (args, scope) => {
26
+ ensureAllowed(scope, args.collection, "read");
27
+ const locale = localeOf(scope, args.locale);
28
+ const doc = await scope.req.payload.findByID({
29
+ collection: args.collection,
30
+ id: args.id,
31
+ depth: args.depth ?? 0,
32
+ draft: args.draft ?? true,
33
+ overrideAccess: false,
34
+ req: scope.req,
35
+ ...locale === void 0 ? {} : { locale }
36
+ });
37
+ if (args.path === void 0 || args.path === "") return jsonResult(doc);
38
+ let value;
39
+ try {
40
+ value = Pointer.fromJSON(args.path).get(doc);
41
+ } catch {
42
+ return errorResult(`"${args.path}" is not a valid JSON pointer.`);
43
+ }
44
+ return jsonResult({
45
+ id: doc["id"],
46
+ status: doc["_status"],
47
+ updatedAt: doc["updatedAt"],
48
+ path: args.path,
49
+ value
50
+ });
51
+ }
52
+ };
53
+ //#endregion
54
+ export { getDocument };
@@ -0,0 +1,23 @@
1
+ import { createDocument } from "./create-document.mjs";
2
+ import { describeSchema } from "./describe-schema.mjs";
3
+ import { findDocuments } from "./find-documents.mjs";
4
+ import { getDocument } from "./get-document.mjs";
5
+ import { listCapabilities } from "./list-capabilities.mjs";
6
+ import { patchDocument } from "./patch-document.mjs";
7
+ import { validateDocument } from "./validate-document.mjs";
8
+ //#region src/tools/index.ts
9
+ /**
10
+ * The builtin tools in registration order. The surface is fixed: adding a
11
+ * collection, block or field never changes it. Typed over `never` because
12
+ * each tool validates its own arguments through its input schema.
13
+ */ const BUILTIN_TOOLS = [
14
+ listCapabilities,
15
+ describeSchema,
16
+ findDocuments,
17
+ getDocument,
18
+ patchDocument,
19
+ createDocument,
20
+ validateDocument
21
+ ];
22
+ //#endregion
23
+ export { BUILTIN_TOOLS };
@@ -0,0 +1,49 @@
1
+ import { jsonResult } from "../endpoint/result.mjs";
2
+ import { translateLabel } from "./shared.mjs";
3
+ import { staticDescription } from "../schema/walk.mjs";
4
+ import { hasDraftValidationEnabled } from "payload/shared";
5
+ //#region src/tools/list-capabilities.ts
6
+ const listCapabilities = {
7
+ name: "listCapabilities",
8
+ description: `Lists what this key may do: the collections it can read or write, their draft behaviour and id type, the configured locales, the limits in force and the custom tools available. Call it first to orient; nothing here changes with the content model.`,
9
+ annotations: {
10
+ readOnlyHint: true,
11
+ openWorldHint: false
12
+ },
13
+ isEnabled: () => true,
14
+ inputSchema: () => ({}),
15
+ handler: (_args, scope) => {
16
+ const { payload } = scope.req;
17
+ const collections = scope.options.collections.flatMap((entry) => {
18
+ const capability = scope.capabilities.collections[entry.slug];
19
+ const collection = payload.collections[entry.slug];
20
+ if (!capability || !collection || !(capability.read || capability.write)) return [];
21
+ const { config } = collection;
22
+ const description = staticDescription(config.admin.description);
23
+ return [{
24
+ slug: entry.slug,
25
+ labels: {
26
+ singular: translateLabel(scope, config.labels.singular, entry.slug),
27
+ plural: translateLabel(scope, config.labels.plural, entry.slug)
28
+ },
29
+ ...description === void 0 ? {} : { description },
30
+ read: capability.read,
31
+ write: capability.write,
32
+ drafts: entry.hasDrafts,
33
+ draftValidation: hasDraftValidationEnabled(config),
34
+ idType: collection.customIDType ?? payload.db.defaultIDType
35
+ }];
36
+ });
37
+ return Promise.resolve(jsonResult({
38
+ collections,
39
+ locales: scope.locales ? {
40
+ codes: scope.locales,
41
+ default: scope.defaultLocale
42
+ } : null,
43
+ limits: scope.options.limits,
44
+ tools: Object.entries(scope.capabilities.tools).filter(([, enabled]) => enabled).map(([name]) => name)
45
+ }));
46
+ }
47
+ };
48
+ //#endregion
49
+ export { listCapabilities };
@@ -0,0 +1,14 @@
1
+ //#region src/tools/names.ts
2
+ /**
3
+ * Names of the builtin tools. Custom tools must not reuse them.
4
+ */ const BUILTIN_TOOL_NAMES = [
5
+ "listCapabilities",
6
+ "describeSchema",
7
+ "findDocuments",
8
+ "getDocument",
9
+ "patchDocument",
10
+ "createDocument",
11
+ "validateDocument"
12
+ ];
13
+ //#endregion
14
+ export { BUILTIN_TOOL_NAMES };