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/README.md +59 -42
- package/dist/__tests__/introspection.test.js +141 -46
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/draft-workflow.d.ts +24 -19
- package/dist/draft-workflow.js +78 -31
- package/dist/draft-workflow.js.map +1 -1
- package/dist/index.d.ts +10 -15
- package/dist/index.js +43 -70
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +14 -7
- package/dist/introspection.js +92 -68
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +44 -35
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts +9 -4
- package/dist/resources.js +44 -52
- package/dist/resources.js.map +1 -1
- package/dist/tools/patch-layout.d.ts +15 -79
- package/dist/tools/patch-layout.js +140 -48
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/types.d.ts +82 -53
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/compose-helpers.d.ts +0 -117
- package/dist/tools/compose-helpers.js +0 -236
- package/dist/tools/compose-helpers.js.map +0 -1
- package/dist/tools/compose-layout.d.ts +0 -139
- package/dist/tools/compose-layout.js +0 -61
- package/dist/tools/compose-layout.js.map +0 -1
package/dist/resources.js
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generate MCP resources that expose the
|
|
3
|
-
*
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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:
|
|
16
|
-
title:
|
|
17
|
-
description:
|
|
18
|
-
uri:
|
|
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
|
|
61
|
+
function collectionSchemasToObject(schemas) {
|
|
33
62
|
const obj = {};
|
|
34
63
|
for (const [slug, schema] of schemas){
|
|
35
64
|
obj[slug] = schema;
|
|
36
65
|
}
|
|
37
|
-
|
|
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
|
package/dist/resources.js.map
CHANGED
|
@@ -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
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
11
|
-
* via updateDocument forces it to send the
|
|
12
|
-
* can wipe out. patchLayout fetches the current
|
|
13
|
-
* operation, and writes back — the LLM only
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
* via updateDocument forces it to send the
|
|
10
|
-
* can wipe out. patchLayout fetches the current
|
|
11
|
-
* operation, and writes back — the LLM only
|
|
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
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
|
24
|
-
|
|
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
|
|
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',
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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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"}
|