payload-mcp-toolkit 0.2.0 → 0.3.1

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 (61) hide show
  1. package/README.md +150 -133
  2. package/dist/__tests__/introspection.test.js +141 -46
  3. package/dist/__tests__/introspection.test.js.map +1 -1
  4. package/dist/draft-workflow.d.ts +24 -19
  5. package/dist/draft-workflow.js +89 -42
  6. package/dist/draft-workflow.js.map +1 -1
  7. package/dist/index.d.ts +10 -15
  8. package/dist/index.js +36 -76
  9. package/dist/index.js.map +1 -1
  10. package/dist/introspection.d.ts +18 -7
  11. package/dist/introspection.js +113 -84
  12. package/dist/introspection.js.map +1 -1
  13. package/dist/prompts.d.ts +4 -4
  14. package/dist/prompts.js +47 -38
  15. package/dist/prompts.js.map +1 -1
  16. package/dist/resources.d.ts +9 -4
  17. package/dist/resources.js +43 -58
  18. package/dist/resources.js.map +1 -1
  19. package/dist/tools/_helpers.d.ts +14 -0
  20. package/dist/tools/_helpers.js +35 -0
  21. package/dist/tools/_helpers.js.map +1 -0
  22. package/dist/tools/patch-layout.d.ts +15 -85
  23. package/dist/tools/patch-layout.js +142 -69
  24. package/dist/tools/patch-layout.js.map +1 -1
  25. package/dist/tools/publish-draft.d.ts +1 -11
  26. package/dist/tools/publish-draft.js +8 -39
  27. package/dist/tools/publish-draft.js.map +1 -1
  28. package/dist/tools/resolve-reference.d.ts +1 -12
  29. package/dist/tools/resolve-reference.js +45 -85
  30. package/dist/tools/resolve-reference.js.map +1 -1
  31. package/dist/tools/safe-delete.d.ts +8 -13
  32. package/dist/tools/safe-delete.js +68 -100
  33. package/dist/tools/safe-delete.js.map +1 -1
  34. package/dist/tools/schedule-publish.d.ts +11 -21
  35. package/dist/tools/schedule-publish.js +18 -61
  36. package/dist/tools/schedule-publish.js.map +1 -1
  37. package/dist/tools/search-content.d.ts +1 -6
  38. package/dist/tools/search-content.js +52 -64
  39. package/dist/tools/search-content.js.map +1 -1
  40. package/dist/tools/update-document.d.ts +4 -14
  41. package/dist/tools/update-document.js +23 -72
  42. package/dist/tools/update-document.js.map +1 -1
  43. package/dist/tools/upload-media.d.ts +1 -10
  44. package/dist/tools/upload-media.js +11 -54
  45. package/dist/tools/upload-media.js.map +1 -1
  46. package/dist/tools/versions.d.ts +7 -20
  47. package/dist/tools/versions.js +25 -82
  48. package/dist/tools/versions.js.map +1 -1
  49. package/dist/types.d.ts +82 -53
  50. package/dist/types.js +6 -1
  51. package/dist/types.js.map +1 -1
  52. package/package.json +1 -1
  53. package/dist/rate-limiter.d.ts +0 -25
  54. package/dist/rate-limiter.js +0 -51
  55. package/dist/rate-limiter.js.map +0 -1
  56. package/dist/tools/compose-helpers.d.ts +0 -117
  57. package/dist/tools/compose-helpers.js +0 -236
  58. package/dist/tools/compose-helpers.js.map +0 -1
  59. package/dist/tools/compose-layout.d.ts +0 -139
  60. package/dist/tools/compose-layout.js +0 -61
  61. package/dist/tools/compose-layout.js.map +0 -1
package/dist/resources.js CHANGED
@@ -1,65 +1,50 @@
1
- /**
2
- * Generate MCP resources that expose the block catalog,
3
- * collection schemas, and relationship graph as static JSON.
4
- */ export function generateResources(schemas, catalog, relationships) {
1
+ /**
2
+ * Generate MCP resources that expose the introspected schema as static JSON.
3
+ *
4
+ * Four resources:
5
+ * - blocks://catalog — flat list of every block and its fields
6
+ * - blocks://nesting — per-blocks-field map of which slugs each field accepts
7
+ * - collections://schema — collection field metadata
8
+ * - collections://relationships — collection relationship graph
9
+ */ export function generateResources(schemas, catalog, nesting, relationships) {
5
10
  return [
6
- buildBlockCatalogResource(catalog),
7
- buildCollectionSchemaResource(schemas),
8
- buildRelationshipGraphResource(relationships)
11
+ buildJsonResource({
12
+ name: 'blockCatalog',
13
+ title: 'Block Catalog',
14
+ description: 'Flat list of every block type and its fields. Pair with the blockNesting resource to know where each block can be placed.',
15
+ uri: 'blocks://catalog',
16
+ payload: catalog
17
+ }),
18
+ buildJsonResource({
19
+ name: 'blockNesting',
20
+ title: 'Block Nesting Map',
21
+ description: 'For every blocks-typed field in the schema (in collections and inside other blocks), lists the block slugs that field accepts. Use this to compose nested layouts at any depth.',
22
+ uri: 'blocks://nesting',
23
+ payload: nesting
24
+ }),
25
+ buildJsonResource({
26
+ name: 'collectionSchema',
27
+ title: 'Collection Schema',
28
+ description: 'JSON schema of all collections — fields, select options, and relationship targets.',
29
+ uri: 'collections://schema',
30
+ payload: Object.fromEntries(schemas)
31
+ }),
32
+ buildJsonResource({
33
+ name: 'relationshipGraph',
34
+ title: 'Relationship Graph',
35
+ description: 'JSON representation of the collection relationship graph — which collections link to which.',
36
+ uri: 'collections://relationships',
37
+ payload: relationships
38
+ })
9
39
  ];
10
40
  }
11
- // ─── Resource builders ────────────────────────────────────────────
12
- function buildBlockCatalogResource(catalog) {
13
- const json = JSON.stringify(catalog, null, 2);
41
+ function buildJsonResource(args) {
42
+ const json = JSON.stringify(args.payload, null, 2);
14
43
  return {
15
- name: 'blockCatalog',
16
- title: 'Block Catalog',
17
- description: 'JSON catalog of all block types — section/leaf hierarchy, nesting rules, and required fields.',
18
- uri: 'blocks://catalog',
19
- mimeType: 'application/json',
20
- handler (uri) {
21
- return {
22
- contents: [
23
- {
24
- uri: uri.href,
25
- text: json
26
- }
27
- ]
28
- };
29
- }
30
- };
31
- }
32
- function buildCollectionSchemaResource(schemas) {
33
- const obj = {};
34
- for (const [slug, schema] of schemas){
35
- obj[slug] = schema;
36
- }
37
- const json = JSON.stringify(obj, null, 2);
38
- return {
39
- name: 'collectionSchema',
40
- title: 'Collection Schema',
41
- description: 'JSON schema of all collections — fields, select options, and relationship targets.',
42
- uri: 'collections://schema',
43
- mimeType: 'application/json',
44
- handler (uri) {
45
- return {
46
- contents: [
47
- {
48
- uri: uri.href,
49
- text: json
50
- }
51
- ]
52
- };
53
- }
54
- };
55
- }
56
- function buildRelationshipGraphResource(relationships) {
57
- const json = JSON.stringify(relationships, null, 2);
58
- return {
59
- name: 'relationshipGraph',
60
- title: 'Relationship Graph',
61
- description: 'JSON representation of the collection relationship graph — which collections link to which.',
62
- uri: 'collections://relationships',
44
+ name: args.name,
45
+ title: args.title,
46
+ description: args.description,
47
+ uri: args.uri,
63
48
  mimeType: 'application/json',
64
49
  handler (uri) {
65
50
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resources.ts"],"sourcesContent":["import type {\n BlockCatalog,\n CollectionSchema,\n RelationshipEdge,\n} from './types'\n\n/**\n * Generate MCP resources that expose the block catalog,\n * collection schemas, and relationship graph as static JSON.\n */\nexport function generateResources(\n schemas: Map<string, CollectionSchema>,\n catalog: BlockCatalog,\n relationships: RelationshipEdge[],\n) {\n return [\n buildBlockCatalogResource(catalog),\n buildCollectionSchemaResource(schemas),\n buildRelationshipGraphResource(relationships),\n ]\n}\n\n// ─── Resource builders ────────────────────────────────────────────\n\nfunction buildBlockCatalogResource(catalog: BlockCatalog) {\n const json = JSON.stringify(catalog, null, 2)\n\n return {\n name: 'blockCatalog',\n title: 'Block Catalog',\n description:\n 'JSON catalog of all block types section/leaf hierarchy, nesting rules, and required fields.',\n uri: 'blocks://catalog',\n mimeType: 'application/json',\n handler(uri: URL) {\n return {\n contents: [{ uri: uri.href, text: json }],\n }\n },\n }\n}\n\nfunction buildCollectionSchemaResource(schemas: Map<string, CollectionSchema>) {\n const obj: Record<string, CollectionSchema> = {}\n for (const [slug, schema] of schemas) {\n obj[slug] = schema\n }\n const json = JSON.stringify(obj, null, 2)\n\n return {\n name: 'collectionSchema',\n title: 'Collection Schema',\n description:\n 'JSON schema of all collections — fields, select options, and relationship targets.',\n uri: 'collections://schema',\n mimeType: 'application/json',\n handler(uri: URL) {\n return {\n contents: [{ uri: uri.href, text: json }],\n }\n },\n }\n}\n\nfunction buildRelationshipGraphResource(relationships: RelationshipEdge[]) {\n const json = JSON.stringify(relationships, null, 2)\n\n return {\n name: 'relationshipGraph',\n title: 'Relationship Graph',\n description:\n 'JSON representation of the collection relationship graph — which collections link to which.',\n uri: 'collections://relationships',\n mimeType: 'application/json',\n handler(uri: URL) {\n return {\n contents: [{ uri: uri.href, text: json }],\n }\n },\n }\n}\n"],"names":["generateResources","schemas","catalog","relationships","buildBlockCatalogResource","buildCollectionSchemaResource","buildRelationshipGraphResource","json","JSON","stringify","name","title","description","uri","mimeType","handler","contents","href","text","obj","slug","schema"],"mappings":"AAMA;;;CAGC,GACD,OAAO,SAASA,kBACdC,OAAsC,EACtCC,OAAqB,EACrBC,aAAiC;IAEjC,OAAO;QACLC,0BAA0BF;QAC1BG,8BAA8BJ;QAC9BK,+BAA+BH;KAChC;AACH;AAEA,qEAAqE;AAErE,SAASC,0BAA0BF,OAAqB;IACtD,MAAMK,OAAOC,KAAKC,SAAS,CAACP,SAAS,MAAM;IAE3C,OAAO;QACLQ,MAAM;QACNC,OAAO;QACPC,aACE;QACFC,KAAK;QACLC,UAAU;QACVC,SAAQF,GAAQ;YACd,OAAO;gBACLG,UAAU;oBAAC;wBAAEH,KAAKA,IAAII,IAAI;wBAAEC,MAAMX;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF;AAEA,SAASF,8BAA8BJ,OAAsC;IAC3E,MAAMkB,MAAwC,CAAC;IAC/C,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAIpB,QAAS;QACpCkB,GAAG,CAACC,KAAK,GAAGC;IACd;IACA,MAAMd,OAAOC,KAAKC,SAAS,CAACU,KAAK,MAAM;IAEvC,OAAO;QACLT,MAAM;QACNC,OAAO;QACPC,aACE;QACFC,KAAK;QACLC,UAAU;QACVC,SAAQF,GAAQ;YACd,OAAO;gBACLG,UAAU;oBAAC;wBAAEH,KAAKA,IAAII,IAAI;wBAAEC,MAAMX;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF;AAEA,SAASD,+BAA+BH,aAAiC;IACvE,MAAMI,OAAOC,KAAKC,SAAS,CAACN,eAAe,MAAM;IAEjD,OAAO;QACLO,MAAM;QACNC,OAAO;QACPC,aACE;QACFC,KAAK;QACLC,UAAU;QACVC,SAAQF,GAAQ;YACd,OAAO;gBACLG,UAAU;oBAAC;wBAAEH,KAAKA,IAAII,IAAI;wBAAEC,MAAMX;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF"}
1
+ {"version":3,"sources":["../src/resources.ts"],"sourcesContent":["import type {\r\n BlockCatalog,\r\n BlockNestingMap,\r\n CollectionSchema,\r\n RelationshipEdge,\r\n} from './types'\r\n\r\n/**\r\n * Generate MCP resources that expose the introspected schema as static JSON.\r\n *\r\n * Four resources:\r\n * - blocks://catalog — flat list of every block and its fields\r\n * - blocks://nesting — per-blocks-field map of which slugs each field accepts\r\n * - collections://schema — collection field metadata\r\n * - collections://relationships collection relationship graph\r\n */\r\nexport function generateResources(\r\n schemas: Map<string, CollectionSchema>,\r\n catalog: BlockCatalog,\r\n nesting: BlockNestingMap,\r\n relationships: RelationshipEdge[],\r\n) {\r\n return [\r\n buildJsonResource({\r\n name: 'blockCatalog',\r\n title: 'Block Catalog',\r\n description:\r\n 'Flat list of every block type and its fields. Pair with the blockNesting resource to know where each block can be placed.',\r\n uri: 'blocks://catalog',\r\n payload: catalog,\r\n }),\r\n buildJsonResource({\r\n name: 'blockNesting',\r\n title: 'Block Nesting Map',\r\n description:\r\n 'For every blocks-typed field in the schema (in collections and inside other blocks), lists the block slugs that field accepts. Use this to compose nested layouts at any depth.',\r\n uri: 'blocks://nesting',\r\n payload: nesting,\r\n }),\r\n buildJsonResource({\r\n name: 'collectionSchema',\r\n title: 'Collection Schema',\r\n description:\r\n 'JSON schema of all collections — fields, select options, and relationship targets.',\r\n uri: 'collections://schema',\r\n payload: Object.fromEntries(schemas),\r\n }),\r\n buildJsonResource({\r\n name: 'relationshipGraph',\r\n title: 'Relationship Graph',\r\n description:\r\n 'JSON representation of the collection relationship graph — which collections link to which.',\r\n uri: 'collections://relationships',\r\n payload: relationships,\r\n }),\r\n ]\r\n}\r\n\r\nfunction buildJsonResource(args: {\r\n name: string\r\n title: string\r\n description: string\r\n uri: string\r\n payload: unknown\r\n}) {\r\n const json = JSON.stringify(args.payload, null, 2)\r\n return {\r\n name: args.name,\r\n title: args.title,\r\n description: args.description,\r\n uri: args.uri,\r\n mimeType: 'application/json',\r\n handler(uri: URL) {\r\n return {\r\n contents: [{ uri: uri.href, text: json }],\r\n }\r\n },\r\n }\r\n}\r\n\r\n"],"names":["generateResources","schemas","catalog","nesting","relationships","buildJsonResource","name","title","description","uri","payload","Object","fromEntries","args","json","JSON","stringify","mimeType","handler","contents","href","text"],"mappings":"AAOA;;;;;;;;CAQC,GACD,OAAO,SAASA,kBACdC,OAAsC,EACtCC,OAAqB,EACrBC,OAAwB,EACxBC,aAAiC;IAEjC,OAAO;QACLC,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASR;QACX;QACAG,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASP;QACX;QACAE,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASC,OAAOC,WAAW,CAACX;QAC9B;QACAI,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASN;QACX;KACD;AACH;AAEA,SAASC,kBAAkBQ,IAM1B;IACC,MAAMC,OAAOC,KAAKC,SAAS,CAACH,KAAKH,OAAO,EAAE,MAAM;IAChD,OAAO;QACLJ,MAAMO,KAAKP,IAAI;QACfC,OAAOM,KAAKN,KAAK;QACjBC,aAAaK,KAAKL,WAAW;QAC7BC,KAAKI,KAAKJ,GAAG;QACbQ,UAAU;QACVC,SAAQT,GAAQ;YACd,OAAO;gBACLU,UAAU;oBAAC;wBAAEV,KAAKA,IAAIW,IAAI;wBAAEC,MAAMP;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF"}
@@ -0,0 +1,14 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ export interface McpTextResponse {
3
+ content: Array<{
4
+ type: 'text';
5
+ text: string;
6
+ }>;
7
+ }
8
+ export declare const DRAFT_NOTE = " Document is in draft status \u2014 use publishDraft to make it live.";
9
+ export declare function textResponse(text: string): McpTextResponse;
10
+ export declare function jsonResponse(payload: unknown): McpTextResponse;
11
+ export declare function errorMessage(error: unknown): string;
12
+ export declare function stampMcpContext(req: PayloadRequest): void;
13
+ export declare function getDocDisplayName(doc: unknown, fallback: string): string;
14
+ export declare function requireDraftCollection(collection: string, draftCollections: Set<string>, noun?: string): McpTextResponse | null;
@@ -0,0 +1,35 @@
1
+ export const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.';
2
+ export function textResponse(text) {
3
+ return {
4
+ content: [
5
+ {
6
+ type: 'text',
7
+ text
8
+ }
9
+ ]
10
+ };
11
+ }
12
+ export function jsonResponse(payload) {
13
+ return textResponse(JSON.stringify(payload));
14
+ }
15
+ export function errorMessage(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ export function stampMcpContext(req) {
19
+ req.context = {
20
+ ...req.context,
21
+ source: 'mcp'
22
+ };
23
+ }
24
+ export function getDocDisplayName(doc, fallback) {
25
+ const d = doc;
26
+ return typeof d?.name === 'string' && d.name || typeof d?.title === 'string' && d.title || typeof d?.slug === 'string' && d.slug || fallback;
27
+ }
28
+ export function requireDraftCollection(collection, draftCollections, noun = 'drafts') {
29
+ if (draftCollections.has(collection)) return null;
30
+ return textResponse(`Error: Collection "${collection}" does not support ${noun}. ` + `Draft-enabled collections: ${[
31
+ ...draftCollections
32
+ ].join(', ') || 'none'}`);
33
+ }
34
+
35
+ //# sourceMappingURL=_helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/tools/_helpers.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nexport interface McpTextResponse {\n content: Array<{ type: 'text'; text: string }>\n}\n\nexport const DRAFT_NOTE = ' Document is in draft status — use publishDraft to make it live.'\n\nexport function textResponse(text: string): McpTextResponse {\n return { content: [{ type: 'text', text }] }\n}\n\nexport function jsonResponse(payload: unknown): McpTextResponse {\n return textResponse(JSON.stringify(payload))\n}\n\nexport function errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport function stampMcpContext(req: PayloadRequest): void {\n req.context = { ...req.context, source: 'mcp' }\n}\n\nexport function getDocDisplayName(doc: unknown, fallback: string): string {\n const d = doc as Record<string, unknown> | null | undefined\n return (\n (typeof d?.name === 'string' && d.name) ||\n (typeof d?.title === 'string' && d.title) ||\n (typeof d?.slug === 'string' && d.slug) ||\n fallback\n )\n}\n\nexport function requireDraftCollection(\n collection: string,\n draftCollections: Set<string>,\n noun = 'drafts',\n): McpTextResponse | null {\n if (draftCollections.has(collection)) return null\n return textResponse(\n `Error: Collection \"${collection}\" does not support ${noun}. ` +\n `Draft-enabled collections: ${[...draftCollections].join(', ') || 'none'}`,\n )\n}\n"],"names":["DRAFT_NOTE","textResponse","text","content","type","jsonResponse","payload","JSON","stringify","errorMessage","error","Error","message","String","stampMcpContext","req","context","source","getDocDisplayName","doc","fallback","d","name","title","slug","requireDraftCollection","collection","draftCollections","noun","has","join"],"mappings":"AAMA,OAAO,MAAMA,aAAa,mEAAkE;AAE5F,OAAO,SAASC,aAAaC,IAAY;IACvC,OAAO;QAAEC,SAAS;YAAC;gBAAEC,MAAM;gBAAQF;YAAK;SAAE;IAAC;AAC7C;AAEA,OAAO,SAASG,aAAaC,OAAgB;IAC3C,OAAOL,aAAaM,KAAKC,SAAS,CAACF;AACrC;AAEA,OAAO,SAASG,aAAaC,KAAc;IACzC,OAAOA,iBAAiBC,QAAQD,MAAME,OAAO,GAAGC,OAAOH;AACzD;AAEA,OAAO,SAASI,gBAAgBC,GAAmB;IACjDA,IAAIC,OAAO,GAAG;QAAE,GAAGD,IAAIC,OAAO;QAAEC,QAAQ;IAAM;AAChD;AAEA,OAAO,SAASC,kBAAkBC,GAAY,EAAEC,QAAgB;IAC9D,MAAMC,IAAIF;IACV,OACE,AAAC,OAAOE,GAAGC,SAAS,YAAYD,EAAEC,IAAI,IACrC,OAAOD,GAAGE,UAAU,YAAYF,EAAEE,KAAK,IACvC,OAAOF,GAAGG,SAAS,YAAYH,EAAEG,IAAI,IACtCJ;AAEJ;AAEA,OAAO,SAASK,uBACdC,UAAkB,EAClBC,gBAA6B,EAC7BC,OAAO,QAAQ;IAEf,IAAID,iBAAiBE,GAAG,CAACH,aAAa,OAAO;IAC7C,OAAOzB,aACL,CAAC,mBAAmB,EAAEyB,WAAW,mBAAmB,EAAEE,KAAK,EAAE,CAAC,GAC5D,CAAC,2BAA2B,EAAE;WAAID;KAAiB,CAACG,IAAI,CAAC,SAAS,QAAQ;AAEhF"}
@@ -1,93 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import type { PayloadRequest } from 'payload';
3
- import type { BlockCatalog } from '../types';
4
- import { type SectionInput } from './compose-helpers';
3
+ import type { BlockCatalog, BlockNestingMap } from '../types';
5
4
  /**
6
- * Creates the patchLayout MCP tool a surgical wrapper around composePageLayout
7
- * that mutates a single document's layout-style field directly, never round-tripping
8
- * the entire array through updateDocument.
5
+ * patchLayout surgical wrapper that mutates a single document's blocks-typed
6
+ * field directly without round-tripping the entire array through updateDocument.
9
7
  *
10
- * Why this exists: prompting an LLM to "add a CTA at the bottom of the home page"
11
- * via updateDocument forces it to send the entire layout array, which one bad token
12
- * can wipe out. patchLayout fetches the current layout itself, applies a scoped
13
- * operation, and writes back — the LLM only ever describes the delta.
8
+ * Why this exists: prompting an LLM to "add a CTA at the bottom of the home
9
+ * page" via updateDocument forces it to send the whole layout array, which one
10
+ * bad token can wipe out. patchLayout fetches the current array itself,
11
+ * applies a scoped operation, and writes back — the LLM only describes the
12
+ * delta.
14
13
  *
15
- * Defaults to layoutField "layout" but accepts any block-array field name.
14
+ * Validation walks every block recursively against the introspected
15
+ * BlockNestingMap, so arbitrarily-nested layouts work as long as each
16
+ * `blocks`-typed field's content matches that field's allow list.
16
17
  */
17
- export declare function createPatchLayoutTool(catalog: BlockCatalog, draftCollections: Set<string>): {
18
+ export declare function createPatchLayoutTool(catalog: BlockCatalog, nesting: BlockNestingMap, draftCollections: Set<string>): {
18
19
  name: string;
19
20
  description: string;
20
21
  parameters: {
21
22
  collection: z.ZodString;
22
23
  documentId: z.ZodString;
23
24
  layoutField: z.ZodDefault<z.ZodOptional<z.ZodString>>;
24
- sections: z.ZodArray<z.ZodObject<{
25
- sectionType: z.ZodString;
26
- config: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
27
- content: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
- blockType: z.ZodString;
29
- fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
30
- }, "strip", z.ZodTypeAny, {
31
- blockType: string;
32
- fields?: Record<string, unknown> | undefined;
33
- }, {
34
- blockType: string;
35
- fields?: Record<string, unknown> | undefined;
36
- }>, "many">>;
37
- leftColumn: z.ZodOptional<z.ZodArray<z.ZodObject<{
38
- blockType: z.ZodString;
39
- fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
40
- }, "strip", z.ZodTypeAny, {
41
- blockType: string;
42
- fields?: Record<string, unknown> | undefined;
43
- }, {
44
- blockType: string;
45
- fields?: Record<string, unknown> | undefined;
46
- }>, "many">>;
47
- rightColumn: z.ZodOptional<z.ZodArray<z.ZodObject<{
48
- blockType: z.ZodString;
49
- fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
50
- }, "strip", z.ZodTypeAny, {
51
- blockType: string;
52
- fields?: Record<string, unknown> | undefined;
53
- }, {
54
- blockType: string;
55
- fields?: Record<string, unknown> | undefined;
56
- }>, "many">>;
57
- fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
58
- }, "strip", z.ZodTypeAny, {
59
- sectionType: string;
60
- fields?: Record<string, unknown> | undefined;
61
- config?: Record<string, unknown> | undefined;
62
- content?: {
63
- blockType: string;
64
- fields?: Record<string, unknown> | undefined;
65
- }[] | undefined;
66
- leftColumn?: {
67
- blockType: string;
68
- fields?: Record<string, unknown> | undefined;
69
- }[] | undefined;
70
- rightColumn?: {
71
- blockType: string;
72
- fields?: Record<string, unknown> | undefined;
73
- }[] | undefined;
74
- }, {
75
- sectionType: string;
76
- fields?: Record<string, unknown> | undefined;
77
- config?: Record<string, unknown> | undefined;
78
- content?: {
79
- blockType: string;
80
- fields?: Record<string, unknown> | undefined;
81
- }[] | undefined;
82
- leftColumn?: {
83
- blockType: string;
84
- fields?: Record<string, unknown> | undefined;
85
- }[] | undefined;
86
- rightColumn?: {
87
- blockType: string;
88
- fields?: Record<string, unknown> | undefined;
89
- }[] | undefined;
90
- }>, "many">;
25
+ blocks: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
91
26
  operation: z.ZodEnum<["append", "prepend", "insertAt", "replaceAt", "full"]>;
92
27
  insertIndex: z.ZodOptional<z.ZodNumber>;
93
28
  };
@@ -95,13 +30,8 @@ export declare function createPatchLayoutTool(catalog: BlockCatalog, draftCollec
95
30
  collection: string;
96
31
  documentId: string;
97
32
  layoutField?: string;
98
- sections: SectionInput[];
33
+ blocks: Array<Record<string, unknown>>;
99
34
  operation: "append" | "prepend" | "insertAt" | "replaceAt" | "full";
100
35
  insertIndex?: number;
101
- }, req: PayloadRequest, _extra: unknown) => Promise<{
102
- content: {
103
- type: "text";
104
- text: string;
105
- }[];
106
- }>;
36
+ }, req: PayloadRequest, _extra: unknown) => Promise<import("./_helpers").McpTextResponse>;
107
37
  };
@@ -1,79 +1,80 @@
1
1
  import { z } from 'zod';
2
- import { applyOperation, buildHint, composeSections, sectionSchema } from './compose-helpers';
2
+ import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpContext } from './_helpers';
3
3
  /**
4
- * Creates the patchLayout MCP tool a surgical wrapper around composePageLayout
5
- * that mutates a single document's layout-style field directly, never round-tripping
6
- * the entire array through updateDocument.
4
+ * patchLayout surgical wrapper that mutates a single document's blocks-typed
5
+ * field directly without round-tripping the entire array through updateDocument.
7
6
  *
8
- * Why this exists: prompting an LLM to "add a CTA at the bottom of the home page"
9
- * via updateDocument forces it to send the entire layout array, which one bad token
10
- * can wipe out. patchLayout fetches the current layout itself, applies a scoped
11
- * operation, and writes back — the LLM only ever describes the delta.
7
+ * Why this exists: prompting an LLM to "add a CTA at the bottom of the home
8
+ * page" via updateDocument forces it to send the whole layout array, which one
9
+ * bad token can wipe out. patchLayout fetches the current array itself,
10
+ * applies a scoped operation, and writes back — the LLM only describes the
11
+ * delta.
12
12
  *
13
- * Defaults to layoutField "layout" but accepts any block-array field name.
14
- */ export function createPatchLayoutTool(catalog, draftCollections) {
15
- const sectionSlugs = catalog.sections.map((s)=>s.slug);
16
- const leafSlugs = catalog.leaves.map((l)=>l.slug);
13
+ * Validation walks every block recursively against the introspected
14
+ * BlockNestingMap, so arbitrarily-nested layouts work as long as each
15
+ * `blocks`-typed field's content matches that field's allow list.
16
+ */ export function createPatchLayoutTool(catalog, nesting, draftCollections) {
17
+ const allBlockSlugs = new Set(catalog.blocks.map((b)=>b.slug));
18
+ // Lookups keyed by `<owner>:<fieldPath>` for O(1) access during recursive validation.
19
+ const nestingByCollectionField = new Map();
20
+ const nestingByBlockField = new Map();
21
+ for (const edge of nesting){
22
+ const key = `${edge.owner}:${edge.fieldPath}`;
23
+ if (edge.ownerType === 'collection') {
24
+ nestingByCollectionField.set(key, edge.acceptedBlockSlugs);
25
+ } else {
26
+ nestingByBlockField.set(key, edge.acceptedBlockSlugs);
27
+ }
28
+ }
17
29
  return {
18
30
  name: 'patchLayout',
19
- description: 'Surgically modify a document\'s block-array field (e.g. "layout") without ' + 'sending the whole array. Pass only the sections to add or replace plus an operation ' + '(append, prepend, insertAt, replaceAt). The current layout is fetched server-side ' + 'and the operation is applied atomically. Safer than updateDocument for incremental edits. ' + `Available sections: ${sectionSlugs.join(', ')}. Available leaves: ${leafSlugs.join(', ')}.`,
31
+ description: 'Surgically modify a document\'s blocks-typed field (e.g. "layout") without sending the whole array. Pass the blocks to add/replace plus an operation (append, prepend, insertAt, replaceAt, full). The current array is fetched server-side and the operation is applied atomically. Each block must include a `blockType` plus its fields; nested `blocks`-typed fields can contain arbitrarily-deep block arrays as long as each level matches the schema. Use the `blockNesting` resource to see which slugs each field accepts.',
20
32
  parameters: {
21
33
  collection: z.string().describe('The collection slug containing the document'),
22
34
  documentId: z.string().describe('The ID of the document to patch'),
23
- layoutField: z.string().optional().default('layout').describe('Name of the block-array field to patch (default "layout")'),
24
- sections: z.array(sectionSchema).describe('Sections to compose. Same shape as composePageLayout.'),
35
+ layoutField: z.string().optional().default('layout').describe('Name of the blocks-typed field to patch (default "layout")'),
36
+ blocks: z.array(z.record(z.string(), z.unknown())).describe('Blocks to compose. Each must have a `blockType` discriminator plus any block-specific fields. Nested blocks fields hold their own `blocks` arrays at any depth.'),
25
37
  operation: z.enum([
26
38
  'append',
27
39
  'prepend',
28
40
  'insertAt',
29
41
  'replaceAt',
30
42
  'full'
31
- ]).describe('How to apply the sections: append (end), prepend (start), insertAt (at index), ' + 'replaceAt (overwrite N starting at index), full (replace entire array — use with care).'),
43
+ ]).describe('How to apply the blocks: append (end), prepend (start), insertAt (at index), replaceAt (overwrite N starting at index), full (replace entire array — use with care).'),
32
44
  insertIndex: z.number().optional().describe('Index for insertAt/replaceAt operations')
33
45
  },
34
46
  handler: async (args, req, _extra)=>{
35
- const { collection, documentId, layoutField = 'layout', sections, operation, insertIndex } = args;
36
- // Compose the new sections first — fail fast on validation errors before touching the doc
37
- const { blocks, errors } = composeSections(sections, catalog);
38
- if (errors.length > 0) {
39
- return {
40
- content: [
41
- {
42
- type: 'text',
43
- text: JSON.stringify({
44
- success: false,
45
- errors,
46
- hint: buildHint(catalog)
47
- })
48
- }
47
+ const { collection, documentId, layoutField = 'layout', blocks, operation, insertIndex } = args;
48
+ const rootKey = `${collection}:${layoutField}`;
49
+ const rootAllowed = nestingByCollectionField.get(rootKey);
50
+ if (!rootAllowed) {
51
+ return errorResponse(`Field "${layoutField}" on collection "${collection}" is not a blocks-typed field, or no nesting map entry exists for it.`, {
52
+ availableFields: [
53
+ ...nestingByCollectionField.keys()
49
54
  ]
50
- };
55
+ });
56
+ }
57
+ const errors = [];
58
+ validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors);
59
+ if (errors.length > 0) {
60
+ return errorResponse('Block validation failed.', {
61
+ errors
62
+ });
51
63
  }
52
- req.context = {
53
- ...req.context,
54
- source: 'mcp'
55
- };
56
- // Fetch current document so we can read the existing layout array
64
+ stampMcpContext(req);
57
65
  let existing;
58
66
  try {
59
67
  existing = await req.payload.findByID({
60
68
  collection: collection,
61
69
  id: documentId,
70
+ depth: 0,
62
71
  draft: true,
63
72
  req,
64
73
  overrideAccess: false,
65
74
  user: req.user
66
75
  });
67
76
  } catch (error) {
68
- const message = error instanceof Error ? error.message : String(error);
69
- return {
70
- content: [
71
- {
72
- type: 'text',
73
- text: `Error fetching ${collection}#${documentId}: ${message}`
74
- }
75
- ]
76
- };
77
+ return errorResponse(`Error fetching ${collection}#${documentId}: ${errorMessage(error)}`);
77
78
  }
78
79
  const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : [];
79
80
  const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout);
@@ -90,34 +91,106 @@ import { applyOperation, buildHint, composeSections, sectionSchema } from './com
90
91
  overrideAccess: false,
91
92
  user: req.user
92
93
  });
93
- const displayName = updated.name || updated.title || updated.slug || documentId;
94
- const draftNote = isDraftCollection ? ' Document is in draft status — use publishDraft to make it live.' : '';
95
- return {
96
- content: [
97
- {
98
- type: 'text',
99
- text: JSON.stringify({
100
- success: true,
101
- message: `Patched ${layoutField} on "${displayName}" (${collection}#${documentId}). ` + `Operation: ${operation}. Block count: ${finalLayout.length}.` + draftNote,
102
- blockCount: finalLayout.length,
103
- operation
104
- })
105
- }
106
- ]
107
- };
94
+ const displayName = getDocDisplayName(updated, documentId);
95
+ const draftNote = isDraftCollection ? DRAFT_NOTE : '';
96
+ return jsonResponse({
97
+ success: true,
98
+ message: `Patched ${layoutField} on "${displayName}" (${collection}#${documentId}). ` + `Operation: ${operation}. Block count: ${finalLayout.length}.` + draftNote,
99
+ blockCount: finalLayout.length,
100
+ operation
101
+ });
108
102
  } catch (error) {
109
- const message = error instanceof Error ? error.message : String(error);
110
- return {
111
- content: [
112
- {
113
- type: 'text',
114
- text: `Error patching ${collection}#${documentId}: ${message}`
115
- }
116
- ]
117
- };
103
+ return errorResponse(`Error patching ${collection}#${documentId}: ${errorMessage(error)}`);
118
104
  }
119
105
  }
120
106
  };
121
107
  }
108
+ function errorResponse(message, extra) {
109
+ return jsonResponse({
110
+ success: false,
111
+ error: message,
112
+ ...extra ?? {}
113
+ });
114
+ }
115
+ /**
116
+ * Recursively validate a block array against an allow list, descending into
117
+ * each block's own `blocks`-typed fields when present.
118
+ */ function validateBlockList(blocks, allowedSlugs, pathLabel, allBlockSlugs, nestingByBlockField, errors) {
119
+ for(let i = 0; i < blocks.length; i++){
120
+ const block = blocks[i];
121
+ const here = `${pathLabel}[${i}]`;
122
+ if (!block || typeof block !== 'object') {
123
+ errors.push(`${here}: not an object`);
124
+ continue;
125
+ }
126
+ const slug = block.blockType;
127
+ if (typeof slug !== 'string' || !slug) {
128
+ errors.push(`${here}: missing string \`blockType\``);
129
+ continue;
130
+ }
131
+ if (!allBlockSlugs.has(slug)) {
132
+ errors.push(`${here}: unknown blockType "${slug}". Known: ${[
133
+ ...allBlockSlugs
134
+ ].join(', ')}`);
135
+ continue;
136
+ }
137
+ if (!allowedSlugs.includes(slug)) {
138
+ errors.push(`${here}: blockType "${slug}" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`);
139
+ continue;
140
+ }
141
+ // Any value that is itself an array of objects with `blockType` is treated
142
+ // as a nested blocks field. The field name is cross-checked against the
143
+ // nesting map to find the next-level allow list.
144
+ for (const [fieldName, value] of Object.entries(block)){
145
+ if (!Array.isArray(value)) continue;
146
+ if (value.length === 0) continue;
147
+ if (!value.every((v)=>v && typeof v === 'object' && 'blockType' in v)) continue;
148
+ const nextKey = `${slug}:${fieldName}`;
149
+ const nextAllowed = nestingByBlockField.get(nextKey);
150
+ if (!nextAllowed) {
151
+ errors.push(`${here}.${fieldName}: block "${slug}" has no blocks field named "${fieldName}" in the schema`);
152
+ continue;
153
+ }
154
+ validateBlockList(value, nextAllowed, `${here}.${fieldName}`, allBlockSlugs, nestingByBlockField, errors);
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Apply a list operation against an existing array of blocks.
160
+ * `full` always replaces; the rest preserve the existing array.
161
+ */ function applyOperation(newBlocks, operation, insertIndex, existingLayout) {
162
+ if (operation === 'full' || !existingLayout) {
163
+ return newBlocks;
164
+ }
165
+ const existing = [
166
+ ...existingLayout
167
+ ];
168
+ if (operation === 'append') return [
169
+ ...existing,
170
+ ...newBlocks
171
+ ];
172
+ if (operation === 'prepend') return [
173
+ ...newBlocks,
174
+ ...existing
175
+ ];
176
+ if (operation === 'insertAt') {
177
+ if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {
178
+ return [
179
+ ...existing,
180
+ ...newBlocks
181
+ ];
182
+ }
183
+ existing.splice(insertIndex, 0, ...newBlocks);
184
+ return existing;
185
+ }
186
+ if (operation === 'replaceAt') {
187
+ if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {
188
+ return existing;
189
+ }
190
+ existing.splice(insertIndex, newBlocks.length, ...newBlocks);
191
+ return existing;
192
+ }
193
+ return newBlocks;
194
+ }
122
195
 
123
196
  //# sourceMappingURL=patch-layout.js.map