payload-mcp-toolkit 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -42
- package/dist/__tests__/introspection.test.js +141 -46
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/draft-workflow.d.ts +24 -19
- package/dist/draft-workflow.js +78 -31
- package/dist/draft-workflow.js.map +1 -1
- package/dist/index.d.ts +10 -15
- package/dist/index.js +43 -70
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +14 -7
- package/dist/introspection.js +92 -68
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.d.ts +4 -4
- package/dist/prompts.js +44 -35
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts +9 -4
- package/dist/resources.js +44 -52
- package/dist/resources.js.map +1 -1
- package/dist/tools/patch-layout.d.ts +15 -79
- package/dist/tools/patch-layout.js +140 -48
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/types.d.ts +82 -53
- package/dist/types.js +6 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/compose-helpers.d.ts +0 -117
- package/dist/tools/compose-helpers.js +0 -236
- package/dist/tools/compose-helpers.js.map +0 -1
- package/dist/tools/compose-layout.d.ts +0 -139
- package/dist/tools/compose-layout.js +0 -61
- package/dist/tools/compose-layout.js.map +0 -1
package/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
|
|
26
|
+
**Custom tools (10, plus an auto-registered scheduler)**
|
|
27
27
|
|
|
28
28
|
*Authoring*
|
|
29
|
-
- `
|
|
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.
|
|
41
|
-
- `listVersions` — recent saved versions of a draft document
|
|
42
|
-
- `restoreVersion` — roll a document back to a saved version (creates a new version on top, so
|
|
43
|
-
- `safeDelete` — relationship-aware delete. Walks the
|
|
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
|
-
-
|
|
47
|
-
- Appends preview URLs to draft responses
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
78
|
+
Every option is an escape hatch — pass only what you need:
|
|
88
79
|
|
|
89
|
-
|
|
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 |
|
|
92
|
-
|
|
93
|
-
| `siteUrl` |
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `mediaUpload.
|
|
100
|
-
| `
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
// ───
|
|
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('
|
|
297
|
-
const catalog = introspectBlocks(
|
|
298
|
-
const
|
|
299
|
-
expect(
|
|
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
|
|
331
|
-
const catalog = introspectBlocks(
|
|
332
|
-
const heading = catalog.
|
|
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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"}
|
package/dist/draft-workflow.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
28
|
+
siteUrl?: string;
|
|
31
29
|
/** Per-collection draft behavior overrides */
|
|
32
|
-
draftBehavior?: Record<string,
|
|
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
|
|
37
|
+
* Determines the draft behavior for a collection.
|
|
38
38
|
*
|
|
39
|
-
* -
|
|
40
|
-
* -
|
|
41
|
-
* -
|
|
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,
|
|
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
|
|
52
|
-
*
|
|
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
|
|
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>;
|