payload-mcp-toolkit 0.2.0 → 0.3.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.
package/dist/resources.js CHANGED
@@ -1,21 +1,50 @@
1
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) {
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: collectionSchemasToObject(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',
44
+ name: args.name,
45
+ title: args.title,
46
+ description: args.description,
47
+ uri: args.uri,
19
48
  mimeType: 'application/json',
20
49
  handler (uri) {
21
50
  return {
@@ -29,49 +58,12 @@ function buildBlockCatalogResource(catalog) {
29
58
  }
30
59
  };
31
60
  }
32
- function buildCollectionSchemaResource(schemas) {
61
+ function collectionSchemasToObject(schemas) {
33
62
  const obj = {};
34
63
  for (const [slug, schema] of schemas){
35
64
  obj[slug] = schema;
36
65
  }
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',
63
- mimeType: 'application/json',
64
- handler (uri) {
65
- return {
66
- contents: [
67
- {
68
- uri: uri.href,
69
- text: json
70
- }
71
- ]
72
- };
73
- }
74
- };
66
+ return obj;
75
67
  }
76
68
 
77
69
  //# sourceMappingURL=resources.js.map
@@ -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 {\n BlockCatalog,\n BlockNestingMap,\n CollectionSchema,\n RelationshipEdge,\n} from './types'\n\n/**\n * Generate MCP resources that expose the introspected schema as static JSON.\n *\n * Four resources:\n * - blocks://catalog — flat list of every block and its fields\n * - blocks://nesting — per-blocks-field map of which slugs each field accepts\n * - collections://schema — collection field metadata\n * - collections://relationships collection relationship graph\n */\nexport function generateResources(\n schemas: Map<string, CollectionSchema>,\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n relationships: RelationshipEdge[],\n) {\n return [\n buildJsonResource({\n name: 'blockCatalog',\n title: 'Block Catalog',\n description:\n 'Flat list of every block type and its fields. Pair with the blockNesting resource to know where each block can be placed.',\n uri: 'blocks://catalog',\n payload: catalog,\n }),\n buildJsonResource({\n name: 'blockNesting',\n title: 'Block Nesting Map',\n description:\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.',\n uri: 'blocks://nesting',\n payload: nesting,\n }),\n buildJsonResource({\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 payload: collectionSchemasToObject(schemas),\n }),\n buildJsonResource({\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 payload: relationships,\n }),\n ]\n}\n\nfunction buildJsonResource(args: {\n name: string\n title: string\n description: string\n uri: string\n payload: unknown\n}) {\n const json = JSON.stringify(args.payload, null, 2)\n return {\n name: args.name,\n title: args.title,\n description: args.description,\n uri: args.uri,\n mimeType: 'application/json',\n handler(uri: URL) {\n return {\n contents: [{ uri: uri.href, text: json }],\n }\n },\n }\n}\n\nfunction collectionSchemasToObject(\n schemas: Map<string, CollectionSchema>,\n): Record<string, CollectionSchema> {\n const obj: Record<string, CollectionSchema> = {}\n for (const [slug, schema] of schemas) {\n obj[slug] = schema\n }\n return obj\n}\n"],"names":["generateResources","schemas","catalog","nesting","relationships","buildJsonResource","name","title","description","uri","payload","collectionSchemasToObject","args","json","JSON","stringify","mimeType","handler","contents","href","text","obj","slug","schema"],"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,0BAA0BV;QACrC;QACAI,kBAAkB;YAChBC,MAAM;YACNC,OAAO;YACPC,aACE;YACFC,KAAK;YACLC,SAASN;QACX;KACD;AACH;AAEA,SAASC,kBAAkBO,IAM1B;IACC,MAAMC,OAAOC,KAAKC,SAAS,CAACH,KAAKF,OAAO,EAAE,MAAM;IAChD,OAAO;QACLJ,MAAMM,KAAKN,IAAI;QACfC,OAAOK,KAAKL,KAAK;QACjBC,aAAaI,KAAKJ,WAAW;QAC7BC,KAAKG,KAAKH,GAAG;QACbO,UAAU;QACVC,SAAQR,GAAQ;YACd,OAAO;gBACLS,UAAU;oBAAC;wBAAET,KAAKA,IAAIU,IAAI;wBAAEC,MAAMP;oBAAK;iBAAE;YAC3C;QACF;IACF;AACF;AAEA,SAASF,0BACPV,OAAsC;IAEtC,MAAMoB,MAAwC,CAAC;IAC/C,KAAK,MAAM,CAACC,MAAMC,OAAO,IAAItB,QAAS;QACpCoB,GAAG,CAACC,KAAK,GAAGC;IACd;IACA,OAAOF;AACT"}
@@ -1,93 +1,29 @@
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
+ * Creates the patchLayout MCP tool — a surgical wrapper that mutates a single
6
+ * document's blocks-typed field directly without round-tripping the entire
7
+ * array through `updateDocument`.
9
8
  *
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.
9
+ * Why this exists: prompting an LLM to "add a CTA at the bottom of the home
10
+ * page" via updateDocument forces it to send the whole layout array, which one
11
+ * bad token can wipe out. patchLayout fetches the current array itself,
12
+ * applies a scoped operation, and writes back — the LLM only describes the
13
+ * delta.
14
14
  *
15
- * Defaults to layoutField "layout" but accepts any block-array field name.
15
+ * Validation walks every block recursively against the introspected
16
+ * BlockNestingMap, so arbitrarily-nested layouts work as long as each
17
+ * `blocks`-typed field's content matches that field's allow list.
16
18
  */
17
- export declare function createPatchLayoutTool(catalog: BlockCatalog, draftCollections: Set<string>): {
19
+ export declare function createPatchLayoutTool(catalog: BlockCatalog, nesting: BlockNestingMap, draftCollections: Set<string>): {
18
20
  name: string;
19
21
  description: string;
20
22
  parameters: {
21
23
  collection: z.ZodString;
22
24
  documentId: z.ZodString;
23
25
  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">;
26
+ blocks: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
91
27
  operation: z.ZodEnum<["append", "prepend", "insertAt", "replaceAt", "full"]>;
92
28
  insertIndex: z.ZodOptional<z.ZodNumber>;
93
29
  };
@@ -95,7 +31,7 @@ export declare function createPatchLayoutTool(catalog: BlockCatalog, draftCollec
95
31
  collection: string;
96
32
  documentId: string;
97
33
  layoutField?: string;
98
- sections: SectionInput[];
34
+ blocks: Array<Record<string, unknown>>;
99
35
  operation: "append" | "prepend" | "insertAt" | "replaceAt" | "full";
100
36
  insertIndex?: number;
101
37
  }, req: PayloadRequest, _extra: unknown) => Promise<{
@@ -1,59 +1,73 @@
1
1
  import { z } from 'zod';
2
- import { applyOperation, buildHint, composeSections, sectionSchema } from './compose-helpers';
3
2
  /**
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.
3
+ * Creates the patchLayout MCP tool — a surgical wrapper that mutates a single
4
+ * document's blocks-typed field directly without round-tripping the entire
5
+ * 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 = catalog.blocks.map((b)=>b.slug);
18
+ // Pre-build lookups keyed by `<owner>:<fieldPath>` for O(1) access during
19
+ // recursive validation.
20
+ const nestingByCollectionField = new Map();
21
+ const nestingByBlockField = new Map();
22
+ for (const edge of nesting){
23
+ const key = `${edge.owner}:${edge.fieldPath}`;
24
+ if (edge.ownerType === 'collection') {
25
+ nestingByCollectionField.set(key, edge.acceptedBlockSlugs);
26
+ } else {
27
+ nestingByBlockField.set(key, edge.acceptedBlockSlugs);
28
+ }
29
+ }
17
30
  return {
18
31
  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(', ')}.`,
32
+ 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
33
  parameters: {
21
34
  collection: z.string().describe('The collection slug containing the document'),
22
35
  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.'),
36
+ layoutField: z.string().optional().default('layout').describe('Name of the blocks-typed field to patch (default "layout")'),
37
+ 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
38
  operation: z.enum([
26
39
  'append',
27
40
  'prepend',
28
41
  'insertAt',
29
42
  'replaceAt',
30
43
  '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).'),
44
+ ]).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
45
  insertIndex: z.number().optional().describe('Index for insertAt/replaceAt operations')
33
46
  },
34
47
  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
- }
48
+ const { collection, documentId, layoutField = 'layout', blocks, operation, insertIndex } = args;
49
+ // Validate the incoming blocks against the nesting map for the target
50
+ // field before touching the database.
51
+ const rootKey = `${collection}:${layoutField}`;
52
+ const rootAllowed = nestingByCollectionField.get(rootKey);
53
+ if (!rootAllowed) {
54
+ return errorResponse(`Field "${layoutField}" on collection "${collection}" is not a blocks-typed field, or no nesting map entry exists for it.`, {
55
+ availableFields: [
56
+ ...nestingByCollectionField.keys()
49
57
  ]
50
- };
58
+ });
59
+ }
60
+ const errors = [];
61
+ validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors);
62
+ if (errors.length > 0) {
63
+ return errorResponse('Block validation failed.', {
64
+ errors
65
+ });
51
66
  }
52
67
  req.context = {
53
68
  ...req.context,
54
69
  source: 'mcp'
55
70
  };
56
- // Fetch current document so we can read the existing layout array
57
71
  let existing;
58
72
  try {
59
73
  existing = await req.payload.findByID({
@@ -66,14 +80,7 @@ import { applyOperation, buildHint, composeSections, sectionSchema } from './com
66
80
  });
67
81
  } catch (error) {
68
82
  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
- };
83
+ return errorResponse(`Error fetching ${collection}#${documentId}: ${message}`);
77
84
  }
78
85
  const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : [];
79
86
  const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout);
@@ -107,17 +114,102 @@ import { applyOperation, buildHint, composeSections, sectionSchema } from './com
107
114
  };
108
115
  } catch (error) {
109
116
  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
- };
117
+ return errorResponse(`Error patching ${collection}#${documentId}: ${message}`);
118
118
  }
119
119
  }
120
120
  };
121
121
  }
122
+ function errorResponse(message, extra) {
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: JSON.stringify({
128
+ success: false,
129
+ error: message,
130
+ ...extra ?? {}
131
+ })
132
+ }
133
+ ]
134
+ };
135
+ }
136
+ /**
137
+ * Recursively validate a block array against an allow list, descending into
138
+ * each block's own `blocks`-typed fields when present.
139
+ */ function validateBlockList(blocks, allowedSlugs, pathLabel, allBlockSlugs, nestingByBlockField, errors) {
140
+ for(let i = 0; i < blocks.length; i++){
141
+ const block = blocks[i];
142
+ const here = `${pathLabel}[${i}]`;
143
+ if (!block || typeof block !== 'object') {
144
+ errors.push(`${here}: not an object`);
145
+ continue;
146
+ }
147
+ const slug = block.blockType;
148
+ if (typeof slug !== 'string' || !slug) {
149
+ errors.push(`${here}: missing string \`blockType\``);
150
+ continue;
151
+ }
152
+ if (!allBlockSlugs.includes(slug)) {
153
+ errors.push(`${here}: unknown blockType "${slug}". Known: ${allBlockSlugs.join(', ')}`);
154
+ continue;
155
+ }
156
+ if (!allowedSlugs.includes(slug)) {
157
+ errors.push(`${here}: blockType "${slug}" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`);
158
+ continue;
159
+ }
160
+ // Recurse: any value on this block that is itself an array of objects with
161
+ // `blockType` is treated as a nested blocks field. Cross-check the field
162
+ // name against the nesting map so we know the allow list for the next level.
163
+ for (const [fieldName, value] of Object.entries(block)){
164
+ if (!Array.isArray(value)) continue;
165
+ if (value.length === 0) continue;
166
+ if (!value.every((v)=>v && typeof v === 'object' && 'blockType' in v)) continue;
167
+ const nextKey = `${slug}:${fieldName}`;
168
+ const nextAllowed = nestingByBlockField.get(nextKey);
169
+ if (!nextAllowed) {
170
+ errors.push(`${here}.${fieldName}: block "${slug}" has no blocks field named "${fieldName}" in the schema`);
171
+ continue;
172
+ }
173
+ validateBlockList(value, nextAllowed, `${here}.${fieldName}`, allBlockSlugs, nestingByBlockField, errors);
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Apply a list operation against an existing array of blocks.
179
+ * `full` always replaces; the rest preserve the existing array.
180
+ */ function applyOperation(newBlocks, operation, insertIndex, existingLayout) {
181
+ if (operation === 'full' || !existingLayout) {
182
+ return newBlocks;
183
+ }
184
+ const existing = [
185
+ ...existingLayout
186
+ ];
187
+ if (operation === 'append') return [
188
+ ...existing,
189
+ ...newBlocks
190
+ ];
191
+ if (operation === 'prepend') return [
192
+ ...newBlocks,
193
+ ...existing
194
+ ];
195
+ if (operation === 'insertAt') {
196
+ if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {
197
+ return [
198
+ ...existing,
199
+ ...newBlocks
200
+ ];
201
+ }
202
+ existing.splice(insertIndex, 0, ...newBlocks);
203
+ return existing;
204
+ }
205
+ if (operation === 'replaceAt') {
206
+ if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {
207
+ return existing;
208
+ }
209
+ existing.splice(insertIndex, newBlocks.length, ...newBlocks);
210
+ return existing;
211
+ }
212
+ return newBlocks;
213
+ }
122
214
 
123
215
  //# sourceMappingURL=patch-layout.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog } from '../types'\nimport {\n applyOperation,\n buildHint,\n composeSections,\n sectionSchema,\n type SectionInput,\n} from './compose-helpers'\n\n/**\n * Creates the patchLayout MCP tool — a surgical wrapper around composePageLayout\n * that mutates a single document's layout-style field directly, never round-tripping\n * the entire array through updateDocument.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home page\"\n * via updateDocument forces it to send the entire layout array, which one bad token\n * can wipe out. patchLayout fetches the current layout itself, applies a scoped\n * operation, and writes back — the LLM only ever describes the delta.\n *\n * Defaults to layoutField \"layout\" but accepts any block-array field name.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n draftCollections: Set<string>,\n) {\n const sectionSlugs = catalog.sections.map((s) => s.slug)\n const leafSlugs = catalog.leaves.map((l) => l.slug)\n\n return {\n name: 'patchLayout',\n description:\n 'Surgically modify a document\\'s block-array field (e.g. \"layout\") without ' +\n 'sending the whole array. Pass only the sections to add or replace plus an operation ' +\n '(append, prepend, insertAt, replaceAt). The current layout is fetched server-side ' +\n 'and the operation is applied atomically. Safer than updateDocument for incremental edits. ' +\n `Available sections: ${sectionSlugs.join(', ')}. Available leaves: ${leafSlugs.join(', ')}.`,\n parameters: {\n collection: z.string().describe('The collection slug containing the document'),\n documentId: z.string().describe('The ID of the document to patch'),\n layoutField: z\n .string()\n .optional()\n .default('layout')\n .describe('Name of the block-array field to patch (default \"layout\")'),\n sections: z\n .array(sectionSchema)\n .describe('Sections to compose. Same shape as composePageLayout.'),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n 'How to apply the sections: append (end), prepend (start), insertAt (at index), ' +\n 'replaceAt (overwrite N starting at index), full (replace entire array — use with care).',\n ),\n insertIndex: z\n .number()\n .optional()\n .describe('Index for insertAt/replaceAt operations'),\n },\n handler: async (\n args: {\n collection: string\n documentId: string\n layoutField?: string\n sections: SectionInput[]\n operation: 'append' | 'prepend' | 'insertAt' | 'replaceAt' | 'full'\n insertIndex?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n documentId,\n layoutField = 'layout',\n sections,\n operation,\n insertIndex,\n } = args\n\n // Compose the new sections first — fail fast on validation errors before touching the doc\n const { blocks, errors } = composeSections(sections, catalog)\n\n if (errors.length > 0) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n success: false,\n errors,\n hint: buildHint(catalog),\n }),\n },\n ],\n }\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n // Fetch current document so we can read the existing layout array\n let existing: any\n try {\n existing = await req.payload.findByID({\n collection: collection as any,\n id: documentId,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error fetching ${collection}#${documentId}: ${message}`,\n },\n ],\n }\n }\n\n const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : []\n const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const updated = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { [layoutField]: finalLayout } as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName =\n (updated as any).name ||\n (updated as any).title ||\n (updated as any).slug ||\n documentId\n\n const draftNote = isDraftCollection\n ? ' Document is in draft status — use publishDraft to make it live.'\n : ''\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n success: true,\n message:\n `Patched ${layoutField} on \"${displayName}\" (${collection}#${documentId}). ` +\n `Operation: ${operation}. Block count: ${finalLayout.length}.` +\n draftNote,\n blockCount: finalLayout.length,\n operation,\n }),\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n content: [\n {\n type: 'text' as const,\n text: `Error patching ${collection}#${documentId}: ${message}`,\n },\n ],\n }\n }\n },\n }\n}\n"],"names":["z","applyOperation","buildHint","composeSections","sectionSchema","createPatchLayoutTool","catalog","draftCollections","sectionSlugs","sections","map","s","slug","leafSlugs","leaves","l","name","description","join","parameters","collection","string","describe","documentId","layoutField","optional","default","array","operation","enum","insertIndex","number","handler","args","req","_extra","blocks","errors","length","content","type","text","JSON","stringify","success","hint","context","source","existing","payload","findByID","id","draft","overrideAccess","user","error","message","Error","String","currentLayout","Array","isArray","finalLayout","isDraftCollection","has","updated","update","data","displayName","title","draftNote","blockCount"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/tools/patch-layout.ts"],"sourcesContent":["import { z } from 'zod'\nimport type { PayloadRequest } from 'payload'\nimport type { BlockCatalog, BlockNestingMap } from '../types'\n\n/**\n * Creates the patchLayout MCP tool — a surgical wrapper that mutates a single\n * document's blocks-typed field directly without round-tripping the entire\n * array through `updateDocument`.\n *\n * Why this exists: prompting an LLM to \"add a CTA at the bottom of the home\n * page\" via updateDocument forces it to send the whole layout array, which one\n * bad token can wipe out. patchLayout fetches the current array itself,\n * applies a scoped operation, and writes back — the LLM only describes the\n * delta.\n *\n * Validation walks every block recursively against the introspected\n * BlockNestingMap, so arbitrarily-nested layouts work as long as each\n * `blocks`-typed field's content matches that field's allow list.\n */\nexport function createPatchLayoutTool(\n catalog: BlockCatalog,\n nesting: BlockNestingMap,\n draftCollections: Set<string>,\n) {\n const allBlockSlugs = catalog.blocks.map((b) => b.slug)\n\n // Pre-build lookups keyed by `<owner>:<fieldPath>` for O(1) access during\n // recursive validation.\n const nestingByCollectionField = new Map<string, string[]>()\n const nestingByBlockField = new Map<string, string[]>()\n for (const edge of nesting) {\n const key = `${edge.owner}:${edge.fieldPath}`\n if (edge.ownerType === 'collection') {\n nestingByCollectionField.set(key, edge.acceptedBlockSlugs)\n } else {\n nestingByBlockField.set(key, edge.acceptedBlockSlugs)\n }\n }\n\n return {\n name: 'patchLayout',\n description:\n '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.',\n parameters: {\n collection: z.string().describe('The collection slug containing the document'),\n documentId: z.string().describe('The ID of the document to patch'),\n layoutField: z\n .string()\n .optional()\n .default('layout')\n .describe('Name of the blocks-typed field to patch (default \"layout\")'),\n blocks: z\n .array(z.record(z.string(), z.unknown()))\n .describe(\n '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.',\n ),\n operation: z\n .enum(['append', 'prepend', 'insertAt', 'replaceAt', 'full'])\n .describe(\n '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).',\n ),\n insertIndex: z\n .number()\n .optional()\n .describe('Index for insertAt/replaceAt operations'),\n },\n handler: async (\n args: {\n collection: string\n documentId: string\n layoutField?: string\n blocks: Array<Record<string, unknown>>\n operation: 'append' | 'prepend' | 'insertAt' | 'replaceAt' | 'full'\n insertIndex?: number\n },\n req: PayloadRequest,\n _extra: unknown,\n ) => {\n const {\n collection,\n documentId,\n layoutField = 'layout',\n blocks,\n operation,\n insertIndex,\n } = args\n\n // Validate the incoming blocks against the nesting map for the target\n // field before touching the database.\n const rootKey = `${collection}:${layoutField}`\n const rootAllowed = nestingByCollectionField.get(rootKey)\n if (!rootAllowed) {\n return errorResponse(\n `Field \"${layoutField}\" on collection \"${collection}\" is not a blocks-typed field, or no nesting map entry exists for it.`,\n { availableFields: [...nestingByCollectionField.keys()] },\n )\n }\n\n const errors: string[] = []\n validateBlockList(blocks, rootAllowed, layoutField, allBlockSlugs, nestingByBlockField, errors)\n\n if (errors.length > 0) {\n return errorResponse('Block validation failed.', { errors })\n }\n\n req.context = { ...req.context, source: 'mcp' }\n\n let existing: any\n try {\n existing = await req.payload.findByID({\n collection: collection as any,\n id: documentId,\n draft: true,\n req,\n overrideAccess: false,\n user: req.user,\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return errorResponse(`Error fetching ${collection}#${documentId}: ${message}`)\n }\n\n const currentLayout = Array.isArray(existing?.[layoutField]) ? existing[layoutField] : []\n const finalLayout = applyOperation(blocks, operation, insertIndex, currentLayout)\n\n const isDraftCollection = draftCollections.has(collection)\n\n try {\n const updated = await req.payload.update({\n collection: collection as any,\n id: documentId,\n data: { [layoutField]: finalLayout } as any,\n draft: isDraftCollection,\n req,\n overrideAccess: false,\n user: req.user,\n })\n\n const displayName =\n (updated as any).name ||\n (updated as any).title ||\n (updated as any).slug ||\n documentId\n\n const draftNote = isDraftCollection\n ? ' Document is in draft status — use publishDraft to make it live.'\n : ''\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n success: true,\n message:\n `Patched ${layoutField} on \"${displayName}\" (${collection}#${documentId}). ` +\n `Operation: ${operation}. Block count: ${finalLayout.length}.` +\n draftNote,\n blockCount: finalLayout.length,\n operation,\n }),\n },\n ],\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return errorResponse(`Error patching ${collection}#${documentId}: ${message}`)\n }\n },\n }\n}\n\nfunction errorResponse(message: string, extra?: Record<string, unknown>) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({ success: false, error: message, ...(extra ?? {}) }),\n },\n ],\n }\n}\n\n/**\n * Recursively validate a block array against an allow list, descending into\n * each block's own `blocks`-typed fields when present.\n */\nfunction validateBlockList(\n blocks: Array<Record<string, unknown>>,\n allowedSlugs: string[],\n pathLabel: string,\n allBlockSlugs: string[],\n nestingByBlockField: Map<string, string[]>,\n errors: string[],\n) {\n for (let i = 0; i < blocks.length; i++) {\n const block = blocks[i]\n const here = `${pathLabel}[${i}]`\n\n if (!block || typeof block !== 'object') {\n errors.push(`${here}: not an object`)\n continue\n }\n\n const slug = block.blockType\n if (typeof slug !== 'string' || !slug) {\n errors.push(`${here}: missing string \\`blockType\\``)\n continue\n }\n\n if (!allBlockSlugs.includes(slug)) {\n errors.push(`${here}: unknown blockType \"${slug}\". Known: ${allBlockSlugs.join(', ')}`)\n continue\n }\n\n if (!allowedSlugs.includes(slug)) {\n errors.push(\n `${here}: blockType \"${slug}\" not allowed here. Allowed at this position: ${allowedSlugs.join(', ') || '(none)'}`,\n )\n continue\n }\n\n // Recurse: any value on this block that is itself an array of objects with\n // `blockType` is treated as a nested blocks field. Cross-check the field\n // name against the nesting map so we know the allow list for the next level.\n for (const [fieldName, value] of Object.entries(block)) {\n if (!Array.isArray(value)) continue\n if (value.length === 0) continue\n if (!value.every((v) => v && typeof v === 'object' && 'blockType' in v)) continue\n\n const nextKey = `${slug}:${fieldName}`\n const nextAllowed = nestingByBlockField.get(nextKey)\n if (!nextAllowed) {\n errors.push(\n `${here}.${fieldName}: block \"${slug}\" has no blocks field named \"${fieldName}\" in the schema`,\n )\n continue\n }\n\n validateBlockList(\n value as Array<Record<string, unknown>>,\n nextAllowed,\n `${here}.${fieldName}`,\n allBlockSlugs,\n nestingByBlockField,\n errors,\n )\n }\n }\n}\n\n/**\n * Apply a list operation against an existing array of blocks.\n * `full` always replaces; the rest preserve the existing array.\n */\nfunction applyOperation(\n newBlocks: Record<string, unknown>[],\n operation: 'full' | 'append' | 'prepend' | 'insertAt' | 'replaceAt',\n insertIndex: number | undefined,\n existingLayout: Record<string, unknown>[] | undefined,\n): Record<string, unknown>[] {\n if (operation === 'full' || !existingLayout) {\n return newBlocks\n }\n\n const existing = [...existingLayout]\n\n if (operation === 'append') return [...existing, ...newBlocks]\n if (operation === 'prepend') return [...newBlocks, ...existing]\n\n if (operation === 'insertAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex > existing.length) {\n return [...existing, ...newBlocks]\n }\n existing.splice(insertIndex, 0, ...newBlocks)\n return existing\n }\n\n if (operation === 'replaceAt') {\n if (insertIndex === undefined || insertIndex < 0 || insertIndex >= existing.length) {\n return existing\n }\n existing.splice(insertIndex, newBlocks.length, ...newBlocks)\n return existing\n }\n\n return newBlocks\n}\n"],"names":["z","createPatchLayoutTool","catalog","nesting","draftCollections","allBlockSlugs","blocks","map","b","slug","nestingByCollectionField","Map","nestingByBlockField","edge","key","owner","fieldPath","ownerType","set","acceptedBlockSlugs","name","description","parameters","collection","string","describe","documentId","layoutField","optional","default","array","record","unknown","operation","enum","insertIndex","number","handler","args","req","_extra","rootKey","rootAllowed","get","errorResponse","availableFields","keys","errors","validateBlockList","length","context","source","existing","payload","findByID","id","draft","overrideAccess","user","error","message","Error","String","currentLayout","Array","isArray","finalLayout","applyOperation","isDraftCollection","has","updated","update","data","displayName","title","draftNote","content","type","text","JSON","stringify","success","blockCount","extra","allowedSlugs","pathLabel","i","block","here","push","blockType","includes","join","fieldName","value","Object","entries","every","v","nextKey","nextAllowed","newBlocks","existingLayout","undefined","splice"],"mappings":"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"}