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 CHANGED
@@ -23,13 +23,12 @@ The official Payload MCP plugin gives every collection a generic CRUD surface. T
23
23
  **Auto-generated resources** (machine-readable JSON for the LLM):
24
24
  - `blocks://catalog`, `collections://schema`, `collections://relationships`.
25
25
 
26
- **Custom tools** (11 total):
26
+ **Custom tools (10, plus an auto-registered scheduler)**
27
27
 
28
28
  *Authoring*
29
- - `composePageLayout` — build a validated page layout from sections + leaves.
30
- - `patchLayout` — surgically append/prepend/insertAt/replaceAt sections on a doc's block-array field without round-tripping the whole array. Safer than `updateDocument` for incremental layout edits.
29
+ - `patchLayout` — surgical append/prepend/insertAt/replaceAt against any blocks-typed field. Validates each block (recursively, at any depth) against the introspected nesting map. Safer than `updateDocument` for incremental layout edits.
31
30
  - `updateDocument` — Local-API based update that survives the upload-field bug in the official plugin.
32
- - `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe), create a Media doc.
31
+ - `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe with streaming size cap), create a Media doc.
33
32
 
34
33
  *Discovery*
35
34
  - `resolveReference` — search collections by name/title/slug for relationship IDs.
@@ -37,14 +36,14 @@ The official Payload MCP plugin gives every collection a generic CRUD surface. T
37
36
 
38
37
  *Lifecycle / safety*
39
38
  - `publishDraft` — flip `_status` from draft to published.
40
- - `schedulePublish` — **bring your own scheduler.** Stamps a future `publishedAt` on a draft and leaves `_status: 'draft'`; it does **not** itself flip status at the appointed time. Auto-registered only for collections that have both drafts AND a `publishedAt` date field. To actually publish on schedule, you must wire up one of: a [Payload Jobs Queue scheduled task](https://payloadcms.com/docs/jobs-queue/scheduled-jobs), an external cron worker, or a `beforeRead` hook that resolves status on the fly. Without one of those, scheduled drafts stay drafts forever — the tool says so in its response, but it's still a footgun if you skim past it.
41
- - `listVersions` — recent saved versions of a draft document with id/status/timestamp/displayName.
42
- - `restoreVersion` — roll a document back to a saved version (creates a new version on top, so itself reversible).
43
- - `safeDelete` — relationship-aware delete. Walks the introspected relationship graph, refuses with a structured impact summary if other documents reference the target. Override with `confirm: true` after reviewing.
39
+ - `schedulePublish` — **bring your own scheduler.** Stamps a future `publishedAt` on a draft and leaves `_status: 'draft'`; it does **not** itself flip status at the appointed time. Auto-registered only for collections that have both drafts AND a `publishedAt` date field. Wire up a [Payload Jobs Queue task](https://payloadcms.com/docs/jobs-queue/scheduled-jobs), external cron, or `beforeRead` hook to actually publish on schedule.
40
+ - `listVersions` — recent saved versions of a draft document.
41
+ - `restoreVersion` — roll a document back to a saved version (creates a new version on top, so reversible).
42
+ - `safeDelete` — relationship-aware delete. Walks the relationship graph, refuses with a structured impact summary if other documents reference the target. Fail-closed on permission errors. Override with `confirm: true`.
44
43
 
45
44
  **Draft workflow** wired into the official plugin's `mcpCollections`:
46
- - Disables raw `update` for `always-draft` collections so clients go through `publishDraft` (or `patchLayout` / `updateDocument`, both of which preserve draft semantics).
47
- - Appends preview URLs to draft responses (path prefixes are configurable via `previewPaths`).
45
+ - For collections with `versions.drafts` enabled, disables raw `update` so clients go through `publishDraft` / `patchLayout` / `updateDocument` (all of which preserve draft semantics).
46
+ - Appends preview URLs to draft responses by calling each collection's own `admin.livePreview.url` or `admin.preview` function — no separate path config needed.
48
47
 
49
48
  ## Install
50
49
 
@@ -54,7 +53,7 @@ pnpm add payload-mcp-toolkit @payloadcms/plugin-mcp
54
53
 
55
54
  Peer dependencies: `payload` ^3, `@payloadcms/plugin-mcp` ^3, `zod` ^3.
56
55
 
57
- ## Use
56
+ ## Use — zero config
58
57
 
59
58
  ```ts
60
59
  // payload.config.ts
@@ -62,43 +61,61 @@ import { contentToolkitPlugin } from 'payload-mcp-toolkit'
62
61
 
63
62
  export default buildConfig({
64
63
  // ...your collections, blocks, globals
65
- plugins: [
66
- contentToolkitPlugin({
67
- siteUrl: process.env.SITE_URL!,
68
- previewSecret: process.env.PREVIEW_SECRET!,
69
- previewPaths: {
70
- pages: '', // pages live at /
71
- posts: '/blog', // posts live at /blog/:slug
72
- },
73
- draftBehavior: {
74
- pages: 'always-draft',
75
- posts: 'always-draft',
76
- },
77
- domainPrompts: [
78
- // Optional site-specific vocabulary — see examples/
79
- ],
80
- }),
81
- ],
64
+ serverURL: process.env.SITE_URL, // used for absolute preview URLs
65
+ admin: { user: 'users' }, // your auth collection
66
+ plugins: [contentToolkitPlugin()],
82
67
  })
83
68
  ```
84
69
 
85
- That's it. The toolkit reads your Payload config and registers everything against the official MCP plugin. Connect any MCP client to your Payload server and the LLM will see the prompts, resources, and tools.
70
+ That's it. The toolkit infers everything from your Payload config:
71
+ - **Draft behavior** — collections with `versions.drafts` get `always-draft` (raw update locked); others publish immediately.
72
+ - **Preview URLs** — pulled from each collection's `admin.livePreview.url` (or `admin.preview` as a fallback). If neither is set, draft responses just get a generic admin-panel hint.
73
+ - **Block nesting** — for every blocks-typed field, anywhere in the schema, the toolkit records which slugs are allowed. The AI composes layouts at any depth from that map.
74
+ - **Auth collection** — comes from `admin.user` (the standard Payload setting). The official plugin handles this directly.
75
+
76
+ ## Optional configuration
86
77
 
87
- See `examples/angels-config.example.ts` for a fully-worked domain-prompt setup from a real-world site.
78
+ Every option is an escape hatch pass only what you need:
88
79
 
89
- ## Configuration reference
80
+ ```ts
81
+ contentToolkitPlugin({
82
+ preview: {
83
+ siteUrl: 'https://staging.example.com', // override serverURL
84
+ disabled: false, // set true to suppress preview URLs entirely
85
+ },
86
+ draftBehavior: {
87
+ posts: 'always-publish', // allow raw update on a draftable collection
88
+ },
89
+ userCollection: 'admins', // override admin.user
90
+ exclude: {
91
+ collections: ['internal-bookkeeping'],
92
+ globals: ['secret-config'],
93
+ },
94
+ mediaUpload: {
95
+ maxFileSize: 25 * 1024 * 1024,
96
+ collectionSlug: 'images',
97
+ },
98
+ domainPrompts: [
99
+ {
100
+ name: 'siteVocabulary',
101
+ title: 'Site Vocabulary',
102
+ description: 'Site-specific terms the AI should know.',
103
+ content: '...',
104
+ },
105
+ ],
106
+ })
107
+ ```
90
108
 
91
- | Option | Type | Description |
92
- |---|---|---|
93
- | `siteUrl` | `string` | Base URL used to construct preview URLs. |
94
- | `previewSecret` | `string` | Secret embedded in the preview URL query. |
95
- | `previewPaths` | `Record<string, string>` | Per-collection URL path prefix. Defaults to `/{slug}` if omitted. Use `''` for collections at the site root. |
96
- | `draftBehavior` | `Record<string, 'always-draft' \| 'always-publish'>` | Override the default draft behavior per collection. |
97
- | `domainPrompts` | `DomainPrompt[]` | Custom prompts that teach the AI site-specific vocabulary. |
98
- | `mediaUpload.maxFileSize` | `number` | Max bytes for `uploadMedia` (default 10MB). |
99
- | `mediaUpload.collectionSlug` | `string` | Media collection slug (default `media`). |
100
- | `excludeCollections` | `string[]` | Collection slugs to hide from MCP. |
101
- | `excludeGlobals` | `string[]` | Global slugs to hide from MCP. |
109
+ | Option | Description |
110
+ |---|---|
111
+ | `preview.siteUrl` | Base URL for preview links. Defaults to `serverURL`, then `NEXT_PUBLIC_SERVER_URL`/`SITE_URL` env vars. |
112
+ | `preview.disabled` | Suppress preview URL injection on draft responses. |
113
+ | `draftBehavior` | Per-collection override of inferred behavior. |
114
+ | `userCollection` | Override `admin.user` for API key linkage. |
115
+ | `exclude.collections` / `exclude.globals` | Hide from MCP exposure. |
116
+ | `domainPrompts` | Site-specific vocabulary prompts. |
117
+ | `mediaUpload.maxFileSize` | Default 10MB. Enforced as a streaming cap, not a post-buffer check. |
118
+ | `mediaUpload.collectionSlug` | Default `'media'`. |
102
119
 
103
120
  ## Development
104
121
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { introspectCollection, introspectCollections, introspectBlocks, buildRelationshipGraph } from '../introspection';
2
+ import { introspectCollection, introspectCollections, introspectBlocks, buildBlockNestingMap, buildRelationshipGraph } from '../introspection';
3
3
  // ─── Sample schema (kept inline so the test is self-contained) ─────
4
4
  const Media = {
5
5
  slug: 'media',
@@ -47,7 +47,7 @@ const Authors = {
47
47
  }
48
48
  ]
49
49
  };
50
- // Leaf blocks
50
+ // Leaf-style blocks
51
51
  const Heading = {
52
52
  slug: 'heading',
53
53
  fields: [
@@ -102,19 +102,18 @@ const ImageBlock = {
102
102
  }
103
103
  ]
104
104
  };
105
- const allLeafBlocks = [
106
- Heading,
107
- RichText,
108
- ImageBlock
109
- ];
110
- // Section blocks
105
+ // Container-style blocks (have nested blocks fields)
111
106
  const FullWidth = {
112
107
  slug: 'fullWidth',
113
108
  fields: [
114
109
  {
115
110
  name: 'content',
116
111
  type: 'blocks',
117
- blocks: allLeafBlocks
112
+ blocks: [
113
+ Heading,
114
+ RichText,
115
+ ImageBlock
116
+ ]
118
117
  }
119
118
  ]
120
119
  };
@@ -149,10 +148,39 @@ const CtaBanner = {
149
148
  }
150
149
  ]
151
150
  };
152
- const allSectionBlocks = [
151
+ // Deeply-nestable container — exercises the recursive path
152
+ const Accordion = {
153
+ slug: 'accordion',
154
+ fields: [
155
+ {
156
+ name: 'panels',
157
+ type: 'array',
158
+ fields: [
159
+ {
160
+ name: 'title',
161
+ type: 'text'
162
+ },
163
+ {
164
+ name: 'body',
165
+ type: 'blocks',
166
+ blocks: [
167
+ Heading,
168
+ RichText,
169
+ FullWidth
170
+ ]
171
+ }
172
+ ]
173
+ }
174
+ ]
175
+ };
176
+ const allBlocks = [
177
+ Heading,
178
+ RichText,
179
+ ImageBlock,
153
180
  FullWidth,
154
181
  HeadingOnly,
155
- CtaBanner
182
+ CtaBanner,
183
+ Accordion
156
184
  ];
157
185
  const Posts = {
158
186
  slug: 'posts',
@@ -242,7 +270,12 @@ const Pages = {
242
270
  {
243
271
  name: 'layout',
244
272
  type: 'blocks',
245
- blocks: allSectionBlocks
273
+ blocks: [
274
+ FullWidth,
275
+ HeadingOnly,
276
+ CtaBanner,
277
+ Accordion
278
+ ]
246
279
  }
247
280
  ]
248
281
  }
@@ -250,7 +283,7 @@ const Pages = {
250
283
  }
251
284
  ]
252
285
  };
253
- // ─── Tests ─────────────────────────────────────────────────────────
286
+ // ─── introspectCollection ──────────────────────────────────────────
254
287
  describe('introspectCollection', ()=>{
255
288
  it('extracts Posts collection fields, relationships, and draft status', ()=>{
256
289
  const schema = introspectCollection(Posts);
@@ -292,44 +325,24 @@ describe('introspectCollection', ()=>{
292
325
  expect(heroSize.options.length).toBe(3);
293
326
  });
294
327
  });
328
+ // ─── introspectBlocks (flat catalog) ───────────────────────────────
295
329
  describe('introspectBlocks', ()=>{
296
- it('discovers fullWidth accepts all leaf blocks (composable)', ()=>{
297
- const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
298
- const fullWidth = catalog.sections.find((s)=>s.slug === 'fullWidth');
299
- expect(fullWidth).toBeDefined();
300
- expect(fullWidth.nestingType).toBe('composable');
301
- expect(fullWidth.acceptedLeafSlugs.length).toBe(allLeafBlocks.length);
302
- });
303
- it('marks ctaBanner as fixed (no nested blocks)', ()=>{
304
- const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
305
- const ctaBanner = catalog.sections.find((s)=>s.slug === 'ctaBanner');
306
- expect(ctaBanner).toBeDefined();
307
- expect(ctaBanner.nestingType).toBe('fixed');
308
- expect(ctaBanner.acceptedLeafSlugs).toHaveLength(0);
309
- });
310
- it('marks headingOnly as constrained (single leaf type, maxRows: 1)', ()=>{
311
- const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
312
- const headingOnly = catalog.sections.find((s)=>s.slug === 'headingOnly');
313
- expect(headingOnly).toBeDefined();
314
- expect(headingOnly.nestingType).toBe('constrained');
315
- expect(headingOnly.acceptedLeafSlugs).toEqual([
316
- 'heading'
317
- ]);
318
- expect(headingOnly.maxRows).toBe(1);
319
- });
320
- it('extracts all leaf blocks', ()=>{
321
- const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
322
- expect(catalog.leaves).toHaveLength(3);
323
- const leafSlugs = catalog.leaves.map((l)=>l.slug);
324
- expect(leafSlugs).toEqual([
330
+ it('returns a flat catalog of every block with no section/leaf split', ()=>{
331
+ const catalog = introspectBlocks(allBlocks);
332
+ const slugs = catalog.blocks.map((b)=>b.slug);
333
+ expect(slugs).toEqual([
325
334
  'heading',
326
335
  'richText',
327
- 'image'
336
+ 'image',
337
+ 'fullWidth',
338
+ 'headingOnly',
339
+ 'ctaBanner',
340
+ 'accordion'
328
341
  ]);
329
342
  });
330
- it('extracts leaf block fields including select options', ()=>{
331
- const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks);
332
- const heading = catalog.leaves.find((l)=>l.slug === 'heading');
343
+ it('extracts each block\'s fields including select options', ()=>{
344
+ const catalog = introspectBlocks(allBlocks);
345
+ const heading = catalog.blocks.find((b)=>b.slug === 'heading');
333
346
  expect(heading).toBeDefined();
334
347
  const headingFieldNames = heading.fields.map((f)=>f.name);
335
348
  expect(headingFieldNames).toEqual([
@@ -341,6 +354,88 @@ describe('introspectBlocks', ()=>{
341
354
  expect(level.options).toBeDefined();
342
355
  });
343
356
  });
357
+ // ─── buildBlockNestingMap ──────────────────────────────────────────
358
+ describe('buildBlockNestingMap', ()=>{
359
+ it('records the layout field on Pages with the slugs it accepts', ()=>{
360
+ const map = buildBlockNestingMap([
361
+ Pages,
362
+ Posts
363
+ ], allBlocks);
364
+ const pageLayout = map.find((e)=>e.ownerType === 'collection' && e.owner === 'pages' && e.fieldPath === 'layout');
365
+ expect(pageLayout).toBeDefined();
366
+ expect(pageLayout.acceptedBlockSlugs).toEqual([
367
+ 'fullWidth',
368
+ 'headingOnly',
369
+ 'ctaBanner',
370
+ 'accordion'
371
+ ]);
372
+ });
373
+ it('records nested blocks fields inside container blocks', ()=>{
374
+ const map = buildBlockNestingMap([
375
+ Pages
376
+ ], allBlocks);
377
+ const fullWidthContent = map.find((e)=>e.ownerType === 'block' && e.owner === 'fullWidth' && e.fieldPath === 'content');
378
+ expect(fullWidthContent).toBeDefined();
379
+ expect(fullWidthContent.acceptedBlockSlugs).toEqual([
380
+ 'heading',
381
+ 'richText',
382
+ 'image'
383
+ ]);
384
+ const headingOnly = map.find((e)=>e.ownerType === 'block' && e.owner === 'headingOnly' && e.fieldPath === 'content');
385
+ expect(headingOnly.acceptedBlockSlugs).toEqual([
386
+ 'heading'
387
+ ]);
388
+ expect(headingOnly.maxRows).toBe(1);
389
+ });
390
+ it('handles arbitrarily-deep nesting via array fields inside blocks', ()=>{
391
+ const map = buildBlockNestingMap([
392
+ Pages
393
+ ], allBlocks);
394
+ const accordionPanelBody = map.find((e)=>e.ownerType === 'block' && e.owner === 'accordion' && e.fieldPath === 'panels[].body');
395
+ expect(accordionPanelBody).toBeDefined();
396
+ expect(accordionPanelBody.acceptedBlockSlugs).toEqual([
397
+ 'heading',
398
+ 'richText',
399
+ 'fullWidth'
400
+ ]);
401
+ });
402
+ it('omits unknown slugs not present in the block list', ()=>{
403
+ const Stray = {
404
+ slug: 'stray',
405
+ fields: [
406
+ {
407
+ name: 'layout',
408
+ type: 'blocks',
409
+ blocks: [
410
+ Heading,
411
+ {
412
+ slug: 'mystery',
413
+ fields: []
414
+ }
415
+ ]
416
+ }
417
+ ]
418
+ };
419
+ const map = buildBlockNestingMap([
420
+ Stray
421
+ ], [
422
+ Heading
423
+ ]) // mystery not in catalog
424
+ ;
425
+ const stray = map.find((e)=>e.owner === 'stray' && e.fieldPath === 'layout');
426
+ expect(stray.acceptedBlockSlugs).toEqual([
427
+ 'heading'
428
+ ]);
429
+ });
430
+ it('omits fixed blocks (no nested blocks fields) from the map', ()=>{
431
+ const map = buildBlockNestingMap([
432
+ Pages
433
+ ], allBlocks);
434
+ const ctaEntries = map.filter((e)=>e.owner === 'ctaBanner');
435
+ expect(ctaEntries).toHaveLength(0);
436
+ });
437
+ });
438
+ // ─── buildRelationshipGraph ────────────────────────────────────────
344
439
  describe('buildRelationshipGraph', ()=>{
345
440
  it('builds correct graph from sample collections', ()=>{
346
441
  const schemas = introspectCollections([
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/__tests__/introspection.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport type { Block, CollectionConfig } from 'payload'\nimport {\n introspectCollection,\n introspectCollections,\n introspectBlocks,\n buildRelationshipGraph,\n} from '../introspection'\n\n// ─── Sample schema (kept inline so the test is self-contained) ─────\n\nconst Media: CollectionConfig = {\n slug: 'media',\n upload: true,\n fields: [{ name: 'alt', type: 'text', required: true }],\n}\n\nconst Categories: CollectionConfig = {\n slug: 'categories',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n ],\n}\n\nconst Authors: CollectionConfig = {\n slug: 'authors',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'avatar', type: 'upload', relationTo: 'media' },\n ],\n}\n\n// Leaf blocks\nconst Heading: Block = {\n slug: 'heading',\n fields: [\n { name: 'text', type: 'text', required: true },\n {\n name: 'level',\n type: 'select',\n options: ['h1', 'h2', 'h3'],\n defaultValue: 'h2',\n },\n {\n name: 'align',\n type: 'select',\n options: ['left', 'center', 'right'],\n defaultValue: 'left',\n },\n ],\n}\n\nconst RichText: Block = {\n slug: 'richText',\n fields: [{ name: 'content', type: 'richText' }],\n}\n\nconst ImageBlock: Block = {\n slug: 'image',\n fields: [\n { name: 'image', type: 'upload', relationTo: 'media', required: true },\n { name: 'caption', type: 'text' },\n ],\n}\n\nconst allLeafBlocks: Block[] = [Heading, RichText, ImageBlock]\n\n// Section blocks\nconst FullWidth: Block = {\n slug: 'fullWidth',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n blocks: allLeafBlocks,\n },\n ],\n}\n\nconst HeadingOnly: Block = {\n slug: 'headingOnly',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n maxRows: 1,\n blocks: [Heading],\n },\n ],\n}\n\nconst CtaBanner: Block = {\n slug: 'ctaBanner',\n fields: [\n { name: 'headline', type: 'text', required: true },\n { name: 'buttonLabel', type: 'text' },\n { name: 'buttonHref', type: 'text' },\n ],\n}\n\nconst allSectionBlocks: Block[] = [FullWidth, HeadingOnly, CtaBanner]\n\nconst Posts: CollectionConfig = {\n slug: 'posts',\n versions: { drafts: true },\n fields: [\n { name: 'title', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'featured', type: 'checkbox' },\n { name: 'category', type: 'relationship', relationTo: 'categories' },\n {\n name: 'authors',\n type: 'relationship',\n relationTo: 'authors',\n hasMany: true,\n },\n { name: 'coverImage', type: 'upload', relationTo: 'media' },\n {\n name: 'tags',\n type: 'array',\n fields: [{ name: 'tag', type: 'text' }],\n },\n ],\n}\n\nconst Pages: CollectionConfig = {\n slug: 'pages',\n versions: { drafts: true },\n fields: [\n {\n type: 'tabs',\n tabs: [\n {\n name: 'hero',\n label: 'Hero',\n fields: [\n { name: 'heroTitle', type: 'text' },\n {\n name: 'heroSize',\n type: 'select',\n options: ['small', 'medium', 'large'],\n defaultValue: 'medium',\n },\n ],\n },\n {\n label: 'Content',\n fields: [\n { name: 'slug', type: 'text', required: true },\n { name: 'layout', type: 'blocks', blocks: allSectionBlocks },\n ],\n },\n ],\n },\n ],\n}\n\n// ─── Tests ─────────────────────────────────────────────────────────\n\ndescribe('introspectCollection', () => {\n it('extracts Posts collection fields, relationships, and draft status', () => {\n const schema = introspectCollection(Posts)\n\n expect(schema.slug).toBe('posts')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('title')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('featured')\n expect(fieldNames).toContain('tags')\n\n const relFieldNames = schema.relationships.map((r) => r.fieldName)\n expect(relFieldNames).toContain('category')\n expect(relFieldNames).toContain('authors')\n\n const cover = schema.relationships.find((r) => r.fieldName === 'coverImage')\n expect(cover).toBeDefined()\n expect(cover!.relationTo).toBe('media')\n\n expect(schema.searchableFields).toContain('title')\n expect(schema.searchableFields).toContain('slug')\n })\n\n it('extracts Pages collection with tab-nested fields', () => {\n const schema = introspectCollection(Pages)\n\n expect(schema.slug).toBe('pages')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('heroTitle')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('layout')\n })\n\n it('detects collections without draft support', () => {\n const schema = introspectCollection(Categories)\n expect(schema.hasDrafts).toBe(false)\n })\n\n it('extracts select field options from Pages heroSize', () => {\n const schema = introspectCollection(Pages)\n const heroSize = schema.fields.find((f) => f.name === 'heroSize')\n expect(heroSize).toBeDefined()\n expect(heroSize!.type).toBe('select')\n expect(heroSize!.options).toBeDefined()\n expect(heroSize!.options!.length).toBe(3)\n })\n})\n\ndescribe('introspectBlocks', () => {\n it('discovers fullWidth accepts all leaf blocks (composable)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const fullWidth = catalog.sections.find((s) => s.slug === 'fullWidth')\n expect(fullWidth).toBeDefined()\n expect(fullWidth!.nestingType).toBe('composable')\n expect(fullWidth!.acceptedLeafSlugs.length).toBe(allLeafBlocks.length)\n })\n\n it('marks ctaBanner as fixed (no nested blocks)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const ctaBanner = catalog.sections.find((s) => s.slug === 'ctaBanner')\n expect(ctaBanner).toBeDefined()\n expect(ctaBanner!.nestingType).toBe('fixed')\n expect(ctaBanner!.acceptedLeafSlugs).toHaveLength(0)\n })\n\n it('marks headingOnly as constrained (single leaf type, maxRows: 1)', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n\n const headingOnly = catalog.sections.find((s) => s.slug === 'headingOnly')\n expect(headingOnly).toBeDefined()\n expect(headingOnly!.nestingType).toBe('constrained')\n expect(headingOnly!.acceptedLeafSlugs).toEqual(['heading'])\n expect(headingOnly!.maxRows).toBe(1)\n })\n\n it('extracts all leaf blocks', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n expect(catalog.leaves).toHaveLength(3)\n const leafSlugs = catalog.leaves.map((l) => l.slug)\n expect(leafSlugs).toEqual(['heading', 'richText', 'image'])\n })\n\n it('extracts leaf block fields including select options', () => {\n const catalog = introspectBlocks(allSectionBlocks, allLeafBlocks)\n const heading = catalog.leaves.find((l) => l.slug === 'heading')\n expect(heading).toBeDefined()\n const headingFieldNames = heading!.fields.map((f) => f.name)\n expect(headingFieldNames).toEqual(['text', 'level', 'align'])\n const level = heading!.fields.find((f) => f.name === 'level')\n expect(level!.options).toBeDefined()\n })\n})\n\ndescribe('buildRelationshipGraph', () => {\n it('builds correct graph from sample collections', () => {\n const schemas = introspectCollections([Posts, Pages, Categories, Authors, Media])\n const edges = buildRelationshipGraph(schemas)\n\n const postEdges = edges.filter((e) => e.fromCollection === 'posts')\n const postTargets = postEdges.map((e) => e.toCollection)\n expect(postTargets).toContain('categories')\n expect(postTargets).toContain('authors')\n expect(postTargets).toContain('media')\n\n const authorEdges = edges.filter((e) => e.fromCollection === 'authors')\n expect(authorEdges.map((e) => e.toCollection)).toContain('media')\n })\n})\n"],"names":["describe","it","expect","introspectCollection","introspectCollections","introspectBlocks","buildRelationshipGraph","Media","slug","upload","fields","name","type","required","Categories","Authors","relationTo","Heading","options","defaultValue","RichText","ImageBlock","allLeafBlocks","FullWidth","blocks","HeadingOnly","maxRows","CtaBanner","allSectionBlocks","Posts","versions","drafts","hasMany","Pages","tabs","label","schema","toBe","hasDrafts","fieldNames","map","f","toContain","relFieldNames","relationships","r","fieldName","cover","find","toBeDefined","searchableFields","heroSize","length","catalog","fullWidth","sections","s","nestingType","acceptedLeafSlugs","ctaBanner","toHaveLength","headingOnly","toEqual","leaves","leafSlugs","l","heading","headingFieldNames","level","schemas","edges","postEdges","filter","e","fromCollection","postTargets","toCollection","authorEdges"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/__tests__/introspection.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\nimport type { Block, CollectionConfig } from 'payload'\nimport {\n introspectCollection,\n introspectCollections,\n introspectBlocks,\n buildBlockNestingMap,\n buildRelationshipGraph,\n} from '../introspection'\n\n// ─── Sample schema (kept inline so the test is self-contained) ─────\n\nconst Media: CollectionConfig = {\n slug: 'media',\n upload: true,\n fields: [{ name: 'alt', type: 'text', required: true }],\n}\n\nconst Categories: CollectionConfig = {\n slug: 'categories',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n ],\n}\n\nconst Authors: CollectionConfig = {\n slug: 'authors',\n fields: [\n { name: 'name', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'avatar', type: 'upload', relationTo: 'media' },\n ],\n}\n\n// Leaf-style blocks\nconst Heading: Block = {\n slug: 'heading',\n fields: [\n { name: 'text', type: 'text', required: true },\n {\n name: 'level',\n type: 'select',\n options: ['h1', 'h2', 'h3'],\n defaultValue: 'h2',\n },\n {\n name: 'align',\n type: 'select',\n options: ['left', 'center', 'right'],\n defaultValue: 'left',\n },\n ],\n}\n\nconst RichText: Block = {\n slug: 'richText',\n fields: [{ name: 'content', type: 'richText' }],\n}\n\nconst ImageBlock: Block = {\n slug: 'image',\n fields: [\n { name: 'image', type: 'upload', relationTo: 'media', required: true },\n { name: 'caption', type: 'text' },\n ],\n}\n\n// Container-style blocks (have nested blocks fields)\nconst FullWidth: Block = {\n slug: 'fullWidth',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n blocks: [Heading, RichText, ImageBlock],\n },\n ],\n}\n\nconst HeadingOnly: Block = {\n slug: 'headingOnly',\n fields: [\n {\n name: 'content',\n type: 'blocks',\n maxRows: 1,\n blocks: [Heading],\n },\n ],\n}\n\nconst CtaBanner: Block = {\n slug: 'ctaBanner',\n fields: [\n { name: 'headline', type: 'text', required: true },\n { name: 'buttonLabel', type: 'text' },\n { name: 'buttonHref', type: 'text' },\n ],\n}\n\n// Deeply-nestable container — exercises the recursive path\nconst Accordion: Block = {\n slug: 'accordion',\n fields: [\n {\n name: 'panels',\n type: 'array',\n fields: [\n { name: 'title', type: 'text' },\n {\n name: 'body',\n type: 'blocks',\n blocks: [Heading, RichText, FullWidth],\n },\n ],\n },\n ],\n}\n\nconst allBlocks: Block[] = [Heading, RichText, ImageBlock, FullWidth, HeadingOnly, CtaBanner, Accordion]\n\nconst Posts: CollectionConfig = {\n slug: 'posts',\n versions: { drafts: true },\n fields: [\n { name: 'title', type: 'text', required: true },\n { name: 'slug', type: 'text', required: true },\n { name: 'featured', type: 'checkbox' },\n { name: 'category', type: 'relationship', relationTo: 'categories' },\n {\n name: 'authors',\n type: 'relationship',\n relationTo: 'authors',\n hasMany: true,\n },\n { name: 'coverImage', type: 'upload', relationTo: 'media' },\n {\n name: 'tags',\n type: 'array',\n fields: [{ name: 'tag', type: 'text' }],\n },\n ],\n}\n\nconst Pages: CollectionConfig = {\n slug: 'pages',\n versions: { drafts: true },\n fields: [\n {\n type: 'tabs',\n tabs: [\n {\n name: 'hero',\n label: 'Hero',\n fields: [\n { name: 'heroTitle', type: 'text' },\n {\n name: 'heroSize',\n type: 'select',\n options: ['small', 'medium', 'large'],\n defaultValue: 'medium',\n },\n ],\n },\n {\n label: 'Content',\n fields: [\n { name: 'slug', type: 'text', required: true },\n { name: 'layout', type: 'blocks', blocks: [FullWidth, HeadingOnly, CtaBanner, Accordion] },\n ],\n },\n ],\n },\n ],\n}\n\n// ─── introspectCollection ──────────────────────────────────────────\n\ndescribe('introspectCollection', () => {\n it('extracts Posts collection fields, relationships, and draft status', () => {\n const schema = introspectCollection(Posts)\n\n expect(schema.slug).toBe('posts')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('title')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('featured')\n expect(fieldNames).toContain('tags')\n\n const relFieldNames = schema.relationships.map((r) => r.fieldName)\n expect(relFieldNames).toContain('category')\n expect(relFieldNames).toContain('authors')\n\n const cover = schema.relationships.find((r) => r.fieldName === 'coverImage')\n expect(cover).toBeDefined()\n expect(cover!.relationTo).toBe('media')\n\n expect(schema.searchableFields).toContain('title')\n expect(schema.searchableFields).toContain('slug')\n })\n\n it('extracts Pages collection with tab-nested fields', () => {\n const schema = introspectCollection(Pages)\n\n expect(schema.slug).toBe('pages')\n expect(schema.hasDrafts).toBe(true)\n\n const fieldNames = schema.fields.map((f) => f.name)\n expect(fieldNames).toContain('heroTitle')\n expect(fieldNames).toContain('slug')\n expect(fieldNames).toContain('layout')\n })\n\n it('detects collections without draft support', () => {\n const schema = introspectCollection(Categories)\n expect(schema.hasDrafts).toBe(false)\n })\n\n it('extracts select field options from Pages heroSize', () => {\n const schema = introspectCollection(Pages)\n const heroSize = schema.fields.find((f) => f.name === 'heroSize')\n expect(heroSize).toBeDefined()\n expect(heroSize!.type).toBe('select')\n expect(heroSize!.options).toBeDefined()\n expect(heroSize!.options!.length).toBe(3)\n })\n})\n\n// ─── introspectBlocks (flat catalog) ───────────────────────────────\n\ndescribe('introspectBlocks', () => {\n it('returns a flat catalog of every block with no section/leaf split', () => {\n const catalog = introspectBlocks(allBlocks)\n const slugs = catalog.blocks.map((b) => b.slug)\n expect(slugs).toEqual([\n 'heading',\n 'richText',\n 'image',\n 'fullWidth',\n 'headingOnly',\n 'ctaBanner',\n 'accordion',\n ])\n })\n\n it('extracts each block\\'s fields including select options', () => {\n const catalog = introspectBlocks(allBlocks)\n const heading = catalog.blocks.find((b) => b.slug === 'heading')\n expect(heading).toBeDefined()\n const headingFieldNames = heading!.fields.map((f) => f.name)\n expect(headingFieldNames).toEqual(['text', 'level', 'align'])\n const level = heading!.fields.find((f) => f.name === 'level')\n expect(level!.options).toBeDefined()\n })\n})\n\n// ─── buildBlockNestingMap ──────────────────────────────────────────\n\ndescribe('buildBlockNestingMap', () => {\n it('records the layout field on Pages with the slugs it accepts', () => {\n const map = buildBlockNestingMap([Pages, Posts], allBlocks)\n const pageLayout = map.find(\n (e) => e.ownerType === 'collection' && e.owner === 'pages' && e.fieldPath === 'layout',\n )\n expect(pageLayout).toBeDefined()\n expect(pageLayout!.acceptedBlockSlugs).toEqual(['fullWidth', 'headingOnly', 'ctaBanner', 'accordion'])\n })\n\n it('records nested blocks fields inside container blocks', () => {\n const map = buildBlockNestingMap([Pages], allBlocks)\n\n const fullWidthContent = map.find(\n (e) => e.ownerType === 'block' && e.owner === 'fullWidth' && e.fieldPath === 'content',\n )\n expect(fullWidthContent).toBeDefined()\n expect(fullWidthContent!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'image'])\n\n const headingOnly = map.find(\n (e) => e.ownerType === 'block' && e.owner === 'headingOnly' && e.fieldPath === 'content',\n )\n expect(headingOnly!.acceptedBlockSlugs).toEqual(['heading'])\n expect(headingOnly!.maxRows).toBe(1)\n })\n\n it('handles arbitrarily-deep nesting via array fields inside blocks', () => {\n const map = buildBlockNestingMap([Pages], allBlocks)\n\n const accordionPanelBody = map.find(\n (e) =>\n e.ownerType === 'block' && e.owner === 'accordion' && e.fieldPath === 'panels[].body',\n )\n expect(accordionPanelBody).toBeDefined()\n expect(accordionPanelBody!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'fullWidth'])\n })\n\n it('omits unknown slugs not present in the block list', () => {\n const Stray: CollectionConfig = {\n slug: 'stray',\n fields: [\n {\n name: 'layout',\n type: 'blocks',\n blocks: [Heading, { slug: 'mystery', fields: [] } as Block],\n },\n ],\n }\n const map = buildBlockNestingMap([Stray], [Heading]) // mystery not in catalog\n const stray = map.find((e) => e.owner === 'stray' && e.fieldPath === 'layout')\n expect(stray!.acceptedBlockSlugs).toEqual(['heading'])\n })\n\n it('omits fixed blocks (no nested blocks fields) from the map', () => {\n const map = buildBlockNestingMap([Pages], allBlocks)\n const ctaEntries = map.filter((e) => e.owner === 'ctaBanner')\n expect(ctaEntries).toHaveLength(0)\n })\n})\n\n// ─── buildRelationshipGraph ────────────────────────────────────────\n\ndescribe('buildRelationshipGraph', () => {\n it('builds correct graph from sample collections', () => {\n const schemas = introspectCollections([Posts, Pages, Categories, Authors, Media])\n const edges = buildRelationshipGraph(schemas)\n\n const postEdges = edges.filter((e) => e.fromCollection === 'posts')\n const postTargets = postEdges.map((e) => e.toCollection)\n expect(postTargets).toContain('categories')\n expect(postTargets).toContain('authors')\n expect(postTargets).toContain('media')\n\n const authorEdges = edges.filter((e) => e.fromCollection === 'authors')\n expect(authorEdges.map((e) => e.toCollection)).toContain('media')\n })\n})\n"],"names":["describe","it","expect","introspectCollection","introspectCollections","introspectBlocks","buildBlockNestingMap","buildRelationshipGraph","Media","slug","upload","fields","name","type","required","Categories","Authors","relationTo","Heading","options","defaultValue","RichText","ImageBlock","FullWidth","blocks","HeadingOnly","maxRows","CtaBanner","Accordion","allBlocks","Posts","versions","drafts","hasMany","Pages","tabs","label","schema","toBe","hasDrafts","fieldNames","map","f","toContain","relFieldNames","relationships","r","fieldName","cover","find","toBeDefined","searchableFields","heroSize","length","catalog","slugs","b","toEqual","heading","headingFieldNames","level","pageLayout","e","ownerType","owner","fieldPath","acceptedBlockSlugs","fullWidthContent","headingOnly","accordionPanelBody","Stray","stray","ctaEntries","filter","toHaveLength","schemas","edges","postEdges","fromCollection","postTargets","toCollection","authorEdges"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import type { CollectionConfig, PayloadRequest } from 'payload';
2
- import type { DraftBehavior } from './types';
3
2
  /** MCP response shape used by overrideResponse */
4
3
  interface McpResponse {
5
4
  content: Array<{
@@ -16,42 +15,48 @@ interface McpCollectionConfig {
16
15
  find: boolean;
17
16
  update: boolean;
18
17
  };
19
- overrideResponse?: (response: McpResponse, doc: Record<string, unknown>, req: PayloadRequest) => McpResponse;
18
+ overrideResponse?: (response: McpResponse, doc: Record<string, unknown>, req: PayloadRequest) => McpResponse | Promise<McpResponse>;
20
19
  }
21
20
  interface GenerateOptions {
22
- /** Base site URL for preview links */
23
- siteUrl: string;
24
- /** Preview authentication secret */
25
- previewSecret: string;
26
21
  /**
27
- * Per-collection URL path prefix used when constructing preview URLs.
28
- * Defaults to `/{slug}` for collections not in the map.
22
+ * Optional absolute base URL prepended to relative preview paths returned
23
+ * by the collection's own preview URL function. Resolved upstream from
24
+ * (in order): `options.preview.siteUrl`, `incomingConfig.serverURL`,
25
+ * `process.env.NEXT_PUBLIC_SERVER_URL`, `process.env.SITE_URL`. May be
26
+ * undefined — relative-path returns will then be skipped.
29
27
  */
30
- previewPaths?: Record<string, string>;
28
+ siteUrl?: string;
31
29
  /** Per-collection draft behavior overrides */
32
- draftBehavior?: Record<string, DraftBehavior>;
30
+ draftBehavior?: Record<string, 'always-draft' | 'always-publish'>;
33
31
  /** Collection slugs to exclude from MCP */
34
32
  excludeCollections?: string[];
33
+ /** Disable preview URL injection entirely */
34
+ previewDisabled?: boolean;
35
35
  }
36
36
  /**
37
- * Determines the draft behavior for a collection based on its config and user overrides.
37
+ * Determines the draft behavior for a collection.
38
38
  *
39
- * - If the collection has no `versions.drafts`: always 'publish' regardless of override
40
- * - If the user specified an override: use that override
41
- * - Default: 'always-draft' for draft-enabled collections, 'publish' for others
39
+ * - No drafts configured 'publish' (raw update allowed; no draft concept)
40
+ * - Drafts configured + override given use override
41
+ * - Drafts configured + no override → 'always-draft' (raw update locked)
42
42
  */
43
43
  export declare function getDraftBehavior(collection: CollectionConfig, options?: {
44
- draftBehavior?: Record<string, DraftBehavior>;
44
+ draftBehavior?: Record<string, 'always-draft' | 'always-publish'>;
45
45
  }): 'always-draft' | 'always-publish' | 'publish';
46
46
  /**
47
47
  * Generates the mcpCollections config object for the official mcpPlugin.
48
48
  *
49
- * For each collection:
49
+ * For each non-excluded collection:
50
50
  * - Determines enabled CRUD operations based on draft behavior
51
- * - For 'always-draft' collections: disables raw `update` to force clients through publishDraft tool
52
- * - Generates `overrideResponse` that appends preview URLs for draft documents
51
+ * - For 'always-draft' collections: disables raw `update` to force clients
52
+ * through publishDraft / patchLayout / updateDocument (which preserve
53
+ * draft semantics)
54
+ * - For draft collections: attaches an `overrideResponse` that appends a
55
+ * preview URL — sourced from the collection's own livePreview/preview
56
+ * function — to draft documents. Falls back to a generic admin-panel
57
+ * message when no preview function is configured.
53
58
  *
54
- * @returns A record of collection slug to MCP collection config, plus the set of draft collection slugs
59
+ * @returns Map of slug MCP collection config, plus the set of draft slugs
55
60
  */
56
61
  export declare function generateMcpCollectionConfigs(collections: CollectionConfig[], options: GenerateOptions): {
57
62
  mcpCollections: Record<string, McpCollectionConfig>;