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.
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Determines the draft behavior for a collection based on its config and user overrides.
2
+ * Determines the draft behavior for a collection.
3
3
  *
4
- * - If the collection has no `versions.drafts`: always 'publish' regardless of override
5
- * - If the user specified an override: use that override
6
- * - Default: 'always-draft' for draft-enabled collections, 'publish' for others
4
+ * - No drafts configured 'publish' (raw update allowed; no draft concept)
5
+ * - Drafts configured + override given use override
6
+ * - Drafts configured + no override → 'always-draft' (raw update locked)
7
7
  */ export function getDraftBehavior(collection, options) {
8
8
  const hasDrafts = typeof collection.versions === 'object' && collection.versions !== null && 'drafts' in collection.versions && Boolean(collection.versions.drafts);
9
9
  if (!hasDrafts) return 'publish';
@@ -12,29 +12,70 @@
12
12
  return 'always-draft';
13
13
  }
14
14
  /**
15
- * Builds a preview URL for a draft document.
15
+ * Build a preview URL for a draft document by delegating to the collection's
16
+ * own configured preview URL function. Tries `admin.livePreview.url` first
17
+ * (the modern API), then `admin.preview` (the older `GeneratePreviewURL`).
16
18
  *
17
- * Path is `${siteUrl}/next/preview?...`. Path prefix per collection comes
18
- * from `previewPaths`, defaulting to `/{collectionSlug}` when not configured.
19
- */ function buildPreviewUrl(doc, collectionSlug, siteUrl, previewSecret, previewPaths) {
20
- const slug = doc.slug || '';
21
- const prefix = previewPaths?.[collectionSlug] ?? `/${collectionSlug}`;
22
- const path = `${prefix}/${slug}`;
23
- const params = new URLSearchParams({
24
- slug,
25
- collection: collectionSlug,
26
- path,
27
- previewSecret
28
- });
29
- return `${siteUrl}/next/preview?${params.toString()}`;
19
+ * If neither is configured, or the function returns null/undefined/empty,
20
+ * returns null and the override response will skip preview injection.
21
+ */ async function resolvePreviewUrl(collection, doc, req, siteUrl) {
22
+ const admin = collection.admin ?? {};
23
+ const locale = req.locale ?? 'en';
24
+ let raw;
25
+ const livePreviewUrl = admin.livePreview?.url;
26
+ if (typeof livePreviewUrl === 'function') {
27
+ try {
28
+ raw = await livePreviewUrl({
29
+ data: doc,
30
+ locale: {
31
+ code: locale,
32
+ label: locale
33
+ },
34
+ req,
35
+ payload: req.payload,
36
+ collectionConfig: collection
37
+ });
38
+ } catch {
39
+ raw = null;
40
+ }
41
+ } else if (typeof livePreviewUrl === 'string') {
42
+ raw = livePreviewUrl;
43
+ }
44
+ if (!raw && typeof admin.preview === 'function') {
45
+ try {
46
+ raw = await admin.preview(doc, {
47
+ locale,
48
+ req,
49
+ token: null
50
+ });
51
+ } catch {
52
+ raw = null;
53
+ }
54
+ }
55
+ if (!raw || typeof raw !== 'string') return null;
56
+ if (raw.startsWith('http://') || raw.startsWith('https://')) {
57
+ return raw;
58
+ }
59
+ if (!siteUrl) return null;
60
+ const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
61
+ const path = raw.startsWith('/') ? raw : `/${raw}`;
62
+ return `${base}${path}`;
30
63
  }
31
- /**
32
- * Creates an overrideResponse function for a draft-enabled collection.
33
- * When a document has `_status === 'draft'`, appends a preview URL to the response.
34
- */ function createOverrideResponse(collectionSlug, siteUrl, previewSecret, previewPaths) {
35
- return (response, doc)=>{
64
+ function createOverrideResponse(collection, siteUrl) {
65
+ return async (response, doc, req)=>{
36
66
  if (doc._status !== 'draft') return response;
37
- const previewUrl = buildPreviewUrl(doc, collectionSlug, siteUrl, previewSecret, previewPaths);
67
+ const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl);
68
+ if (!previewUrl) {
69
+ return {
70
+ content: [
71
+ ...response.content,
72
+ {
73
+ type: 'text',
74
+ text: '\n📋 This document is a draft. Use the admin panel to preview it.'
75
+ }
76
+ ]
77
+ };
78
+ }
38
79
  return {
39
80
  content: [
40
81
  ...response.content,
@@ -49,22 +90,28 @@
49
90
  /**
50
91
  * Generates the mcpCollections config object for the official mcpPlugin.
51
92
  *
52
- * For each collection:
93
+ * For each non-excluded collection:
53
94
  * - Determines enabled CRUD operations based on draft behavior
54
- * - For 'always-draft' collections: disables raw `update` to force clients through publishDraft tool
55
- * - Generates `overrideResponse` that appends preview URLs for draft documents
95
+ * - For 'always-draft' collections: disables raw `update` to force clients
96
+ * through publishDraft / patchLayout / updateDocument (which preserve
97
+ * draft semantics)
98
+ * - For draft collections: attaches an `overrideResponse` that appends a
99
+ * preview URL — sourced from the collection's own livePreview/preview
100
+ * function — to draft documents. Falls back to a generic admin-panel
101
+ * message when no preview function is configured.
56
102
  *
57
- * @returns A record of collection slug to MCP collection config, plus the set of draft collection slugs
103
+ * @returns Map of slug MCP collection config, plus the set of draft slugs
58
104
  */ export function generateMcpCollectionConfigs(collections, options) {
59
105
  const mcpCollections = {};
60
106
  const draftCollections = new Set();
61
107
  const excludeSlugs = new Set([
62
- 'users',
63
108
  'payload-mcp-api-keys',
64
109
  ...options.excludeCollections ?? []
65
110
  ]);
66
111
  for (const collection of collections){
67
112
  if (excludeSlugs.has(collection.slug)) continue;
113
+ // Auth-enabled collections are users — never expose them via MCP.
114
+ if (collection.auth) continue;
68
115
  const behavior = getDraftBehavior(collection, options);
69
116
  if (behavior !== 'publish') {
70
117
  draftCollections.add(collection.slug);
@@ -79,8 +126,8 @@
79
126
  description: `Manage ${collection.slug} content`,
80
127
  enabled
81
128
  };
82
- if (draftCollections.has(collection.slug)) {
83
- config.overrideResponse = createOverrideResponse(collection.slug, options.siteUrl, options.previewSecret, options.previewPaths);
129
+ if (draftCollections.has(collection.slug) && !options.previewDisabled) {
130
+ config.overrideResponse = createOverrideResponse(collection, options.siteUrl);
84
131
  }
85
132
  mcpCollections[collection.slug] = config;
86
133
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\nimport type { DraftBehavior } from './types'\n\n/** MCP response shape used by overrideResponse */\ninterface McpResponse {\n content: Array<{ text: string; type: string }>\n}\n\n/** Per-collection MCP config with enabled operations and optional overrideResponse */\ninterface McpCollectionConfig {\n description: string\n enabled: {\n create: boolean\n delete: boolean\n find: boolean\n update: boolean\n }\n overrideResponse?: (\n response: McpResponse,\n doc: Record<string, unknown>,\n req: PayloadRequest,\n ) => McpResponse\n}\n\ninterface GenerateOptions {\n /** Base site URL for preview links */\n siteUrl: string\n /** Preview authentication secret */\n previewSecret: string\n /**\n * Per-collection URL path prefix used when constructing preview URLs.\n * Defaults to `/{slug}` for collections not in the map.\n */\n previewPaths?: Record<string, string>\n /** Per-collection draft behavior overrides */\n draftBehavior?: Record<string, DraftBehavior>\n /** Collection slugs to exclude from MCP */\n excludeCollections?: string[]\n}\n\n/**\n * Determines the draft behavior for a collection based on its config and user overrides.\n *\n * - If the collection has no `versions.drafts`: always 'publish' regardless of override\n * - If the user specified an override: use that override\n * - Default: 'always-draft' for draft-enabled collections, 'publish' for others\n */\nexport function getDraftBehavior(\n collection: CollectionConfig,\n options?: { draftBehavior?: Record<string, DraftBehavior> },\n): 'always-draft' | 'always-publish' | 'publish' {\n const hasDrafts =\n typeof collection.versions === 'object' &&\n collection.versions !== null &&\n 'drafts' in collection.versions &&\n Boolean(collection.versions.drafts)\n\n if (!hasDrafts) return 'publish'\n\n const override = options?.draftBehavior?.[collection.slug]\n if (override) return override\n\n return 'always-draft'\n}\n\n/**\n * Builds a preview URL for a draft document.\n *\n * Path is `${siteUrl}/next/preview?...`. Path prefix per collection comes\n * from `previewPaths`, defaulting to `/{collectionSlug}` when not configured.\n */\nfunction buildPreviewUrl(\n doc: Record<string, unknown>,\n collectionSlug: string,\n siteUrl: string,\n previewSecret: string,\n previewPaths?: Record<string, string>,\n): string {\n const slug = (doc.slug as string) || ''\n const prefix = previewPaths?.[collectionSlug] ?? `/${collectionSlug}`\n const path = `${prefix}/${slug}`\n\n const params = new URLSearchParams({\n slug,\n collection: collectionSlug,\n path,\n previewSecret,\n })\n\n return `${siteUrl}/next/preview?${params.toString()}`\n}\n\n/**\n * Creates an overrideResponse function for a draft-enabled collection.\n * When a document has `_status === 'draft'`, appends a preview URL to the response.\n */\nfunction createOverrideResponse(\n collectionSlug: string,\n siteUrl: string,\n previewSecret: string,\n previewPaths?: Record<string, string>,\n): McpCollectionConfig['overrideResponse'] {\n return (response: McpResponse, doc: Record<string, unknown>): McpResponse => {\n if (doc._status !== 'draft') return response\n\n const previewUrl = buildPreviewUrl(doc, collectionSlug, siteUrl, previewSecret, previewPaths)\n\n return {\n content: [\n ...response.content,\n {\n type: 'text',\n text: `\\n📋 This document is a draft. Preview it here: ${previewUrl}`,\n },\n ],\n }\n }\n}\n\n/**\n * Generates the mcpCollections config object for the official mcpPlugin.\n *\n * For each collection:\n * - Determines enabled CRUD operations based on draft behavior\n * - For 'always-draft' collections: disables raw `update` to force clients through publishDraft tool\n * - Generates `overrideResponse` that appends preview URLs for draft documents\n *\n * @returns A record of collection slug to MCP collection config, plus the set of draft collection slugs\n */\nexport function generateMcpCollectionConfigs(\n collections: CollectionConfig[],\n options: GenerateOptions,\n): {\n mcpCollections: Record<string, McpCollectionConfig>\n draftCollections: Set<string>\n} {\n const mcpCollections: Record<string, McpCollectionConfig> = {}\n const draftCollections = new Set<string>()\n\n const excludeSlugs = new Set([\n 'users',\n 'payload-mcp-api-keys',\n ...(options.excludeCollections ?? []),\n ])\n\n for (const collection of collections) {\n if (excludeSlugs.has(collection.slug)) continue\n\n const behavior = getDraftBehavior(collection, options)\n\n if (behavior !== 'publish') {\n draftCollections.add(collection.slug)\n }\n\n const enabled = {\n find: true,\n create: true,\n update: behavior !== 'always-draft',\n delete: true,\n }\n\n const config: McpCollectionConfig = {\n description: `Manage ${collection.slug} content`,\n enabled,\n }\n\n if (draftCollections.has(collection.slug)) {\n config.overrideResponse = createOverrideResponse(\n collection.slug,\n options.siteUrl,\n options.previewSecret,\n options.previewPaths,\n )\n }\n\n mcpCollections[collection.slug] = config\n }\n\n return { mcpCollections, draftCollections }\n}\n"],"names":["getDraftBehavior","collection","options","hasDrafts","versions","Boolean","drafts","override","draftBehavior","slug","buildPreviewUrl","doc","collectionSlug","siteUrl","previewSecret","previewPaths","prefix","path","params","URLSearchParams","toString","createOverrideResponse","response","_status","previewUrl","content","type","text","generateMcpCollectionConfigs","collections","mcpCollections","draftCollections","Set","excludeSlugs","excludeCollections","has","behavior","add","enabled","find","create","update","delete","config","description","overrideResponse"],"mappings":"AAwCA;;;;;;CAMC,GACD,OAAO,SAASA,iBACdC,UAA4B,EAC5BC,OAA2D;IAE3D,MAAMC,YACJ,OAAOF,WAAWG,QAAQ,KAAK,YAC/BH,WAAWG,QAAQ,KAAK,QACxB,YAAYH,WAAWG,QAAQ,IAC/BC,QAAQJ,WAAWG,QAAQ,CAACE,MAAM;IAEpC,IAAI,CAACH,WAAW,OAAO;IAEvB,MAAMI,WAAWL,SAASM,eAAe,CAACP,WAAWQ,IAAI,CAAC;IAC1D,IAAIF,UAAU,OAAOA;IAErB,OAAO;AACT;AAEA;;;;;CAKC,GACD,SAASG,gBACPC,GAA4B,EAC5BC,cAAsB,EACtBC,OAAe,EACfC,aAAqB,EACrBC,YAAqC;IAErC,MAAMN,OAAO,AAACE,IAAIF,IAAI,IAAe;IACrC,MAAMO,SAASD,cAAc,CAACH,eAAe,IAAI,CAAC,CAAC,EAAEA,gBAAgB;IACrE,MAAMK,OAAO,GAAGD,OAAO,CAAC,EAAEP,MAAM;IAEhC,MAAMS,SAAS,IAAIC,gBAAgB;QACjCV;QACAR,YAAYW;QACZK;QACAH;IACF;IAEA,OAAO,GAAGD,QAAQ,cAAc,EAAEK,OAAOE,QAAQ,IAAI;AACvD;AAEA;;;CAGC,GACD,SAASC,uBACPT,cAAsB,EACtBC,OAAe,EACfC,aAAqB,EACrBC,YAAqC;IAErC,OAAO,CAACO,UAAuBX;QAC7B,IAAIA,IAAIY,OAAO,KAAK,SAAS,OAAOD;QAEpC,MAAME,aAAad,gBAAgBC,KAAKC,gBAAgBC,SAASC,eAAeC;QAEhF,OAAO;YACLU,SAAS;mBACJH,SAASG,OAAO;gBACnB;oBACEC,MAAM;oBACNC,MAAM,CAAC,gDAAgD,EAAEH,YAAY;gBACvE;aACD;QACH;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASI,6BACdC,WAA+B,EAC/B3B,OAAwB;IAKxB,MAAM4B,iBAAsD,CAAC;IAC7D,MAAMC,mBAAmB,IAAIC;IAE7B,MAAMC,eAAe,IAAID,IAAI;QAC3B;QACA;WACI9B,QAAQgC,kBAAkB,IAAI,EAAE;KACrC;IAED,KAAK,MAAMjC,cAAc4B,YAAa;QACpC,IAAII,aAAaE,GAAG,CAAClC,WAAWQ,IAAI,GAAG;QAEvC,MAAM2B,WAAWpC,iBAAiBC,YAAYC;QAE9C,IAAIkC,aAAa,WAAW;YAC1BL,iBAAiBM,GAAG,CAACpC,WAAWQ,IAAI;QACtC;QAEA,MAAM6B,UAAU;YACdC,MAAM;YACNC,QAAQ;YACRC,QAAQL,aAAa;YACrBM,QAAQ;QACV;QAEA,MAAMC,SAA8B;YAClCC,aAAa,CAAC,OAAO,EAAE3C,WAAWQ,IAAI,CAAC,QAAQ,CAAC;YAChD6B;QACF;QAEA,IAAIP,iBAAiBI,GAAG,CAAClC,WAAWQ,IAAI,GAAG;YACzCkC,OAAOE,gBAAgB,GAAGxB,uBACxBpB,WAAWQ,IAAI,EACfP,QAAQW,OAAO,EACfX,QAAQY,aAAa,EACrBZ,QAAQa,YAAY;QAExB;QAEAe,cAAc,CAAC7B,WAAWQ,IAAI,CAAC,GAAGkC;IACpC;IAEA,OAAO;QAAEb;QAAgBC;IAAiB;AAC5C"}
1
+ {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\n\n/** MCP response shape used by overrideResponse */\ninterface McpResponse {\n content: Array<{ text: string; type: string }>\n}\n\n/** Per-collection MCP config with enabled operations and optional overrideResponse */\ninterface McpCollectionConfig {\n description: string\n enabled: {\n create: boolean\n delete: boolean\n find: boolean\n update: boolean\n }\n overrideResponse?: (\n response: McpResponse,\n doc: Record<string, unknown>,\n req: PayloadRequest,\n ) => McpResponse | Promise<McpResponse>\n}\n\ninterface GenerateOptions {\n /**\n * Optional absolute base URL prepended to relative preview paths returned\n * by the collection's own preview URL function. Resolved upstream from\n * (in order): `options.preview.siteUrl`, `incomingConfig.serverURL`,\n * `process.env.NEXT_PUBLIC_SERVER_URL`, `process.env.SITE_URL`. May be\n * undefined relative-path returns will then be skipped.\n */\n siteUrl?: string\n /** Per-collection draft behavior overrides */\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\n /** Collection slugs to exclude from MCP */\n excludeCollections?: string[]\n /** Disable preview URL injection entirely */\n previewDisabled?: boolean\n}\n\n/**\n * Determines the draft behavior for a collection.\n *\n * - No drafts configured 'publish' (raw update allowed; no draft concept)\n * - Drafts configured + override given use override\n * - Drafts configured + no override → 'always-draft' (raw update locked)\n */\nexport function getDraftBehavior(\n collection: CollectionConfig,\n options?: { draftBehavior?: Record<string, 'always-draft' | 'always-publish'> },\n): 'always-draft' | 'always-publish' | 'publish' {\n const hasDrafts =\n typeof collection.versions === 'object' &&\n collection.versions !== null &&\n 'drafts' in collection.versions &&\n Boolean(collection.versions.drafts)\n\n if (!hasDrafts) return 'publish'\n\n const override = options?.draftBehavior?.[collection.slug]\n if (override) return override\n\n return 'always-draft'\n}\n\n/**\n * Build a preview URL for a draft document by delegating to the collection's\n * own configured preview URL function. Tries `admin.livePreview.url` first\n * (the modern API), then `admin.preview` (the older `GeneratePreviewURL`).\n *\n * If neither is configured, or the function returns null/undefined/empty,\n * returns null and the override response will skip preview injection.\n */\nasync function resolvePreviewUrl(\n collection: CollectionConfig,\n doc: Record<string, unknown>,\n req: PayloadRequest,\n siteUrl: string | undefined,\n): Promise<string | null> {\n const admin = (collection.admin ?? {}) as Record<string, any>\n const locale = (req as any).locale ?? 'en'\n\n let raw: string | null | undefined\n\n const livePreviewUrl = admin.livePreview?.url\n if (typeof livePreviewUrl === 'function') {\n try {\n raw = await livePreviewUrl({\n data: doc,\n locale: { code: locale, label: locale },\n req,\n payload: req.payload,\n collectionConfig: collection as any,\n })\n } catch {\n raw = null\n }\n } else if (typeof livePreviewUrl === 'string') {\n raw = livePreviewUrl\n }\n\n if (!raw && typeof admin.preview === 'function') {\n try {\n raw = await admin.preview(doc, {\n locale,\n req,\n token: null,\n })\n } catch {\n raw = null\n }\n }\n\n if (!raw || typeof raw !== 'string') return null\n\n if (raw.startsWith('http://') || raw.startsWith('https://')) {\n return raw\n }\n\n if (!siteUrl) return null\n\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\n const path = raw.startsWith('/') ? raw : `/${raw}`\n return `${base}${path}`\n}\n\nfunction createOverrideResponse(\n collection: CollectionConfig,\n siteUrl: string | undefined,\n): McpCollectionConfig['overrideResponse'] {\n return async (response, doc, req): Promise<McpResponse> => {\n if (doc._status !== 'draft') return response\n\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\n if (!previewUrl) {\n return {\n content: [\n ...response.content,\n {\n type: 'text',\n text: '\\n📋 This document is a draft. Use the admin panel to preview it.',\n },\n ],\n }\n }\n\n return {\n content: [\n ...response.content,\n {\n type: 'text',\n text: `\\n📋 This document is a draft. Preview it here: ${previewUrl}`,\n },\n ],\n }\n }\n}\n\n/**\n * Generates the mcpCollections config object for the official mcpPlugin.\n *\n * For each non-excluded collection:\n * - Determines enabled CRUD operations based on draft behavior\n * - For 'always-draft' collections: disables raw `update` to force clients\n * through publishDraft / patchLayout / updateDocument (which preserve\n * draft semantics)\n * - For draft collections: attaches an `overrideResponse` that appends a\n * preview URL sourced from the collection's own livePreview/preview\n * function — to draft documents. Falls back to a generic admin-panel\n * message when no preview function is configured.\n *\n * @returns Map of slug MCP collection config, plus the set of draft slugs\n */\nexport function generateMcpCollectionConfigs(\n collections: CollectionConfig[],\n options: GenerateOptions,\n): {\n mcpCollections: Record<string, McpCollectionConfig>\n draftCollections: Set<string>\n} {\n const mcpCollections: Record<string, McpCollectionConfig> = {}\n const draftCollections = new Set<string>()\n\n const excludeSlugs = new Set([\n 'payload-mcp-api-keys',\n ...(options.excludeCollections ?? []),\n ])\n\n for (const collection of collections) {\n if (excludeSlugs.has(collection.slug)) continue\n\n // Auth-enabled collections are users — never expose them via MCP.\n if ((collection as any).auth) continue\n\n const behavior = getDraftBehavior(collection, options)\n\n if (behavior !== 'publish') {\n draftCollections.add(collection.slug)\n }\n\n const enabled = {\n find: true,\n create: true,\n update: behavior !== 'always-draft',\n delete: true,\n }\n\n const config: McpCollectionConfig = {\n description: `Manage ${collection.slug} content`,\n enabled,\n }\n\n if (draftCollections.has(collection.slug) && !options.previewDisabled) {\n config.overrideResponse = createOverrideResponse(collection, options.siteUrl)\n }\n\n mcpCollections[collection.slug] = config\n }\n\n return { mcpCollections, draftCollections }\n}\n"],"names":["getDraftBehavior","collection","options","hasDrafts","versions","Boolean","drafts","override","draftBehavior","slug","resolvePreviewUrl","doc","req","siteUrl","admin","locale","raw","livePreviewUrl","livePreview","url","data","code","label","payload","collectionConfig","preview","token","startsWith","base","endsWith","slice","path","createOverrideResponse","response","_status","previewUrl","content","type","text","generateMcpCollectionConfigs","collections","mcpCollections","draftCollections","Set","excludeSlugs","excludeCollections","has","auth","behavior","add","enabled","find","create","update","delete","config","description","previewDisabled","overrideResponse"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -3,22 +3,17 @@ import type { ContentToolkitOptions } from './types';
3
3
  /**
4
4
  * Payload MCP Toolkit
5
5
  *
6
- * A wrapper plugin that introspects the Payload config and generates
7
- * domain-aware MCP tools, prompts, and resources for AI content management.
6
+ * Layered on top of the official @payloadcms/plugin-mcp. The toolkit
7
+ * introspects your Payload config and registers schema-aware prompts,
8
+ * resources, and tools so AI clients can drive the CMS without
9
+ * hand-built plumbing.
8
10
  *
9
- * Usage in payload.config.ts:
11
+ * Zero-config usage:
10
12
  * ```ts
11
- * import { contentToolkitPlugin } from 'payload-mcp-toolkit'
12
- *
13
- * plugins: [
14
- * contentToolkitPlugin({
15
- * siteUrl: process.env.SITE_URL!,
16
- * previewSecret: process.env.PREVIEW_SECRET!,
17
- * previewPaths: { posts: '/blog', pages: '' },
18
- * draftBehavior: { pages: 'always-draft' },
19
- * }),
20
- * ]
13
+ * plugins: [contentToolkitPlugin()]
21
14
  * ```
15
+ *
16
+ * Every option below is an optional escape hatch — see ContentToolkitOptions.
22
17
  */
23
- export declare function contentToolkitPlugin(options: ContentToolkitOptions): Plugin;
24
- export type { ContentToolkitOptions, DomainPrompt, DraftBehavior, CollectionSchema, BlockCatalog, SectionBlockSchema, LeafBlockSchema, RelationshipEdge, FieldSchema, } from './types';
18
+ export declare function contentToolkitPlugin(options?: ContentToolkitOptions): Plugin;
19
+ export type { ContentToolkitOptions, DomainPrompt, CollectionSchema, BlockCatalog, BlockSchema, BlockNestingMap, BlockNestingEdge, RelationshipEdge, FieldSchema, } from './types';
package/dist/index.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { mcpPlugin } from '@payloadcms/plugin-mcp';
2
- import { introspectCollections, introspectBlocks, buildRelationshipGraph } from './introspection';
2
+ import { introspectCollections, introspectBlocks, buildBlockNestingMap, buildRelationshipGraph } from './introspection';
3
3
  import { generatePrompts } from './prompts';
4
4
  import { generateResources } from './resources';
5
5
  import { generateMcpCollectionConfigs } from './draft-workflow';
6
- import { createComposeLayoutTool } from './tools/compose-layout';
7
6
  import { createPatchLayoutTool } from './tools/patch-layout';
8
7
  import { createPublishDraftTool } from './tools/publish-draft';
9
8
  import { createResolveReferenceTool } from './tools/resolve-reference';
@@ -16,80 +15,50 @@ import { createListVersionsTool, createRestoreVersionTool } from './tools/versio
16
15
  /**
17
16
  * Payload MCP Toolkit
18
17
  *
19
- * A wrapper plugin that introspects the Payload config and generates
20
- * domain-aware MCP tools, prompts, and resources for AI content management.
18
+ * Layered on top of the official @payloadcms/plugin-mcp. The toolkit
19
+ * introspects your Payload config and registers schema-aware prompts,
20
+ * resources, and tools so AI clients can drive the CMS without
21
+ * hand-built plumbing.
21
22
  *
22
- * Usage in payload.config.ts:
23
+ * Zero-config usage:
23
24
  * ```ts
24
- * import { contentToolkitPlugin } from 'payload-mcp-toolkit'
25
- *
26
- * plugins: [
27
- * contentToolkitPlugin({
28
- * siteUrl: process.env.SITE_URL!,
29
- * previewSecret: process.env.PREVIEW_SECRET!,
30
- * previewPaths: { posts: '/blog', pages: '' },
31
- * draftBehavior: { pages: 'always-draft' },
32
- * }),
33
- * ]
25
+ * plugins: [contentToolkitPlugin()]
34
26
  * ```
35
- */ export function contentToolkitPlugin(options) {
27
+ *
28
+ * Every option below is an optional escape hatch — see ContentToolkitOptions.
29
+ */ export function contentToolkitPlugin(options = {}) {
36
30
  return (incomingConfig)=>{
37
31
  const collections = incomingConfig.collections ?? [];
38
32
  const allBlocks = incomingConfig.blocks ?? [];
39
- // Separate section blocks from leaf blocks.
40
- //
41
- // Preferred: `options.sectionBlockSlugs` — explicit, unambiguous.
42
- // Fallback heuristic: blocks containing a nested `blocks`-type field are
43
- // sections, all others are leaves. The heuristic mis-classifies "fixed"
44
- // sections (no nested blocks but their own standalone fields), so prefer
45
- // the explicit option whenever the schema has any fixed sections.
46
- const sectionBlocks = [];
47
- const leafBlocks = [];
48
- if (options.sectionBlockSlugs && options.sectionBlockSlugs.length > 0) {
49
- const sectionSlugs = new Set(options.sectionBlockSlugs);
50
- for (const block of allBlocks){
51
- if (sectionSlugs.has(block.slug)) {
52
- sectionBlocks.push(block);
53
- } else {
54
- leafBlocks.push(block);
55
- }
56
- }
57
- } else {
58
- for (const block of allBlocks){
59
- const hasBlocksField = block.fields.some((f)=>f.type === 'blocks' || f.type === 'tabs' && 'tabs' in f && f.tabs.some((tab)=>'fields' in tab && tab.fields.some((tf)=>tf.type === 'blocks')));
60
- if (hasBlocksField) {
61
- sectionBlocks.push(block);
62
- } else {
63
- leafBlocks.push(block);
64
- }
65
- }
66
- }
67
- // 1. Schema Introspection
33
+ // 1. Schema introspection
68
34
  const collectionSchemas = introspectCollections(collections);
69
- const blockCatalog = introspectBlocks(sectionBlocks, leafBlocks);
35
+ const blockCatalog = introspectBlocks(allBlocks);
36
+ const blockNesting = buildBlockNestingMap(collections, allBlocks);
70
37
  const relationships = buildRelationshipGraph(collectionSchemas);
71
- // 2. Generate Prompts
72
- const prompts = generatePrompts(collectionSchemas, blockCatalog, relationships, options.domainPrompts);
73
- // 3. Generate Resources
74
- const resources = generateResources(collectionSchemas, blockCatalog, relationships);
75
- // 4. Generate Tools
38
+ // 2. Resolve preview siteUrl from explicit option, then Payload's own
39
+ // serverURL, then conventional env vars. May still be undefined —
40
+ // that's fine; relative-path preview URLs are skipped in that case.
41
+ const previewSiteUrl = options.preview?.siteUrl ?? incomingConfig.serverURL ?? process.env.NEXT_PUBLIC_SERVER_URL ?? process.env.SITE_URL;
42
+ // 3. Generate prompts and resources
43
+ const prompts = generatePrompts(collectionSchemas, blockCatalog, blockNesting, relationships, options.domainPrompts);
44
+ const resources = generateResources(collectionSchemas, blockCatalog, blockNesting, relationships);
45
+ // 4. Build MCP collection configs (preview URL + draft behavior)
46
+ const { mcpCollections, draftCollections } = generateMcpCollectionConfigs(collections, {
47
+ siteUrl: previewSiteUrl,
48
+ draftBehavior: options.draftBehavior,
49
+ excludeCollections: options.exclude?.collections,
50
+ previewDisabled: options.preview?.disabled
51
+ });
52
+ // 5. Build the searchable-fields map for resolveReference
76
53
  const searchableCollections = new Map();
77
54
  for (const [slug, schema] of collectionSchemas){
78
55
  if (schema.searchableFields.length > 0) {
79
56
  searchableCollections.set(slug, schema.searchableFields);
80
57
  }
81
58
  }
82
- // 5. Generate MCP Collection Configs with draft workflow
83
- const { mcpCollections, draftCollections } = generateMcpCollectionConfigs(collections, {
84
- siteUrl: options.siteUrl,
85
- previewSecret: options.previewSecret,
86
- previewPaths: options.previewPaths,
87
- draftBehavior: options.draftBehavior,
88
- excludeCollections: options.excludeCollections
89
- });
59
+ // 6. Tools
90
60
  const tools = [
91
- createComposeLayoutTool(blockCatalog),
92
- createPatchLayoutTool(blockCatalog, draftCollections),
61
+ createPatchLayoutTool(blockCatalog, blockNesting, draftCollections),
93
62
  createPublishDraftTool(draftCollections),
94
63
  createResolveReferenceTool(searchableCollections),
95
64
  createSafeDeleteTool(relationships),
@@ -102,12 +71,11 @@ import { createListVersionsTool, createRestoreVersionTool } from './tools/versio
102
71
  createListVersionsTool(draftCollections),
103
72
  createRestoreVersionTool(draftCollections)
104
73
  ];
105
- // schedulePublish only registers when at least one draft collection has a `publishedAt` date field
106
74
  const schedulePublish = createSchedulePublishTool(collectionSchemas, draftCollections);
107
75
  if (schedulePublish) tools.push(schedulePublish);
108
- // Build MCP global configs
76
+ // 7. Globals — expose every non-excluded global with default capabilities
109
77
  const mcpGlobals = {};
110
- const excludeGlobalSlugs = new Set(options.excludeGlobals ?? []);
78
+ const excludeGlobalSlugs = new Set(options.exclude?.globals ?? []);
111
79
  for (const global of incomingConfig.globals ?? []){
112
80
  if (excludeGlobalSlugs.has(global.slug)) continue;
113
81
  mcpGlobals[global.slug] = {
@@ -115,20 +83,25 @@ import { createListVersionsTool, createRestoreVersionTool } from './tools/versio
115
83
  description: `Manage ${global.slug} global settings`
116
84
  };
117
85
  }
118
- // Apply the official MCP plugin with our generated config
86
+ // 8. Apply the official MCP plugin with our generated config.
87
+ //
88
+ // userCollection: passthrough — when omitted, the official plugin falls
89
+ // back to `incomingConfig.admin.user`, which is already the canonical
90
+ // Payload way to declare your auth collection.
91
+ //
92
+ // overrideAuth: rebinds req.user from the API key's linked user so our
93
+ // custom tools' overrideAccess: false runs against the right identity.
94
+ // Safe — getDefault() throws inside the official plugin if the API key
95
+ // has no linked user, so settings.user is guaranteed here.
119
96
  const withMcp = mcpPlugin({
120
97
  collections: mcpCollections,
121
98
  globals: mcpGlobals,
99
+ userCollection: options.userCollection,
122
100
  mcp: {
123
101
  tools: tools,
124
102
  prompts: prompts,
125
103
  resources: resources
126
104
  },
127
- // Set req.user from the API key's linked user so custom tools
128
- // can use overrideAccess: false and relationship field validation
129
- // has a valid user context for access control checks.
130
- // Safe: getDefault() throws inside the official plugin if the API key
131
- // has no linked user, so settings.user is guaranteed to exist here.
132
105
  overrideAuth: async (req, getDefault)=>{
133
106
  const settings = await getDefault();
134
107
  req.user = settings.user;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Block, CollectionConfig, Config, Plugin } from 'payload'\nimport { mcpPlugin } from '@payloadcms/plugin-mcp'\nimport type { ContentToolkitOptions } from './types'\nimport { introspectCollections, introspectBlocks, buildRelationshipGraph } from './introspection'\nimport { generatePrompts } from './prompts'\nimport { generateResources } from './resources'\nimport { generateMcpCollectionConfigs } from './draft-workflow'\nimport { createComposeLayoutTool } from './tools/compose-layout'\nimport { createPatchLayoutTool } from './tools/patch-layout'\nimport { createPublishDraftTool } from './tools/publish-draft'\nimport { createResolveReferenceTool } from './tools/resolve-reference'\nimport { createSafeDeleteTool } from './tools/safe-delete'\nimport { createSchedulePublishTool } from './tools/schedule-publish'\nimport { createSearchContentTool } from './tools/search-content'\nimport { createUpdateDocumentTool } from './tools/update-document'\nimport { createUploadMediaTool } from './tools/upload-media'\nimport { createListVersionsTool, createRestoreVersionTool } from './tools/versions'\n\n/**\n * Payload MCP Toolkit\n *\n * A wrapper plugin that introspects the Payload config and generates\n * domain-aware MCP tools, prompts, and resources for AI content management.\n *\n * Usage in payload.config.ts:\n * ```ts\n * import { contentToolkitPlugin } from 'payload-mcp-toolkit'\n *\n * plugins: [\n * contentToolkitPlugin({\n * siteUrl: process.env.SITE_URL!,\n * previewSecret: process.env.PREVIEW_SECRET!,\n * previewPaths: { posts: '/blog', pages: '' },\n * draftBehavior: { pages: 'always-draft' },\n * }),\n * ]\n * ```\n */\nexport function contentToolkitPlugin(options: ContentToolkitOptions): Plugin {\n return (incomingConfig: Config): Config => {\n const collections = (incomingConfig.collections ?? []) as CollectionConfig[]\n const allBlocks = (incomingConfig.blocks ?? []) as Block[]\n\n // Separate section blocks from leaf blocks.\n //\n // Preferred: `options.sectionBlockSlugs` — explicit, unambiguous.\n // Fallback heuristic: blocks containing a nested `blocks`-type field are\n // sections, all others are leaves. The heuristic mis-classifies \"fixed\"\n // sections (no nested blocks but their own standalone fields), so prefer\n // the explicit option whenever the schema has any fixed sections.\n const sectionBlocks: Block[] = []\n const leafBlocks: Block[] = []\n\n if (options.sectionBlockSlugs && options.sectionBlockSlugs.length > 0) {\n const sectionSlugs = new Set(options.sectionBlockSlugs)\n for (const block of allBlocks) {\n if (sectionSlugs.has(block.slug)) {\n sectionBlocks.push(block)\n } else {\n leafBlocks.push(block)\n }\n }\n } else {\n for (const block of allBlocks) {\n const hasBlocksField = block.fields.some(\n (f) => f.type === 'blocks' ||\n (f.type === 'tabs' && 'tabs' in f && f.tabs.some(\n (tab: any) => 'fields' in tab && tab.fields.some((tf: any) => tf.type === 'blocks')\n ))\n )\n if (hasBlocksField) {\n sectionBlocks.push(block)\n } else {\n leafBlocks.push(block)\n }\n }\n }\n\n // 1. Schema Introspection\n const collectionSchemas = introspectCollections(collections)\n const blockCatalog = introspectBlocks(sectionBlocks, leafBlocks)\n const relationships = buildRelationshipGraph(collectionSchemas)\n\n // 2. Generate Prompts\n const prompts = generatePrompts(\n collectionSchemas,\n blockCatalog,\n relationships,\n options.domainPrompts,\n )\n\n // 3. Generate Resources\n const resources = generateResources(collectionSchemas, blockCatalog, relationships)\n\n // 4. Generate Tools\n const searchableCollections = new Map<string, string[]>()\n for (const [slug, schema] of collectionSchemas) {\n if (schema.searchableFields.length > 0) {\n searchableCollections.set(slug, schema.searchableFields)\n }\n }\n\n // 5. Generate MCP Collection Configs with draft workflow\n const { mcpCollections, draftCollections } = generateMcpCollectionConfigs(collections, {\n siteUrl: options.siteUrl,\n previewSecret: options.previewSecret,\n previewPaths: options.previewPaths,\n draftBehavior: options.draftBehavior,\n excludeCollections: options.excludeCollections,\n })\n\n const tools: any[] = [\n createComposeLayoutTool(blockCatalog),\n createPatchLayoutTool(blockCatalog, draftCollections),\n createPublishDraftTool(draftCollections),\n createResolveReferenceTool(searchableCollections),\n createSafeDeleteTool(relationships),\n createSearchContentTool(collectionSchemas),\n createUpdateDocumentTool(collectionSchemas, draftCollections),\n createUploadMediaTool({\n maxFileSize: options.mediaUpload?.maxFileSize,\n collectionSlug: options.mediaUpload?.collectionSlug,\n }),\n createListVersionsTool(draftCollections),\n createRestoreVersionTool(draftCollections),\n ]\n\n // schedulePublish only registers when at least one draft collection has a `publishedAt` date field\n const schedulePublish = createSchedulePublishTool(collectionSchemas, draftCollections)\n if (schedulePublish) tools.push(schedulePublish)\n\n // Build MCP global configs\n const mcpGlobals: Record<string, { enabled: boolean; description?: string }> = {}\n const excludeGlobalSlugs = new Set(options.excludeGlobals ?? [])\n\n for (const global of (incomingConfig.globals ?? []) as Array<{ slug: string }>) {\n if (excludeGlobalSlugs.has(global.slug)) continue\n mcpGlobals[global.slug] = {\n enabled: true,\n description: `Manage ${global.slug} global settings`,\n }\n }\n\n // Apply the official MCP plugin with our generated config\n const withMcp = mcpPlugin({\n collections: mcpCollections as any,\n globals: mcpGlobals as any,\n mcp: {\n tools: tools as any[],\n prompts: prompts as any[],\n resources: resources as any[],\n },\n // Set req.user from the API key's linked user so custom tools\n // can use overrideAccess: false and relationship field validation\n // has a valid user context for access control checks.\n // Safe: getDefault() throws inside the official plugin if the API key\n // has no linked user, so settings.user is guaranteed to exist here.\n overrideAuth: async (req, getDefault) => {\n const settings = await getDefault()\n req.user = (settings as any).user\n return settings\n },\n })\n\n return withMcp(incomingConfig)\n }\n}\n\nexport type {\n ContentToolkitOptions,\n DomainPrompt,\n DraftBehavior,\n CollectionSchema,\n BlockCatalog,\n SectionBlockSchema,\n LeafBlockSchema,\n RelationshipEdge,\n FieldSchema,\n} from './types'\n"],"names":["mcpPlugin","introspectCollections","introspectBlocks","buildRelationshipGraph","generatePrompts","generateResources","generateMcpCollectionConfigs","createComposeLayoutTool","createPatchLayoutTool","createPublishDraftTool","createResolveReferenceTool","createSafeDeleteTool","createSchedulePublishTool","createSearchContentTool","createUpdateDocumentTool","createUploadMediaTool","createListVersionsTool","createRestoreVersionTool","contentToolkitPlugin","options","incomingConfig","collections","allBlocks","blocks","sectionBlocks","leafBlocks","sectionBlockSlugs","length","sectionSlugs","Set","block","has","slug","push","hasBlocksField","fields","some","f","type","tabs","tab","tf","collectionSchemas","blockCatalog","relationships","prompts","domainPrompts","resources","searchableCollections","Map","schema","searchableFields","set","mcpCollections","draftCollections","siteUrl","previewSecret","previewPaths","draftBehavior","excludeCollections","tools","maxFileSize","mediaUpload","collectionSlug","schedulePublish","mcpGlobals","excludeGlobalSlugs","excludeGlobals","global","globals","enabled","description","withMcp","mcp","overrideAuth","req","getDefault","settings","user"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Block, CollectionConfig, Config, Plugin } from 'payload'\nimport { mcpPlugin } from '@payloadcms/plugin-mcp'\nimport type { ContentToolkitOptions } from './types'\nimport {\n introspectCollections,\n introspectBlocks,\n buildBlockNestingMap,\n buildRelationshipGraph,\n} from './introspection'\nimport { generatePrompts } from './prompts'\nimport { generateResources } from './resources'\nimport { generateMcpCollectionConfigs } from './draft-workflow'\nimport { createPatchLayoutTool } from './tools/patch-layout'\nimport { createPublishDraftTool } from './tools/publish-draft'\nimport { createResolveReferenceTool } from './tools/resolve-reference'\nimport { createSafeDeleteTool } from './tools/safe-delete'\nimport { createSchedulePublishTool } from './tools/schedule-publish'\nimport { createSearchContentTool } from './tools/search-content'\nimport { createUpdateDocumentTool } from './tools/update-document'\nimport { createUploadMediaTool } from './tools/upload-media'\nimport { createListVersionsTool, createRestoreVersionTool } from './tools/versions'\n\n/**\n * Payload MCP Toolkit\n *\n * Layered on top of the official @payloadcms/plugin-mcp. The toolkit\n * introspects your Payload config and registers schema-aware prompts,\n * resources, and tools so AI clients can drive the CMS without\n * hand-built plumbing.\n *\n * Zero-config usage:\n * ```ts\n * plugins: [contentToolkitPlugin()]\n * ```\n *\n * Every option below is an optional escape hatch — see ContentToolkitOptions.\n */\nexport function contentToolkitPlugin(options: ContentToolkitOptions = {}): Plugin {\n return (incomingConfig: Config): Config => {\n const collections = (incomingConfig.collections ?? []) as CollectionConfig[]\n const allBlocks = (incomingConfig.blocks ?? []) as Block[]\n\n // 1. Schema introspection\n const collectionSchemas = introspectCollections(collections)\n const blockCatalog = introspectBlocks(allBlocks)\n const blockNesting = buildBlockNestingMap(collections, allBlocks)\n const relationships = buildRelationshipGraph(collectionSchemas)\n\n // 2. Resolve preview siteUrl from explicit option, then Payload's own\n // serverURL, then conventional env vars. May still be undefined —\n // that's fine; relative-path preview URLs are skipped in that case.\n const previewSiteUrl =\n options.preview?.siteUrl ??\n incomingConfig.serverURL ??\n process.env.NEXT_PUBLIC_SERVER_URL ??\n process.env.SITE_URL\n\n // 3. Generate prompts and resources\n const prompts = generatePrompts(\n collectionSchemas,\n blockCatalog,\n blockNesting,\n relationships,\n options.domainPrompts,\n )\n const resources = generateResources(\n collectionSchemas,\n blockCatalog,\n blockNesting,\n relationships,\n )\n\n // 4. Build MCP collection configs (preview URL + draft behavior)\n const { mcpCollections, draftCollections } = generateMcpCollectionConfigs(collections, {\n siteUrl: previewSiteUrl,\n draftBehavior: options.draftBehavior,\n excludeCollections: options.exclude?.collections,\n previewDisabled: options.preview?.disabled,\n })\n\n // 5. Build the searchable-fields map for resolveReference\n const searchableCollections = new Map<string, string[]>()\n for (const [slug, schema] of collectionSchemas) {\n if (schema.searchableFields.length > 0) {\n searchableCollections.set(slug, schema.searchableFields)\n }\n }\n\n // 6. Tools\n const tools: any[] = [\n createPatchLayoutTool(blockCatalog, blockNesting, draftCollections),\n createPublishDraftTool(draftCollections),\n createResolveReferenceTool(searchableCollections),\n createSafeDeleteTool(relationships),\n createSearchContentTool(collectionSchemas),\n createUpdateDocumentTool(collectionSchemas, draftCollections),\n createUploadMediaTool({\n maxFileSize: options.mediaUpload?.maxFileSize,\n collectionSlug: options.mediaUpload?.collectionSlug,\n }),\n createListVersionsTool(draftCollections),\n createRestoreVersionTool(draftCollections),\n ]\n\n const schedulePublish = createSchedulePublishTool(collectionSchemas, draftCollections)\n if (schedulePublish) tools.push(schedulePublish)\n\n // 7. Globals — expose every non-excluded global with default capabilities\n const mcpGlobals: Record<string, { enabled: boolean; description?: string }> = {}\n const excludeGlobalSlugs = new Set(options.exclude?.globals ?? [])\n for (const global of (incomingConfig.globals ?? []) as Array<{ slug: string }>) {\n if (excludeGlobalSlugs.has(global.slug)) continue\n mcpGlobals[global.slug] = {\n enabled: true,\n description: `Manage ${global.slug} global settings`,\n }\n }\n\n // 8. Apply the official MCP plugin with our generated config.\n //\n // userCollection: passthrough — when omitted, the official plugin falls\n // back to `incomingConfig.admin.user`, which is already the canonical\n // Payload way to declare your auth collection.\n //\n // overrideAuth: rebinds req.user from the API key's linked user so our\n // custom tools' overrideAccess: false runs against the right identity.\n // Safe — getDefault() throws inside the official plugin if the API key\n // has no linked user, so settings.user is guaranteed here.\n const withMcp = mcpPlugin({\n collections: mcpCollections as any,\n globals: mcpGlobals as any,\n userCollection: options.userCollection as any,\n mcp: {\n tools: tools as any[],\n prompts: prompts as any[],\n resources: resources as any[],\n },\n overrideAuth: async (req, getDefault) => {\n const settings = await getDefault()\n req.user = (settings as any).user\n return settings\n },\n })\n\n return withMcp(incomingConfig)\n }\n}\n\nexport type {\n ContentToolkitOptions,\n DomainPrompt,\n CollectionSchema,\n BlockCatalog,\n BlockSchema,\n BlockNestingMap,\n BlockNestingEdge,\n RelationshipEdge,\n FieldSchema,\n} from './types'\n"],"names":["mcpPlugin","introspectCollections","introspectBlocks","buildBlockNestingMap","buildRelationshipGraph","generatePrompts","generateResources","generateMcpCollectionConfigs","createPatchLayoutTool","createPublishDraftTool","createResolveReferenceTool","createSafeDeleteTool","createSchedulePublishTool","createSearchContentTool","createUpdateDocumentTool","createUploadMediaTool","createListVersionsTool","createRestoreVersionTool","contentToolkitPlugin","options","incomingConfig","collections","allBlocks","blocks","collectionSchemas","blockCatalog","blockNesting","relationships","previewSiteUrl","preview","siteUrl","serverURL","process","env","NEXT_PUBLIC_SERVER_URL","SITE_URL","prompts","domainPrompts","resources","mcpCollections","draftCollections","draftBehavior","excludeCollections","exclude","previewDisabled","disabled","searchableCollections","Map","slug","schema","searchableFields","length","set","tools","maxFileSize","mediaUpload","collectionSlug","schedulePublish","push","mcpGlobals","excludeGlobalSlugs","Set","globals","global","has","enabled","description","withMcp","userCollection","mcp","overrideAuth","req","getDefault","settings","user"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import type { Block, CollectionConfig } from 'payload';
2
- import type { BlockCatalog, CollectionSchema, RelationshipEdge } from './types';
2
+ import type { BlockCatalog, BlockNestingMap, CollectionSchema, RelationshipEdge } from './types';
3
3
  /**
4
4
  * Introspect a Payload collection config into structured metadata.
5
5
  */
@@ -9,14 +9,21 @@ export declare function introspectCollection(collection: CollectionConfig): Coll
9
9
  */
10
10
  export declare function introspectCollections(collections: CollectionConfig[]): Map<string, CollectionSchema>;
11
11
  /**
12
- * Introspect block configs into a block catalog with section/leaf hierarchy and nesting rules.
12
+ * Build a flat catalog of every block in the schema. Whether a block can
13
+ * nest other blocks is represented separately in the BlockNestingMap, not
14
+ * as a section/leaf classification — the AI reads both and composes
15
+ * arbitrarily-nested layouts from there.
16
+ */
17
+ export declare function introspectBlocks(blocks: Block[]): BlockCatalog;
18
+ /**
19
+ * Walk every collection and every block, recording each `blocks`-typed
20
+ * field's owner, dotted path, and accepted slugs.
13
21
  *
14
- * NOTE: In the Payload plugin context, blockReferences may or may not be resolved
15
- * depending on when introspection runs. This function handles both cases:
16
- * - If field.blocks contains resolved block objects, reads slugs from them
17
- * - If field.blocks is empty and field.blockReferences exists, uses those slugs directly
22
+ * The AI uses this to compose layouts at any depth: it looks up which
23
+ * slugs the relevant field accepts, picks one, then if that block has
24
+ * its own `blocks` fields it recurses against the same map.
18
25
  */
19
- export declare function introspectBlocks(sectionBlocks: Block[], leafBlocks: Block[]): BlockCatalog;
26
+ export declare function buildBlockNestingMap(collections: CollectionConfig[], blocks: Block[]): BlockNestingMap;
20
27
  /**
21
28
  * Build a relationship graph from introspected collection schemas.
22
29
  */