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.
- package/README.md +150 -133
- 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 +89 -42
- package/dist/draft-workflow.js.map +1 -1
- package/dist/index.d.ts +10 -15
- package/dist/index.js +36 -76
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +18 -7
- package/dist/introspection.js +113 -84
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +47 -38
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts +9 -4
- package/dist/resources.js +43 -58
- package/dist/resources.js.map +1 -1
- package/dist/tools/_helpers.d.ts +14 -0
- package/dist/tools/_helpers.js +35 -0
- package/dist/tools/_helpers.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +15 -85
- package/dist/tools/patch-layout.js +142 -69
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +1 -11
- package/dist/tools/publish-draft.js +8 -39
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/resolve-reference.d.ts +1 -12
- package/dist/tools/resolve-reference.js +45 -85
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +8 -13
- package/dist/tools/safe-delete.js +68 -100
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +11 -21
- package/dist/tools/schedule-publish.js +18 -61
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +1 -6
- package/dist/tools/search-content.js +52 -64
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +4 -14
- package/dist/tools/update-document.js +23 -72
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/upload-media.d.ts +1 -10
- package/dist/tools/upload-media.js +11 -54
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +7 -20
- package/dist/tools/versions.js +25 -82
- package/dist/tools/versions.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/rate-limiter.d.ts +0 -25
- package/dist/rate-limiter.js +0 -51
- package/dist/rate-limiter.js.map +0 -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,65 +1,50 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate MCP resources that expose the
|
|
3
|
-
*
|
|
4
|
-
|
|
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
|
-
|
|
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: 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
|
-
|
|
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:
|
|
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 {
|
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 {\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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
11
|
-
* via updateDocument forces it to send the
|
|
12
|
-
* can wipe out. patchLayout fetches the current
|
|
13
|
-
* operation, and writes back — the LLM only
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
2
|
+
import { DRAFT_NOTE, errorMessage, getDocDisplayName, jsonResponse, stampMcpContext } from './_helpers';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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 = 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
|
|
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
|
|
24
|
-
|
|
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
|
|
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',
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
if (
|
|
39
|
-
return {
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
94
|
-
const draftNote = isDraftCollection ?
|
|
95
|
-
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|