payload-mcp-toolkit 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +150 -150
  2. package/dist/__tests__/introspection.test.js.map +1 -1
  3. package/dist/draft-workflow.js +29 -29
  4. package/dist/draft-workflow.js.map +1 -1
  5. package/dist/index.js +20 -33
  6. package/dist/index.js.map +1 -1
  7. package/dist/introspection.d.ts +4 -0
  8. package/dist/introspection.js +38 -33
  9. package/dist/introspection.js.map +1 -1
  10. package/dist/prompts.js +5 -5
  11. package/dist/prompts.js.map +1 -1
  12. package/dist/resources.js +9 -16
  13. package/dist/resources.js.map +1 -1
  14. package/dist/tools/_helpers.d.ts +14 -0
  15. package/dist/tools/_helpers.js +35 -0
  16. package/dist/tools/_helpers.js.map +1 -0
  17. package/dist/tools/patch-layout.d.ts +3 -9
  18. package/dist/tools/patch-layout.js +29 -48
  19. package/dist/tools/patch-layout.js.map +1 -1
  20. package/dist/tools/publish-draft.d.ts +1 -11
  21. package/dist/tools/publish-draft.js +8 -39
  22. package/dist/tools/publish-draft.js.map +1 -1
  23. package/dist/tools/resolve-reference.d.ts +1 -12
  24. package/dist/tools/resolve-reference.js +45 -85
  25. package/dist/tools/resolve-reference.js.map +1 -1
  26. package/dist/tools/safe-delete.d.ts +8 -13
  27. package/dist/tools/safe-delete.js +68 -100
  28. package/dist/tools/safe-delete.js.map +1 -1
  29. package/dist/tools/schedule-publish.d.ts +11 -21
  30. package/dist/tools/schedule-publish.js +18 -61
  31. package/dist/tools/schedule-publish.js.map +1 -1
  32. package/dist/tools/search-content.d.ts +1 -6
  33. package/dist/tools/search-content.js +52 -64
  34. package/dist/tools/search-content.js.map +1 -1
  35. package/dist/tools/update-document.d.ts +4 -14
  36. package/dist/tools/update-document.js +23 -72
  37. package/dist/tools/update-document.js.map +1 -1
  38. package/dist/tools/upload-media.d.ts +1 -10
  39. package/dist/tools/upload-media.js +11 -54
  40. package/dist/tools/upload-media.js.map +1 -1
  41. package/dist/tools/versions.d.ts +7 -20
  42. package/dist/tools/versions.js +25 -82
  43. package/dist/tools/versions.js.map +1 -1
  44. package/dist/types.js +5 -5
  45. package/dist/types.js.map +1 -1
  46. package/package.json +1 -1
  47. package/dist/rate-limiter.d.ts +0 -25
  48. package/dist/rate-limiter.js +0 -51
  49. package/dist/rate-limiter.js.map +0 -1
package/README.md CHANGED
@@ -1,150 +1,150 @@
1
- # payload-mcp-toolkit
2
-
3
- > Schema-aware MCP toolkit for Payload CMS — wraps the official [`@payloadcms/plugin-mcp`](https://github.com/payloadcms/payload/tree/main/packages/plugin-mcp) with introspected prompts, resources, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.
4
-
5
- ## What it does
6
-
7
- The official Payload MCP plugin gives every collection a generic CRUD surface. That works, but an LLM driving it has no idea:
8
-
9
- - which collections support drafts vs publish-immediately,
10
- - which block types are valid inside which sections,
11
- - which fields are searchable for resolving relationships,
12
- - how to compose a page layout without trial and error.
13
-
14
- `payload-mcp-toolkit` introspects the Payload config at boot, then layers schema-aware **prompts**, **resources**, and **tools** on top of the official plugin so an AI client (Claude Desktop, Claude API, any MCP-compatible chat) can drive your CMS confidently.
15
-
16
- ## What it adds
17
-
18
- **Auto-generated prompts** (no setup required):
19
- - `contentModelOverview` — every collection, fields, and relationships.
20
- - `blockCompositionGuide` — section/leaf hierarchy and nesting rules.
21
- - `draftWorkflowGuide` — which collections need `publishDraft` to go live.
22
-
23
- **Auto-generated resources** (machine-readable JSON for the LLM):
24
- - `blocks://catalog`, `collections://schema`, `collections://relationships`.
25
-
26
- **Custom tools (10, plus an auto-registered scheduler)**
27
-
28
- *Authoring*
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.
30
- - `updateDocument` — Local-API based update that survives the upload-field bug in the official plugin.
31
- - `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe with streaming size cap), create a Media doc.
32
-
33
- *Discovery*
34
- - `resolveReference` — search collections by name/title/slug for relationship IDs.
35
- - `searchContent` — natural-language editor triage. Filter by `status`, `olderThanDays` / `newerThanDays`, `missingFields`, free-text `query`, scoped to one collection or all.
36
-
37
- *Lifecycle / safety*
38
- - `publishDraft` — flip `_status` from draft to published.
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`.
43
-
44
- **Draft workflow** wired into the official plugin's `mcpCollections`:
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.
47
-
48
- ## Install
49
-
50
- ```bash
51
- pnpm add payload-mcp-toolkit @payloadcms/plugin-mcp
52
- ```
53
-
54
- Peer dependencies: `payload` ^3, `@payloadcms/plugin-mcp` ^3, `zod` ^3.
55
-
56
- ## Use — zero config
57
-
58
- ```ts
59
- // payload.config.ts
60
- import { contentToolkitPlugin } from 'payload-mcp-toolkit'
61
-
62
- export default buildConfig({
63
- // ...your collections, blocks, globals
64
- serverURL: process.env.SITE_URL, // used for absolute preview URLs
65
- admin: { user: 'users' }, // your auth collection
66
- plugins: [contentToolkitPlugin()],
67
- })
68
- ```
69
-
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
77
-
78
- Every option is an escape hatch — pass only what you need:
79
-
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
- ```
108
-
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'`. |
119
-
120
- ## Development
121
-
122
- This package follows the [official Payload 3 plugin template](https://github.com/payloadcms/payload/tree/main/templates/plugin) layout: source in `src/`, a fully-working Payload + Next.js app in `dev/`, source-export `package.json` so the dev harness consumes the plugin directly without a build step.
123
-
124
- ```bash
125
- pnpm install
126
- cp dev/.env.example dev/.env
127
- pnpm dev # boot dev/ Next.js + Payload at http://localhost:3000
128
- pnpm test # vitest — runs introspection unit tests
129
- pnpm build # produce dist/ for npm publish
130
- ```
131
-
132
- The dev harness ships with a realistic CMS schema:
133
- - `Pages` — block-based layout (FullWidth, TwoColumn, CtaBanner, HeadingOnly), drafts enabled.
134
- - `Posts` — title/slug/excerpt/content/cover/category/authors/tags/SEO, drafts enabled.
135
- - `Authors`, `Categories`, `Media`, `Users` — taxonomy + auth.
136
- - `SiteSettings` — global with site name, logo, social, footer.
137
- - 5 leaf blocks (Heading, RichText, Image, ButtonGroup, Quote) and 4 section blocks.
138
-
139
- Seed sample content:
140
-
141
- ```bash
142
- # Generate the admin import map first time:
143
- pnpm dev:generate-importmap
144
-
145
- # Then visit http://localhost:3000/admin and create your first user.
146
- ```
147
-
148
- ## License
149
-
150
- MIT
1
+ # payload-mcp-toolkit
2
+
3
+ > Schema-aware MCP toolkit for Payload CMS — wraps the official [`@payloadcms/plugin-mcp`](https://github.com/payloadcms/payload/tree/main/packages/plugin-mcp) with introspected prompts, resources, draft workflow, and AI-friendly tools so non-technical editors can manage content via AI chat.
4
+
5
+ ## What it does
6
+
7
+ The official Payload MCP plugin gives every collection a generic CRUD surface. That works, but an LLM driving it has no idea:
8
+
9
+ - which collections support drafts vs publish-immediately,
10
+ - which block types are valid inside which sections,
11
+ - which fields are searchable for resolving relationships,
12
+ - how to compose a page layout without trial and error.
13
+
14
+ `payload-mcp-toolkit` introspects the Payload config at boot, then layers schema-aware **prompts**, **resources**, and **tools** on top of the official plugin so an AI client (Claude Desktop, Claude API, any MCP-compatible chat) can drive your CMS confidently.
15
+
16
+ ## What it adds
17
+
18
+ **Auto-generated prompts** (no setup required):
19
+ - `contentModelOverview` — every collection, fields, and relationships.
20
+ - `blockCompositionGuide` — section/leaf hierarchy and nesting rules.
21
+ - `draftWorkflowGuide` — which collections need `publishDraft` to go live.
22
+
23
+ **Auto-generated resources** (machine-readable JSON for the LLM):
24
+ - `blocks://catalog`, `collections://schema`, `collections://relationships`.
25
+
26
+ **Custom tools (10, plus an auto-registered scheduler)**
27
+
28
+ *Authoring*
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.
30
+ - `updateDocument` — Local-API based update that survives the upload-field bug in the official plugin.
31
+ - `uploadMedia` — fetch a public HTTPS image, validate (SSRF-safe with streaming size cap), create a Media doc.
32
+
33
+ *Discovery*
34
+ - `resolveReference` — search collections by name/title/slug for relationship IDs.
35
+ - `searchContent` — natural-language editor triage. Filter by `status`, `olderThanDays` / `newerThanDays`, `missingFields`, free-text `query`, scoped to one collection or all.
36
+
37
+ *Lifecycle / safety*
38
+ - `publishDraft` — flip `_status` from draft to published.
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`.
43
+
44
+ **Draft workflow** wired into the official plugin's `mcpCollections`:
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.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pnpm add payload-mcp-toolkit @payloadcms/plugin-mcp
52
+ ```
53
+
54
+ Peer dependencies: `payload` ^3, `@payloadcms/plugin-mcp` ^3, `zod` ^3.
55
+
56
+ ## Use — zero config
57
+
58
+ ```ts
59
+ // payload.config.ts
60
+ import { contentToolkitPlugin } from 'payload-mcp-toolkit'
61
+
62
+ export default buildConfig({
63
+ // ...your collections, blocks, globals
64
+ serverURL: process.env.SITE_URL, // used for absolute preview URLs
65
+ admin: { user: 'users' }, // your auth collection
66
+ plugins: [contentToolkitPlugin()],
67
+ })
68
+ ```
69
+
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
77
+
78
+ Every option is an escape hatch — pass only what you need:
79
+
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
+ ```
108
+
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'`. |
119
+
120
+ ## Development
121
+
122
+ This package follows the [official Payload 3 plugin template](https://github.com/payloadcms/payload/tree/main/templates/plugin) layout: source in `src/`, a fully-working Payload + Next.js app in `dev/`, source-export `package.json` so the dev harness consumes the plugin directly without a build step.
123
+
124
+ ```bash
125
+ pnpm install
126
+ cp dev/.env.example dev/.env
127
+ pnpm dev # boot dev/ Next.js + Payload at http://localhost:3000
128
+ pnpm test # vitest — runs introspection unit tests
129
+ pnpm build # produce dist/ for npm publish
130
+ ```
131
+
132
+ The dev harness ships with a realistic CMS schema:
133
+ - `Pages` — block-based layout (FullWidth, TwoColumn, CtaBanner, HeadingOnly), drafts enabled.
134
+ - `Posts` — title/slug/excerpt/content/cover/category/authors/tags/SEO, drafts enabled.
135
+ - `Authors`, `Categories`, `Media`, `Users` — taxonomy + auth.
136
+ - `SiteSettings` — global with site name, logo, social, footer.
137
+ - 5 leaf blocks (Heading, RichText, Image, ButtonGroup, Quote) and 4 section blocks.
138
+
139
+ Seed sample content:
140
+
141
+ ```bash
142
+ # Generate the admin import map first time:
143
+ pnpm dev:generate-importmap
144
+
145
+ # Then visit http://localhost:3000/admin and create your first user.
146
+ ```
147
+
148
+ ## License
149
+
150
+ MIT
@@ -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 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
+ {"version":3,"sources":["../../src/__tests__/introspection.test.ts"],"sourcesContent":["import { describe, it, expect } from 'vitest'\r\nimport type { Block, CollectionConfig } from 'payload'\r\nimport {\r\n introspectCollection,\r\n introspectCollections,\r\n introspectBlocks,\r\n buildBlockNestingMap,\r\n buildRelationshipGraph,\r\n} from '../introspection'\r\n\r\n// ─── Sample schema (kept inline so the test is self-contained) ─────\r\n\r\nconst Media: CollectionConfig = {\r\n slug: 'media',\r\n upload: true,\r\n fields: [{ name: 'alt', type: 'text', required: true }],\r\n}\r\n\r\nconst Categories: CollectionConfig = {\r\n slug: 'categories',\r\n fields: [\r\n { name: 'name', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n ],\r\n}\r\n\r\nconst Authors: CollectionConfig = {\r\n slug: 'authors',\r\n fields: [\r\n { name: 'name', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'avatar', type: 'upload', relationTo: 'media' },\r\n ],\r\n}\r\n\r\n// Leaf-style blocks\r\nconst Heading: Block = {\r\n slug: 'heading',\r\n fields: [\r\n { name: 'text', type: 'text', required: true },\r\n {\r\n name: 'level',\r\n type: 'select',\r\n options: ['h1', 'h2', 'h3'],\r\n defaultValue: 'h2',\r\n },\r\n {\r\n name: 'align',\r\n type: 'select',\r\n options: ['left', 'center', 'right'],\r\n defaultValue: 'left',\r\n },\r\n ],\r\n}\r\n\r\nconst RichText: Block = {\r\n slug: 'richText',\r\n fields: [{ name: 'content', type: 'richText' }],\r\n}\r\n\r\nconst ImageBlock: Block = {\r\n slug: 'image',\r\n fields: [\r\n { name: 'image', type: 'upload', relationTo: 'media', required: true },\r\n { name: 'caption', type: 'text' },\r\n ],\r\n}\r\n\r\n// Container-style blocks (have nested blocks fields)\r\nconst FullWidth: Block = {\r\n slug: 'fullWidth',\r\n fields: [\r\n {\r\n name: 'content',\r\n type: 'blocks',\r\n blocks: [Heading, RichText, ImageBlock],\r\n },\r\n ],\r\n}\r\n\r\nconst HeadingOnly: Block = {\r\n slug: 'headingOnly',\r\n fields: [\r\n {\r\n name: 'content',\r\n type: 'blocks',\r\n maxRows: 1,\r\n blocks: [Heading],\r\n },\r\n ],\r\n}\r\n\r\nconst CtaBanner: Block = {\r\n slug: 'ctaBanner',\r\n fields: [\r\n { name: 'headline', type: 'text', required: true },\r\n { name: 'buttonLabel', type: 'text' },\r\n { name: 'buttonHref', type: 'text' },\r\n ],\r\n}\r\n\r\n// Deeply-nestable container — exercises the recursive path\r\nconst Accordion: Block = {\r\n slug: 'accordion',\r\n fields: [\r\n {\r\n name: 'panels',\r\n type: 'array',\r\n fields: [\r\n { name: 'title', type: 'text' },\r\n {\r\n name: 'body',\r\n type: 'blocks',\r\n blocks: [Heading, RichText, FullWidth],\r\n },\r\n ],\r\n },\r\n ],\r\n}\r\n\r\nconst allBlocks: Block[] = [Heading, RichText, ImageBlock, FullWidth, HeadingOnly, CtaBanner, Accordion]\r\n\r\nconst Posts: CollectionConfig = {\r\n slug: 'posts',\r\n versions: { drafts: true },\r\n fields: [\r\n { name: 'title', type: 'text', required: true },\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'featured', type: 'checkbox' },\r\n { name: 'category', type: 'relationship', relationTo: 'categories' },\r\n {\r\n name: 'authors',\r\n type: 'relationship',\r\n relationTo: 'authors',\r\n hasMany: true,\r\n },\r\n { name: 'coverImage', type: 'upload', relationTo: 'media' },\r\n {\r\n name: 'tags',\r\n type: 'array',\r\n fields: [{ name: 'tag', type: 'text' }],\r\n },\r\n ],\r\n}\r\n\r\nconst Pages: CollectionConfig = {\r\n slug: 'pages',\r\n versions: { drafts: true },\r\n fields: [\r\n {\r\n type: 'tabs',\r\n tabs: [\r\n {\r\n name: 'hero',\r\n label: 'Hero',\r\n fields: [\r\n { name: 'heroTitle', type: 'text' },\r\n {\r\n name: 'heroSize',\r\n type: 'select',\r\n options: ['small', 'medium', 'large'],\r\n defaultValue: 'medium',\r\n },\r\n ],\r\n },\r\n {\r\n label: 'Content',\r\n fields: [\r\n { name: 'slug', type: 'text', required: true },\r\n { name: 'layout', type: 'blocks', blocks: [FullWidth, HeadingOnly, CtaBanner, Accordion] },\r\n ],\r\n },\r\n ],\r\n },\r\n ],\r\n}\r\n\r\n// ─── introspectCollection ──────────────────────────────────────────\r\n\r\ndescribe('introspectCollection', () => {\r\n it('extracts Posts collection fields, relationships, and draft status', () => {\r\n const schema = introspectCollection(Posts)\r\n\r\n expect(schema.slug).toBe('posts')\r\n expect(schema.hasDrafts).toBe(true)\r\n\r\n const fieldNames = schema.fields.map((f) => f.name)\r\n expect(fieldNames).toContain('title')\r\n expect(fieldNames).toContain('slug')\r\n expect(fieldNames).toContain('featured')\r\n expect(fieldNames).toContain('tags')\r\n\r\n const relFieldNames = schema.relationships.map((r) => r.fieldName)\r\n expect(relFieldNames).toContain('category')\r\n expect(relFieldNames).toContain('authors')\r\n\r\n const cover = schema.relationships.find((r) => r.fieldName === 'coverImage')\r\n expect(cover).toBeDefined()\r\n expect(cover!.relationTo).toBe('media')\r\n\r\n expect(schema.searchableFields).toContain('title')\r\n expect(schema.searchableFields).toContain('slug')\r\n })\r\n\r\n it('extracts Pages collection with tab-nested fields', () => {\r\n const schema = introspectCollection(Pages)\r\n\r\n expect(schema.slug).toBe('pages')\r\n expect(schema.hasDrafts).toBe(true)\r\n\r\n const fieldNames = schema.fields.map((f) => f.name)\r\n expect(fieldNames).toContain('heroTitle')\r\n expect(fieldNames).toContain('slug')\r\n expect(fieldNames).toContain('layout')\r\n })\r\n\r\n it('detects collections without draft support', () => {\r\n const schema = introspectCollection(Categories)\r\n expect(schema.hasDrafts).toBe(false)\r\n })\r\n\r\n it('extracts select field options from Pages heroSize', () => {\r\n const schema = introspectCollection(Pages)\r\n const heroSize = schema.fields.find((f) => f.name === 'heroSize')\r\n expect(heroSize).toBeDefined()\r\n expect(heroSize!.type).toBe('select')\r\n expect(heroSize!.options).toBeDefined()\r\n expect(heroSize!.options!.length).toBe(3)\r\n })\r\n})\r\n\r\n// ─── introspectBlocks (flat catalog) ───────────────────────────────\r\n\r\ndescribe('introspectBlocks', () => {\r\n it('returns a flat catalog of every block with no section/leaf split', () => {\r\n const catalog = introspectBlocks(allBlocks)\r\n const slugs = catalog.blocks.map((b) => b.slug)\r\n expect(slugs).toEqual([\r\n 'heading',\r\n 'richText',\r\n 'image',\r\n 'fullWidth',\r\n 'headingOnly',\r\n 'ctaBanner',\r\n 'accordion',\r\n ])\r\n })\r\n\r\n it('extracts each block\\'s fields including select options', () => {\r\n const catalog = introspectBlocks(allBlocks)\r\n const heading = catalog.blocks.find((b) => b.slug === 'heading')\r\n expect(heading).toBeDefined()\r\n const headingFieldNames = heading!.fields.map((f) => f.name)\r\n expect(headingFieldNames).toEqual(['text', 'level', 'align'])\r\n const level = heading!.fields.find((f) => f.name === 'level')\r\n expect(level!.options).toBeDefined()\r\n })\r\n})\r\n\r\n// ─── buildBlockNestingMap ──────────────────────────────────────────\r\n\r\ndescribe('buildBlockNestingMap', () => {\r\n it('records the layout field on Pages with the slugs it accepts', () => {\r\n const map = buildBlockNestingMap([Pages, Posts], allBlocks)\r\n const pageLayout = map.find(\r\n (e) => e.ownerType === 'collection' && e.owner === 'pages' && e.fieldPath === 'layout',\r\n )\r\n expect(pageLayout).toBeDefined()\r\n expect(pageLayout!.acceptedBlockSlugs).toEqual(['fullWidth', 'headingOnly', 'ctaBanner', 'accordion'])\r\n })\r\n\r\n it('records nested blocks fields inside container blocks', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n\r\n const fullWidthContent = map.find(\r\n (e) => e.ownerType === 'block' && e.owner === 'fullWidth' && e.fieldPath === 'content',\r\n )\r\n expect(fullWidthContent).toBeDefined()\r\n expect(fullWidthContent!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'image'])\r\n\r\n const headingOnly = map.find(\r\n (e) => e.ownerType === 'block' && e.owner === 'headingOnly' && e.fieldPath === 'content',\r\n )\r\n expect(headingOnly!.acceptedBlockSlugs).toEqual(['heading'])\r\n expect(headingOnly!.maxRows).toBe(1)\r\n })\r\n\r\n it('handles arbitrarily-deep nesting via array fields inside blocks', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n\r\n const accordionPanelBody = map.find(\r\n (e) =>\r\n e.ownerType === 'block' && e.owner === 'accordion' && e.fieldPath === 'panels[].body',\r\n )\r\n expect(accordionPanelBody).toBeDefined()\r\n expect(accordionPanelBody!.acceptedBlockSlugs).toEqual(['heading', 'richText', 'fullWidth'])\r\n })\r\n\r\n it('omits unknown slugs not present in the block list', () => {\r\n const Stray: CollectionConfig = {\r\n slug: 'stray',\r\n fields: [\r\n {\r\n name: 'layout',\r\n type: 'blocks',\r\n blocks: [Heading, { slug: 'mystery', fields: [] } as Block],\r\n },\r\n ],\r\n }\r\n const map = buildBlockNestingMap([Stray], [Heading]) // mystery not in catalog\r\n const stray = map.find((e) => e.owner === 'stray' && e.fieldPath === 'layout')\r\n expect(stray!.acceptedBlockSlugs).toEqual(['heading'])\r\n })\r\n\r\n it('omits fixed blocks (no nested blocks fields) from the map', () => {\r\n const map = buildBlockNestingMap([Pages], allBlocks)\r\n const ctaEntries = map.filter((e) => e.owner === 'ctaBanner')\r\n expect(ctaEntries).toHaveLength(0)\r\n })\r\n})\r\n\r\n// ─── buildRelationshipGraph ────────────────────────────────────────\r\n\r\ndescribe('buildRelationshipGraph', () => {\r\n it('builds correct graph from sample collections', () => {\r\n const schemas = introspectCollections([Posts, Pages, Categories, Authors, Media])\r\n const edges = buildRelationshipGraph(schemas)\r\n\r\n const postEdges = edges.filter((e) => e.fromCollection === 'posts')\r\n const postTargets = postEdges.map((e) => e.toCollection)\r\n expect(postTargets).toContain('categories')\r\n expect(postTargets).toContain('authors')\r\n expect(postTargets).toContain('media')\r\n\r\n const authorEdges = edges.filter((e) => e.fromCollection === 'authors')\r\n expect(authorEdges.map((e) => e.toCollection)).toContain('media')\r\n })\r\n})\r\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,23 +1,23 @@
1
- /**
2
- * Determines the draft behavior for a collection.
3
- *
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)
1
+ import { hasCollectionDrafts } from './introspection';
2
+ /**
3
+ * Determines the draft behavior for a collection.
4
+ *
5
+ * - No drafts configured 'publish' (raw update allowed; no draft concept)
6
+ * - Drafts configured + override given use override
7
+ * - Drafts configured + no override → 'always-draft' (raw update locked)
7
8
  */ export function getDraftBehavior(collection, options) {
8
- const hasDrafts = typeof collection.versions === 'object' && collection.versions !== null && 'drafts' in collection.versions && Boolean(collection.versions.drafts);
9
- if (!hasDrafts) return 'publish';
9
+ if (!hasCollectionDrafts(collection)) return 'publish';
10
10
  const override = options?.draftBehavior?.[collection.slug];
11
11
  if (override) return override;
12
12
  return 'always-draft';
13
13
  }
14
- /**
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`).
18
- *
19
- * If neither is configured, or the function returns null/undefined/empty,
20
- * returns null and the override response will skip preview injection.
14
+ /**
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`).
18
+ *
19
+ * If neither is configured, or the function returns null/undefined/empty,
20
+ * returns null and the override response will skip preview injection.
21
21
  */ async function resolvePreviewUrl(collection, doc, req, siteUrl) {
22
22
  const admin = collection.admin ?? {};
23
23
  const locale = req.locale ?? 'en';
@@ -87,20 +87,20 @@ function createOverrideResponse(collection, siteUrl) {
87
87
  };
88
88
  };
89
89
  }
90
- /**
91
- * Generates the mcpCollections config object for the official mcpPlugin.
92
- *
93
- * For each non-excluded collection:
94
- * - Determines enabled CRUD operations based on draft behavior
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.
102
- *
103
- * @returns Map of slug → MCP collection config, plus the set of draft slugs
90
+ /**
91
+ * Generates the mcpCollections config object for the official mcpPlugin.
92
+ *
93
+ * For each non-excluded collection:
94
+ * - Determines enabled CRUD operations based on draft behavior
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.
102
+ *
103
+ * @returns Map of slug → MCP collection config, plus the set of draft slugs
104
104
  */ export function generateMcpCollectionConfigs(collections, options) {
105
105
  const mcpCollections = {};
106
106
  const draftCollections = new Set();
@@ -1 +1 @@
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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"}
1
+ {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\r\nimport { hasCollectionDrafts } from './introspection'\r\n\r\n/** MCP response shape used by overrideResponse */\r\ninterface McpResponse {\r\n content: Array<{ text: string; type: string }>\r\n}\r\n\r\n/** Per-collection MCP config with enabled operations and optional overrideResponse */\r\ninterface McpCollectionConfig {\r\n description: string\r\n enabled: {\r\n create: boolean\r\n delete: boolean\r\n find: boolean\r\n update: boolean\r\n }\r\n overrideResponse?: (\r\n response: McpResponse,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n ) => McpResponse | Promise<McpResponse>\r\n}\r\n\r\ninterface GenerateOptions {\r\n /**\r\n * Optional absolute base URL prepended to relative preview paths returned\r\n * by the collection's own preview URL function. Resolved upstream from\r\n * (in order): `options.preview.siteUrl`, `incomingConfig.serverURL`,\r\n * `process.env.NEXT_PUBLIC_SERVER_URL`, `process.env.SITE_URL`. May be\r\n * undefined — relative-path returns will then be skipped.\r\n */\r\n siteUrl?: string\r\n /** Per-collection draft behavior overrides */\r\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\r\n /** Collection slugs to exclude from MCP */\r\n excludeCollections?: string[]\r\n /** Disable preview URL injection entirely */\r\n previewDisabled?: boolean\r\n}\r\n\r\n/**\r\n * Determines the draft behavior for a collection.\r\n *\r\n * - No drafts configured → 'publish' (raw update allowed; no draft concept)\r\n * - Drafts configured + override given → use override\r\n * - Drafts configured + no override → 'always-draft' (raw update locked)\r\n */\r\nexport function getDraftBehavior(\r\n collection: CollectionConfig,\r\n options?: { draftBehavior?: Record<string, 'always-draft' | 'always-publish'> },\r\n): 'always-draft' | 'always-publish' | 'publish' {\r\n if (!hasCollectionDrafts(collection)) return 'publish'\r\n\r\n const override = options?.draftBehavior?.[collection.slug]\r\n if (override) return override\r\n\r\n return 'always-draft'\r\n}\r\n\r\n/**\r\n * Build a preview URL for a draft document by delegating to the collection's\r\n * own configured preview URL function. Tries `admin.livePreview.url` first\r\n * (the modern API), then `admin.preview` (the older `GeneratePreviewURL`).\r\n *\r\n * If neither is configured, or the function returns null/undefined/empty,\r\n * returns null and the override response will skip preview injection.\r\n */\r\nasync function resolvePreviewUrl(\r\n collection: CollectionConfig,\r\n doc: Record<string, unknown>,\r\n req: PayloadRequest,\r\n siteUrl: string | undefined,\r\n): Promise<string | null> {\r\n const admin = (collection.admin ?? {}) as Record<string, any>\r\n const locale = (req as any).locale ?? 'en'\r\n\r\n let raw: string | null | undefined\r\n\r\n const livePreviewUrl = admin.livePreview?.url\r\n if (typeof livePreviewUrl === 'function') {\r\n try {\r\n raw = await livePreviewUrl({\r\n data: doc,\r\n locale: { code: locale, label: locale },\r\n req,\r\n payload: req.payload,\r\n collectionConfig: collection as any,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n } else if (typeof livePreviewUrl === 'string') {\r\n raw = livePreviewUrl\r\n }\r\n\r\n if (!raw && typeof admin.preview === 'function') {\r\n try {\r\n raw = await admin.preview(doc, {\r\n locale,\r\n req,\r\n token: null,\r\n })\r\n } catch {\r\n raw = null\r\n }\r\n }\r\n\r\n if (!raw || typeof raw !== 'string') return null\r\n\r\n if (raw.startsWith('http://') || raw.startsWith('https://')) {\r\n return raw\r\n }\r\n\r\n if (!siteUrl) return null\r\n\r\n const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl\r\n const path = raw.startsWith('/') ? raw : `/${raw}`\r\n return `${base}${path}`\r\n}\r\n\r\nfunction createOverrideResponse(\r\n collection: CollectionConfig,\r\n siteUrl: string | undefined,\r\n): McpCollectionConfig['overrideResponse'] {\r\n return async (response, doc, req): Promise<McpResponse> => {\r\n if (doc._status !== 'draft') return response\r\n\r\n const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl)\r\n if (!previewUrl) {\r\n return {\r\n content: [\r\n ...response.content,\r\n {\r\n type: 'text',\r\n text: '\\n📋 This document is a draft. Use the admin panel to preview it.',\r\n },\r\n ],\r\n }\r\n }\r\n\r\n return {\r\n content: [\r\n ...response.content,\r\n {\r\n type: 'text',\r\n text: `\\n📋 This document is a draft. Preview it here: ${previewUrl}`,\r\n },\r\n ],\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Generates the mcpCollections config object for the official mcpPlugin.\r\n *\r\n * For each non-excluded collection:\r\n * - Determines enabled CRUD operations based on draft behavior\r\n * - For 'always-draft' collections: disables raw `update` to force clients\r\n * through publishDraft / patchLayout / updateDocument (which preserve\r\n * draft semantics)\r\n * - For draft collections: attaches an `overrideResponse` that appends a\r\n * preview URL — sourced from the collection's own livePreview/preview\r\n * function — to draft documents. Falls back to a generic admin-panel\r\n * message when no preview function is configured.\r\n *\r\n * @returns Map of slug → MCP collection config, plus the set of draft slugs\r\n */\r\nexport function generateMcpCollectionConfigs(\r\n collections: CollectionConfig[],\r\n options: GenerateOptions,\r\n): {\r\n mcpCollections: Record<string, McpCollectionConfig>\r\n draftCollections: Set<string>\r\n} {\r\n const mcpCollections: Record<string, McpCollectionConfig> = {}\r\n const draftCollections = new Set<string>()\r\n\r\n const excludeSlugs = new Set([\r\n 'payload-mcp-api-keys',\r\n ...(options.excludeCollections ?? []),\r\n ])\r\n\r\n for (const collection of collections) {\r\n if (excludeSlugs.has(collection.slug)) continue\r\n\r\n // Auth-enabled collections are users — never expose them via MCP.\r\n if ((collection as any).auth) continue\r\n\r\n const behavior = getDraftBehavior(collection, options)\r\n\r\n if (behavior !== 'publish') {\r\n draftCollections.add(collection.slug)\r\n }\r\n\r\n const enabled = {\r\n find: true,\r\n create: true,\r\n update: behavior !== 'always-draft',\r\n delete: true,\r\n }\r\n\r\n const config: McpCollectionConfig = {\r\n description: `Manage ${collection.slug} content`,\r\n enabled,\r\n }\r\n\r\n if (draftCollections.has(collection.slug) && !options.previewDisabled) {\r\n config.overrideResponse = createOverrideResponse(collection, options.siteUrl)\r\n }\r\n\r\n mcpCollections[collection.slug] = config\r\n }\r\n\r\n return { mcpCollections, draftCollections }\r\n}\r\n"],"names":["hasCollectionDrafts","getDraftBehavior","collection","options","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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"}