payload-mcp-toolkit 0.3.4 → 0.7.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.
Files changed (144) hide show
  1. package/README.md +232 -151
  2. package/dist/__tests__/api-keys.test.js +292 -0
  3. package/dist/__tests__/api-keys.test.js.map +1 -0
  4. package/dist/__tests__/auth-strategy.test.js +681 -0
  5. package/dist/__tests__/auth-strategy.test.js.map +1 -0
  6. package/dist/__tests__/conflict-detection.test.js +69 -0
  7. package/dist/__tests__/conflict-detection.test.js.map +1 -0
  8. package/dist/__tests__/delete-document.test.js +70 -0
  9. package/dist/__tests__/delete-document.test.js.map +1 -0
  10. package/dist/__tests__/endpoint.test.js +143 -0
  11. package/dist/__tests__/endpoint.test.js.map +1 -0
  12. package/dist/__tests__/find-document.test.js +178 -0
  13. package/dist/__tests__/find-document.test.js.map +1 -0
  14. package/dist/__tests__/find-global.test.js +173 -0
  15. package/dist/__tests__/find-global.test.js.map +1 -0
  16. package/dist/__tests__/global-versions.test.js +183 -0
  17. package/dist/__tests__/global-versions.test.js.map +1 -0
  18. package/dist/__tests__/hash.test.js +58 -0
  19. package/dist/__tests__/hash.test.js.map +1 -0
  20. package/dist/__tests__/index-integration.test.js +191 -0
  21. package/dist/__tests__/index-integration.test.js.map +1 -0
  22. package/dist/__tests__/introspection.test.js +201 -1
  23. package/dist/__tests__/introspection.test.js.map +1 -1
  24. package/dist/__tests__/patch-global-layout.test.js +474 -0
  25. package/dist/__tests__/patch-global-layout.test.js.map +1 -0
  26. package/dist/__tests__/patch-layout.test.js +171 -0
  27. package/dist/__tests__/patch-layout.test.js.map +1 -0
  28. package/dist/__tests__/registry.test.js +795 -0
  29. package/dist/__tests__/registry.test.js.map +1 -0
  30. package/dist/__tests__/resources.test.js +139 -0
  31. package/dist/__tests__/resources.test.js.map +1 -0
  32. package/dist/__tests__/update-global.test.js +157 -0
  33. package/dist/__tests__/update-global.test.js.map +1 -0
  34. package/dist/api-keys.d.ts +46 -0
  35. package/dist/api-keys.js +272 -0
  36. package/dist/api-keys.js.map +1 -0
  37. package/dist/auth-strategy.d.ts +85 -0
  38. package/dist/auth-strategy.js +219 -0
  39. package/dist/auth-strategy.js.map +1 -0
  40. package/dist/components/CollectionScopesMatrix.d.ts +8 -0
  41. package/dist/components/CollectionScopesMatrix.js +32 -0
  42. package/dist/components/CollectionScopesMatrix.js.map +1 -0
  43. package/dist/components/GlobalScopesMatrix.d.ts +8 -0
  44. package/dist/components/GlobalScopesMatrix.js +28 -0
  45. package/dist/components/GlobalScopesMatrix.js.map +1 -0
  46. package/dist/components/ScopesTable.d.ts +19 -0
  47. package/dist/components/ScopesTable.js +285 -0
  48. package/dist/components/ScopesTable.js.map +1 -0
  49. package/dist/components/index.d.ts +2 -0
  50. package/dist/components/index.js +4 -0
  51. package/dist/components/index.js.map +1 -0
  52. package/dist/conflict-detection.d.ts +13 -0
  53. package/dist/conflict-detection.js +41 -0
  54. package/dist/conflict-detection.js.map +1 -0
  55. package/dist/draft-workflow.d.ts +46 -48
  56. package/dist/draft-workflow.js +53 -135
  57. package/dist/draft-workflow.js.map +1 -1
  58. package/dist/endpoint.d.ts +35 -0
  59. package/dist/endpoint.js +105 -0
  60. package/dist/endpoint.js.map +1 -0
  61. package/dist/hash.d.ts +21 -0
  62. package/dist/hash.js +36 -0
  63. package/dist/hash.js.map +1 -0
  64. package/dist/index.d.ts +9 -9
  65. package/dist/index.js +167 -69
  66. package/dist/index.js.map +1 -1
  67. package/dist/introspection.d.ts +17 -3
  68. package/dist/introspection.js +95 -36
  69. package/dist/introspection.js.map +1 -1
  70. package/dist/prompts.js +5 -5
  71. package/dist/prompts.js.map +1 -1
  72. package/dist/registry.d.ts +50 -0
  73. package/dist/registry.js +169 -0
  74. package/dist/registry.js.map +1 -0
  75. package/dist/resources.d.ts +5 -3
  76. package/dist/resources.js +23 -11
  77. package/dist/resources.js.map +1 -1
  78. package/dist/scope/audit-log.d.ts +18 -0
  79. package/dist/scope/audit-log.js +50 -0
  80. package/dist/scope/audit-log.js.map +1 -0
  81. package/dist/scope/policy.d.ts +73 -0
  82. package/dist/scope/policy.js +218 -0
  83. package/dist/scope/policy.js.map +1 -0
  84. package/dist/tools/_helpers.d.ts +28 -1
  85. package/dist/tools/_helpers.js +83 -0
  86. package/dist/tools/_helpers.js.map +1 -1
  87. package/dist/tools/_layout-helpers.d.ts +43 -0
  88. package/dist/tools/_layout-helpers.js +159 -0
  89. package/dist/tools/_layout-helpers.js.map +1 -0
  90. package/dist/tools/create-document.d.ts +5 -5
  91. package/dist/tools/create-document.js +25 -21
  92. package/dist/tools/create-document.js.map +1 -1
  93. package/dist/tools/delete-document.d.ts +25 -0
  94. package/dist/tools/delete-document.js +49 -0
  95. package/dist/tools/delete-document.js.map +1 -0
  96. package/dist/tools/find-document.d.ts +33 -0
  97. package/dist/tools/find-document.js +97 -0
  98. package/dist/tools/find-document.js.map +1 -0
  99. package/dist/tools/find-global.d.ts +26 -0
  100. package/dist/tools/find-global.js +122 -0
  101. package/dist/tools/find-global.js.map +1 -0
  102. package/dist/tools/global-versions.d.ts +39 -0
  103. package/dist/tools/global-versions.js +132 -0
  104. package/dist/tools/global-versions.js.map +1 -0
  105. package/dist/tools/patch-global-layout.d.ts +31 -0
  106. package/dist/tools/patch-global-layout.js +127 -0
  107. package/dist/tools/patch-global-layout.js.map +1 -0
  108. package/dist/tools/patch-layout.d.ts +5 -8
  109. package/dist/tools/patch-layout.js +18 -100
  110. package/dist/tools/patch-layout.js.map +1 -1
  111. package/dist/tools/publish-draft.d.ts +5 -4
  112. package/dist/tools/publish-draft.js +6 -1
  113. package/dist/tools/publish-draft.js.map +1 -1
  114. package/dist/tools/publish-global-draft.d.ts +20 -0
  115. package/dist/tools/publish-global-draft.js +50 -0
  116. package/dist/tools/publish-global-draft.js.map +1 -0
  117. package/dist/tools/resolve-reference.d.ts +5 -4
  118. package/dist/tools/resolve-reference.js +4 -0
  119. package/dist/tools/resolve-reference.js.map +1 -1
  120. package/dist/tools/safe-delete.d.ts +5 -5
  121. package/dist/tools/safe-delete.js +20 -15
  122. package/dist/tools/safe-delete.js.map +1 -1
  123. package/dist/tools/schedule-publish.d.ts +5 -5
  124. package/dist/tools/schedule-publish.js +23 -19
  125. package/dist/tools/schedule-publish.js.map +1 -1
  126. package/dist/tools/search-content.d.ts +5 -9
  127. package/dist/tools/search-content.js +16 -12
  128. package/dist/tools/search-content.js.map +1 -1
  129. package/dist/tools/update-document.d.ts +5 -5
  130. package/dist/tools/update-document.js +10 -5
  131. package/dist/tools/update-document.js.map +1 -1
  132. package/dist/tools/update-global.d.ts +27 -0
  133. package/dist/tools/update-global.js +72 -0
  134. package/dist/tools/update-global.js.map +1 -0
  135. package/dist/tools/upload-media.d.ts +5 -4
  136. package/dist/tools/upload-media.js +6 -1
  137. package/dist/tools/upload-media.js.map +1 -1
  138. package/dist/tools/versions.d.ts +10 -9
  139. package/dist/tools/versions.js +15 -7
  140. package/dist/tools/versions.js.map +1 -1
  141. package/dist/types.d.ts +56 -3
  142. package/dist/types.js +13 -6
  143. package/dist/types.js.map +1 -1
  144. package/package.json +11 -4
@@ -1,152 +1,70 @@
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)
1
+ import { hasCollectionDrafts, hasGlobalDrafts } 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)
8
8
  */ export function getDraftBehavior(collection, options) {
9
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.
21
- */ async function resolvePreviewUrl(collection, doc, req, siteUrl) {
22
- const admin = collection.admin ?? {};
23
- const locale = req.locale ?? 'en';
24
- let raw;
25
- const livePreviewUrl = admin.livePreview?.url;
26
- if (typeof livePreviewUrl === 'function') {
27
- try {
28
- raw = await livePreviewUrl({
29
- data: doc,
30
- locale: {
31
- code: locale,
32
- label: locale
33
- },
34
- req,
35
- payload: req.payload,
36
- collectionConfig: collection
37
- });
38
- } catch {
39
- raw = null;
40
- }
41
- } else if (typeof livePreviewUrl === 'string') {
42
- raw = livePreviewUrl;
43
- }
44
- if (!raw && typeof admin.preview === 'function') {
45
- try {
46
- raw = await admin.preview(doc, {
47
- locale,
48
- req,
49
- token: null
50
- });
51
- } catch {
52
- raw = null;
53
- }
54
- }
55
- if (!raw || typeof raw !== 'string') return null;
56
- if (raw.startsWith('http://') || raw.startsWith('https://')) {
57
- return raw;
58
- }
59
- if (!siteUrl) return null;
60
- const base = siteUrl.endsWith('/') ? siteUrl.slice(0, -1) : siteUrl;
61
- const path = raw.startsWith('/') ? raw : `/${raw}`;
62
- return `${base}${path}`;
63
- }
64
- function createOverrideResponse(collection, siteUrl) {
65
- return async (response, doc, req)=>{
66
- if (doc._status !== 'draft') return response;
67
- const previewUrl = await resolvePreviewUrl(collection, doc, req, siteUrl);
68
- if (!previewUrl) {
69
- return {
70
- content: [
71
- ...response.content,
72
- {
73
- type: 'text',
74
- text: '\n📋 This document is a draft. Use the admin panel to preview it.'
75
- }
76
- ]
77
- };
78
- }
79
- return {
80
- content: [
81
- ...response.content,
82
- {
83
- type: 'text',
84
- text: `\n📋 This document is a draft. Preview it here: ${previewUrl}`
85
- }
86
- ]
87
- };
88
- };
89
- }
90
- /**
91
- * Generates the mcpCollections config object for the official mcpPlugin.
92
- *
93
- * For each non-excluded collection:
94
- * - Enables `find` / `delete`; disables the official plugin's raw
95
- * `create<Resource>` and `update<Resource>` tools universally. Both
96
- * crash or silently drop fields on collections whose schemas contain
97
- * richText, upload, blocks, or relationship-array fields. The toolkit's
98
- * `createDocument` / `updateDocument` / `patchLayout` cover both ops via
99
- * the local API. Draft semantics are preserved by `publishDraft`.
100
- * - For draft collections: attaches an `overrideResponse` that appends a
101
- * preview URL — sourced from the collection's own livePreview/preview
102
- * function — to draft documents. Falls back to a generic admin-panel
103
- * message when no preview function is configured.
104
- *
105
- * @returns Map of slug → MCP collection config, plus the set of draft slugs
106
- */ export function generateMcpCollectionConfigs(collections, options) {
107
- const mcpCollections = {};
14
+ /**
15
+ * Walks the collection list and returns:
16
+ * - `draftCollections`: slugs whose `versions.drafts` is on (or whose
17
+ * behavior override is `'always-draft'`).
18
+ * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys
19
+ * collection itself, anything with `auth: true`, anything in
20
+ * `excludeCollections`).
21
+ *
22
+ * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the
23
+ * toolkit owns the endpoint and tool dispatch directly — no need to produce
24
+ * an upstream `mcpCollections` config object.
25
+ */ export function computeDraftCollections(collections, options = {}) {
108
26
  const draftCollections = new Set();
109
- const excludeSlugs = new Set([
110
- 'payload-mcp-api-keys',
27
+ const excluded = new Set([
28
+ options.apiKeysSlug ?? 'payload-mcp-api-keys',
111
29
  ...options.excludeCollections ?? []
112
30
  ]);
113
31
  for (const collection of collections){
114
- if (excludeSlugs.has(collection.slug)) continue;
32
+ if (excluded.has(collection.slug)) continue;
115
33
  // Auth-enabled collections are users — never expose them via MCP.
116
- if (collection.auth) continue;
117
- const behavior = getDraftBehavior(collection, options);
118
- if (behavior !== 'publish') {
119
- draftCollections.add(collection.slug);
34
+ if (collection.auth) {
35
+ excluded.add(collection.slug);
36
+ continue;
120
37
  }
121
- // Disable the official plugin's per-collection `create<Resource>` and
122
- // `update<Resource>` tools. Both call `convertCollectionSchemaToZod` and
123
- // produce broken input schemas on any collection whose JSON schema can't
124
- // be losslessly converted (richText, upload, blocks, relationship-array
125
- // fields → fallback returns `z.record()`):
126
- // - update<Resource>: throws `convertedFields.partial is not a function`
127
- // - create<Resource>: registers with metadata-only params, then the MCP
128
- // SDK strips every content field before it reaches `payload.create()`,
129
- // so creates fail required-field validation with empty data.
130
- // The toolkit's `createDocument` / `updateDocument` / `patchLayout` cover
131
- // both ops via the local API and survive these upstream bugs.
132
- const enabled = {
133
- find: true,
134
- create: false,
135
- update: false,
136
- delete: true
137
- };
138
- const config = {
139
- description: `Manage ${collection.slug} content`,
140
- enabled
141
- };
142
- if (draftCollections.has(collection.slug) && !options.previewDisabled) {
143
- config.overrideResponse = createOverrideResponse(collection, options.siteUrl);
144
- }
145
- mcpCollections[collection.slug] = config;
38
+ const behavior = getDraftBehavior(collection, options);
39
+ if (behavior !== 'publish') draftCollections.add(collection.slug);
40
+ }
41
+ return {
42
+ draftCollections,
43
+ excluded
44
+ };
45
+ }
46
+ /**
47
+ * Peer of `computeDraftCollections` for globals. Returns the slugs whose
48
+ * draft workflow is on (i.e. `versions.drafts` enabled, with optional
49
+ * `'always-publish'` override turning it off), and the set excluded from
50
+ * the MCP surface entirely.
51
+ *
52
+ * Mirrors the collection rule: `versions.drafts: true` defaults to
53
+ * `'always-draft'` so raw publish is locked and `updateGlobal` / patch
54
+ * routes preserve draft semantics.
55
+ */ export function computeDraftGlobals(globals, options = {}) {
56
+ const draftGlobals = new Set();
57
+ const excluded = new Set(options.excludeGlobals ?? []);
58
+ for (const global of globals){
59
+ if (excluded.has(global.slug)) continue;
60
+ if (!hasGlobalDrafts(global)) continue;
61
+ const override = options.draftBehavior?.[global.slug];
62
+ if (override === 'always-publish') continue;
63
+ draftGlobals.add(global.slug);
146
64
  }
147
65
  return {
148
- mcpCollections,
149
- draftCollections
66
+ draftGlobals,
67
+ excluded
150
68
  };
151
69
  }
152
70
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, PayloadRequest } from 'payload'\nimport { hasCollectionDrafts } from './introspection'\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 if (!hasCollectionDrafts(collection)) 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 * - Enables `find` / `delete`; disables the official plugin's raw\n * `create<Resource>` and `update<Resource>` tools universally. Both\n * crash or silently drop fields on collections whose schemas contain\n * richText, upload, blocks, or relationship-array fields. The toolkit's\n * `createDocument` / `updateDocument` / `patchLayout` cover both ops via\n * the local API. Draft semantics are preserved by `publishDraft`.\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 // Disable the official plugin's per-collection `create<Resource>` and\n // `update<Resource>` tools. Both call `convertCollectionSchemaToZod` and\n // produce broken input schemas on any collection whose JSON schema can't\n // be losslessly converted (richText, upload, blocks, relationship-array\n // fields → fallback returns `z.record()`):\n // - update<Resource>: throws `convertedFields.partial is not a function`\n // - create<Resource>: registers with metadata-only params, then the MCP\n // SDK strips every content field before it reaches `payload.create()`,\n // so creates fail required-field validation with empty data.\n // The toolkit's `createDocument` / `updateDocument` / `patchLayout` cover\n // both ops via the local API and survive these upstream bugs.\n const enabled = {\n find: true,\n create: false,\n update: false,\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":["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":"AACA,SAASA,mBAAmB,QAAQ,kBAAiB;AAwCrD;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,UAA4B,EAC5BC,OAA+E;IAE/E,IAAI,CAACH,oBAAoBE,aAAa,OAAO;IAE7C,MAAME,WAAWD,SAASE,eAAe,CAACH,WAAWI,IAAI,CAAC;IAC1D,IAAIF,UAAU,OAAOA;IAErB,OAAO;AACT;AAEA;;;;;;;CAOC,GACD,eAAeG,kBACbL,UAA4B,EAC5BM,GAA4B,EAC5BC,GAAmB,EACnBC,OAA2B;IAE3B,MAAMC,QAAST,WAAWS,KAAK,IAAI,CAAC;IACpC,MAAMC,SAAS,AAACH,IAAYG,MAAM,IAAI;IAEtC,IAAIC;IAEJ,MAAMC,iBAAiBH,MAAMI,WAAW,EAAEC;IAC1C,IAAI,OAAOF,mBAAmB,YAAY;QACxC,IAAI;YACFD,MAAM,MAAMC,eAAe;gBACzBG,MAAMT;gBACNI,QAAQ;oBAAEM,MAAMN;oBAAQO,OAAOP;gBAAO;gBACtCH;gBACAW,SAASX,IAAIW,OAAO;gBACpBC,kBAAkBnB;YACpB;QACF,EAAE,OAAM;YACNW,MAAM;QACR;IACF,OAAO,IAAI,OAAOC,mBAAmB,UAAU;QAC7CD,MAAMC;IACR;IAEA,IAAI,CAACD,OAAO,OAAOF,MAAMW,OAAO,KAAK,YAAY;QAC/C,IAAI;YACFT,MAAM,MAAMF,MAAMW,OAAO,CAACd,KAAK;gBAC7BI;gBACAH;gBACAc,OAAO;YACT;QACF,EAAE,OAAM;YACNV,MAAM;QACR;IACF;IAEA,IAAI,CAACA,OAAO,OAAOA,QAAQ,UAAU,OAAO;IAE5C,IAAIA,IAAIW,UAAU,CAAC,cAAcX,IAAIW,UAAU,CAAC,aAAa;QAC3D,OAAOX;IACT;IAEA,IAAI,CAACH,SAAS,OAAO;IAErB,MAAMe,OAAOf,QAAQgB,QAAQ,CAAC,OAAOhB,QAAQiB,KAAK,CAAC,GAAG,CAAC,KAAKjB;IAC5D,MAAMkB,OAAOf,IAAIW,UAAU,CAAC,OAAOX,MAAM,CAAC,CAAC,EAAEA,KAAK;IAClD,OAAO,GAAGY,OAAOG,MAAM;AACzB;AAEA,SAASC,uBACP3B,UAA4B,EAC5BQ,OAA2B;IAE3B,OAAO,OAAOoB,UAAUtB,KAAKC;QAC3B,IAAID,IAAIuB,OAAO,KAAK,SAAS,OAAOD;QAEpC,MAAME,aAAa,MAAMzB,kBAAkBL,YAAYM,KAAKC,KAAKC;QACjE,IAAI,CAACsB,YAAY;YACf,OAAO;gBACLC,SAAS;uBACJH,SAASG,OAAO;oBACnB;wBACEC,MAAM;wBACNC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,OAAO;YACLF,SAAS;mBACJH,SAASG,OAAO;gBACnB;oBACEC,MAAM;oBACNC,MAAM,CAAC,gDAAgD,EAAEH,YAAY;gBACvE;aACD;QACH;IACF;AACF;AAEA;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASI,6BACdC,WAA+B,EAC/BlC,OAAwB;IAKxB,MAAMmC,iBAAsD,CAAC;IAC7D,MAAMC,mBAAmB,IAAIC;IAE7B,MAAMC,eAAe,IAAID,IAAI;QAC3B;WACIrC,QAAQuC,kBAAkB,IAAI,EAAE;KACrC;IAED,KAAK,MAAMxC,cAAcmC,YAAa;QACpC,IAAII,aAAaE,GAAG,CAACzC,WAAWI,IAAI,GAAG;QAEvC,kEAAkE;QAClE,IAAI,AAACJ,WAAmB0C,IAAI,EAAE;QAE9B,MAAMC,WAAW5C,iBAAiBC,YAAYC;QAE9C,IAAI0C,aAAa,WAAW;YAC1BN,iBAAiBO,GAAG,CAAC5C,WAAWI,IAAI;QACtC;QAEA,sEAAsE;QACtE,yEAAyE;QACzE,yEAAyE;QACzE,wEAAwE;QACxE,2CAA2C;QAC3C,2EAA2E;QAC3E,0EAA0E;QAC1E,2EAA2E;QAC3E,iEAAiE;QACjE,0EAA0E;QAC1E,8DAA8D;QAC9D,MAAMyC,UAAU;YACdC,MAAM;YACNC,QAAQ;YACRC,QAAQ;YACRC,QAAQ;QACV;QAEA,MAAMC,SAA8B;YAClCC,aAAa,CAAC,OAAO,EAAEnD,WAAWI,IAAI,CAAC,QAAQ,CAAC;YAChDyC;QACF;QAEA,IAAIR,iBAAiBI,GAAG,CAACzC,WAAWI,IAAI,KAAK,CAACH,QAAQmD,eAAe,EAAE;YACrEF,OAAOG,gBAAgB,GAAG1B,uBAAuB3B,YAAYC,QAAQO,OAAO;QAC9E;QAEA4B,cAAc,CAACpC,WAAWI,IAAI,CAAC,GAAG8C;IACpC;IAEA,OAAO;QAAEd;QAAgBC;IAAiB;AAC5C"}
1
+ {"version":3,"sources":["../src/draft-workflow.ts"],"sourcesContent":["import type { CollectionConfig, GlobalConfig } from 'payload'\r\nimport { hasCollectionDrafts, hasGlobalDrafts } from './introspection'\r\n\r\ninterface ComputeDraftCollectionsOptions {\r\n /** Per-collection draft behavior overrides */\r\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\r\n /** Collection slugs to exclude from MCP entirely */\r\n excludeCollections?: string[]\r\n /** API-keys collection slug — always excluded from the MCP surface */\r\n apiKeysSlug?: string\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\nexport interface DraftCollectionsResult {\r\n /** Slugs of collections that participate in the draft workflow. */\r\n draftCollections: Set<string>\r\n /** Slugs of collections excluded from the MCP surface entirely. */\r\n excluded: Set<string>\r\n}\r\n\r\n/**\r\n * Walks the collection list and returns:\r\n * - `draftCollections`: slugs whose `versions.drafts` is on (or whose\r\n * behavior override is `'always-draft'`).\r\n * - `excluded`: slugs to hide from the MCP surface entirely (the api-keys\r\n * collection itself, anything with `auth: true`, anything in\r\n * `excludeCollections`).\r\n *\r\n * Replaces the v0.3.x `generateMcpCollectionConfigs` shape now that the\r\n * toolkit owns the endpoint and tool dispatch directly — no need to produce\r\n * an upstream `mcpCollections` config object.\r\n */\r\nexport function computeDraftCollections(\r\n collections: CollectionConfig[],\r\n options: ComputeDraftCollectionsOptions = {},\r\n): DraftCollectionsResult {\r\n const draftCollections = new Set<string>()\r\n const excluded = new Set<string>([\r\n options.apiKeysSlug ?? 'payload-mcp-api-keys',\r\n ...(options.excludeCollections ?? []),\r\n ])\r\n\r\n for (const collection of collections) {\r\n if (excluded.has(collection.slug)) continue\r\n\r\n // Auth-enabled collections are users — never expose them via MCP.\r\n if ((collection as { auth?: unknown }).auth) {\r\n excluded.add(collection.slug)\r\n continue\r\n }\r\n\r\n const behavior = getDraftBehavior(collection, options)\r\n if (behavior !== 'publish') draftCollections.add(collection.slug)\r\n }\r\n\r\n return { draftCollections, excluded }\r\n}\r\n\r\ninterface ComputeDraftGlobalsOptions {\r\n /**\r\n * Per-resource draft behavior overrides. Shared with collections: a single\r\n * record may contain both collection slugs and global slugs. Collisions\r\n * are unambiguous in practice — Payload's slug registry forbids a global\r\n * and a collection sharing a slug.\r\n */\r\n draftBehavior?: Record<string, 'always-draft' | 'always-publish'>\r\n /** Global slugs to exclude from the MCP surface entirely */\r\n excludeGlobals?: string[]\r\n}\r\n\r\nexport interface DraftGlobalsResult {\r\n draftGlobals: Set<string>\r\n excluded: Set<string>\r\n}\r\n\r\n/**\r\n * Peer of `computeDraftCollections` for globals. Returns the slugs whose\r\n * draft workflow is on (i.e. `versions.drafts` enabled, with optional\r\n * `'always-publish'` override turning it off), and the set excluded from\r\n * the MCP surface entirely.\r\n *\r\n * Mirrors the collection rule: `versions.drafts: true` defaults to\r\n * `'always-draft'` so raw publish is locked and `updateGlobal` / patch\r\n * routes preserve draft semantics.\r\n */\r\nexport function computeDraftGlobals(\r\n globals: GlobalConfig[],\r\n options: ComputeDraftGlobalsOptions = {},\r\n): DraftGlobalsResult {\r\n const draftGlobals = new Set<string>()\r\n const excluded = new Set<string>(options.excludeGlobals ?? [])\r\n\r\n for (const global of globals) {\r\n if (excluded.has(global.slug)) continue\r\n if (!hasGlobalDrafts(global)) continue\r\n const override = options.draftBehavior?.[global.slug]\r\n if (override === 'always-publish') continue\r\n draftGlobals.add(global.slug)\r\n }\r\n\r\n return { draftGlobals, excluded }\r\n}\r\n"],"names":["hasCollectionDrafts","hasGlobalDrafts","getDraftBehavior","collection","options","override","draftBehavior","slug","computeDraftCollections","collections","draftCollections","Set","excluded","apiKeysSlug","excludeCollections","has","auth","add","behavior","computeDraftGlobals","globals","draftGlobals","excludeGlobals","global"],"mappings":"AACA,SAASA,mBAAmB,EAAEC,eAAe,QAAQ,kBAAiB;AAWtE;;;;;;CAMC,GACD,OAAO,SAASC,iBACdC,UAA4B,EAC5BC,OAA+E;IAE/E,IAAI,CAACJ,oBAAoBG,aAAa,OAAO;IAE7C,MAAME,WAAWD,SAASE,eAAe,CAACH,WAAWI,IAAI,CAAC;IAC1D,IAAIF,UAAU,OAAOA;IAErB,OAAO;AACT;AASA;;;;;;;;;;;CAWC,GACD,OAAO,SAASG,wBACdC,WAA+B,EAC/BL,UAA0C,CAAC,CAAC;IAE5C,MAAMM,mBAAmB,IAAIC;IAC7B,MAAMC,WAAW,IAAID,IAAY;QAC/BP,QAAQS,WAAW,IAAI;WACnBT,QAAQU,kBAAkB,IAAI,EAAE;KACrC;IAED,KAAK,MAAMX,cAAcM,YAAa;QACpC,IAAIG,SAASG,GAAG,CAACZ,WAAWI,IAAI,GAAG;QAEnC,kEAAkE;QAClE,IAAI,AAACJ,WAAkCa,IAAI,EAAE;YAC3CJ,SAASK,GAAG,CAACd,WAAWI,IAAI;YAC5B;QACF;QAEA,MAAMW,WAAWhB,iBAAiBC,YAAYC;QAC9C,IAAIc,aAAa,WAAWR,iBAAiBO,GAAG,CAACd,WAAWI,IAAI;IAClE;IAEA,OAAO;QAAEG;QAAkBE;IAAS;AACtC;AAmBA;;;;;;;;;CASC,GACD,OAAO,SAASO,oBACdC,OAAuB,EACvBhB,UAAsC,CAAC,CAAC;IAExC,MAAMiB,eAAe,IAAIV;IACzB,MAAMC,WAAW,IAAID,IAAYP,QAAQkB,cAAc,IAAI,EAAE;IAE7D,KAAK,MAAMC,UAAUH,QAAS;QAC5B,IAAIR,SAASG,GAAG,CAACQ,OAAOhB,IAAI,GAAG;QAC/B,IAAI,CAACN,gBAAgBsB,SAAS;QAC9B,MAAMlB,WAAWD,QAAQE,aAAa,EAAE,CAACiB,OAAOhB,IAAI,CAAC;QACrD,IAAIF,aAAa,kBAAkB;QACnCgB,aAAaJ,GAAG,CAACM,OAAOhB,IAAI;IAC9B;IAEA,OAAO;QAAEc;QAAcT;IAAS;AAClC"}
@@ -0,0 +1,35 @@
1
+ import type { Endpoint, PayloadRequest } from 'payload';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ export declare const MCP_BASE_PATH = "/api/mcp";
4
+ export declare const MCP_ENDPOINT_PATH = "/mcp";
5
+ export type InitializeServerForRequest = (req: PayloadRequest) => (server: McpServer) => void | Promise<void>;
6
+ export interface CreateMcpEndpointsOptions {
7
+ /**
8
+ * Per-request factory that returns the McpServer initializer. Called once
9
+ * per POST /api/mcp; the returned callback receives a fresh McpServer
10
+ * instance and registers tools/prompts/resources scoped to this request.
11
+ */
12
+ buildInitializeServer: InitializeServerForRequest;
13
+ /**
14
+ * Origins permitted to send the `Origin` header. Server-to-server callers
15
+ * (no Origin) are always allowed. An empty / unset list means "no browsers".
16
+ * `*` is intentionally not honoured — be explicit.
17
+ */
18
+ allowedOrigins?: string[];
19
+ /**
20
+ * The Payload `serverURL`. When set, the request `Host` header must match
21
+ * this origin's host (DNS-rebinding mitigation). When unset, host check is
22
+ * skipped (useful in dev where the host varies).
23
+ */
24
+ serverURL?: string;
25
+ /** Forwarded to mcp-handler. Default false. */
26
+ verboseLogs?: boolean;
27
+ }
28
+ /**
29
+ * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into
30
+ * `incomingConfig.endpoints` (additive — existing endpoints are preserved).
31
+ *
32
+ * Pure function; can be unit-tested by invoking the returned handlers with a
33
+ * minimally-shaped PayloadRequest.
34
+ */
35
+ export declare function createMcpEndpoints(options: CreateMcpEndpointsOptions): Endpoint[];
@@ -0,0 +1,105 @@
1
+ import { createMcpHandler } from 'mcp-handler';
2
+ import { getApiKeyContext } from './auth-strategy';
3
+ export const MCP_BASE_PATH = '/api/mcp';
4
+ export const MCP_ENDPOINT_PATH = '/mcp';
5
+ /**
6
+ * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To
7
+ * land that on `/api/mcp` (the public URL), we pass `/api` here — not the
8
+ * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.
9
+ */ const MCP_HANDLER_BASE_PATH = '/api';
10
+ function jsonRpcError(message, code, status) {
11
+ const body = {
12
+ jsonrpc: '2.0',
13
+ id: null,
14
+ error: {
15
+ code,
16
+ message
17
+ }
18
+ };
19
+ return new Response(JSON.stringify(body), {
20
+ status,
21
+ headers: {
22
+ 'content-type': 'application/json'
23
+ }
24
+ });
25
+ }
26
+ function isOriginAllowed(origin, allowedOrigins) {
27
+ if (!origin) return true // server-to-server
28
+ ;
29
+ if (!allowedOrigins || allowedOrigins.length === 0) return false;
30
+ return allowedOrigins.includes(origin);
31
+ }
32
+ function isHostAllowed(host, serverURL) {
33
+ if (!serverURL) return true // dev / no serverURL configured
34
+ ;
35
+ if (!host) return false;
36
+ let expected;
37
+ try {
38
+ expected = new URL(serverURL).host;
39
+ } catch {
40
+ return true // misconfigured serverURL — fail open rather than break boot
41
+ ;
42
+ }
43
+ return host === expected;
44
+ }
45
+ /**
46
+ * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into
47
+ * `incomingConfig.endpoints` (additive — existing endpoints are preserved).
48
+ *
49
+ * Pure function; can be unit-tested by invoking the returned handlers with a
50
+ * minimally-shaped PayloadRequest.
51
+ */ export function createMcpEndpoints(options) {
52
+ const { buildInitializeServer, allowedOrigins, serverURL, verboseLogs = false } = options;
53
+ const postHandler = async (req)=>{
54
+ const headers = req.headers;
55
+ const origin = headers?.get('origin') ?? null;
56
+ const host = headers?.get('host') ?? null;
57
+ if (!isHostAllowed(host, serverURL)) {
58
+ return jsonRpcError('Invalid host', -32600, 400);
59
+ }
60
+ if (!isOriginAllowed(origin, allowedOrigins)) {
61
+ return jsonRpcError('Origin not allowed', -32600, 403);
62
+ }
63
+ // Auth gate. The bearer strategy populates req.user._mcpKey on a valid
64
+ // Authorization: Bearer <key>; missing context means the request did not
65
+ // present a recognized MCP API key. We refuse before constructing any
66
+ // mcp-handler — refusing here prevents tools/list and tool dispatch from
67
+ // running on unauthenticated requests.
68
+ if (!getApiKeyContext(req)) {
69
+ return jsonRpcError('Unauthorized: MCP API key required', -32001, 401);
70
+ }
71
+ if (!req.url) return jsonRpcError('Missing request URL', -32600, 400);
72
+ const handler = createMcpHandler(buildInitializeServer(req), undefined, {
73
+ basePath: MCP_HANDLER_BASE_PATH,
74
+ disableSse: true,
75
+ verboseLogs
76
+ });
77
+ const fetchRequest = new Request(req.url, {
78
+ method: req.method ?? 'POST',
79
+ headers: req.headers,
80
+ body: req.body,
81
+ // duplex is required for streaming bodies in newer node fetch
82
+ ...{
83
+ duplex: 'half'
84
+ }
85
+ });
86
+ return handler(fetchRequest);
87
+ };
88
+ const getHandler = async ()=>{
89
+ return jsonRpcError('POST required for MCP requests', -32600, 405);
90
+ };
91
+ return [
92
+ {
93
+ path: MCP_ENDPOINT_PATH,
94
+ method: 'post',
95
+ handler: postHandler
96
+ },
97
+ {
98
+ path: MCP_ENDPOINT_PATH,
99
+ method: 'get',
100
+ handler: getHandler
101
+ }
102
+ ];
103
+ }
104
+
105
+ //# sourceMappingURL=endpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/endpoint.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload'\r\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\r\nimport { createMcpHandler } from 'mcp-handler'\r\nimport { getApiKeyContext } from './auth-strategy'\r\n\r\nexport const MCP_BASE_PATH = '/api/mcp'\r\nexport const MCP_ENDPOINT_PATH = '/mcp'\r\n/**\r\n * `mcp-handler` derives its streamable-HTTP route as `${basePath}/mcp`. To\r\n * land that on `/api/mcp` (the public URL), we pass `/api` here — not the\r\n * full `/api/mcp`, which would route to `/api/mcp/mcp` and 404.\r\n */\r\nconst MCP_HANDLER_BASE_PATH = '/api'\r\n\r\nexport type InitializeServerForRequest = (\r\n req: PayloadRequest,\r\n) => (server: McpServer) => void | Promise<void>\r\n\r\nexport interface CreateMcpEndpointsOptions {\r\n /**\r\n * Per-request factory that returns the McpServer initializer. Called once\r\n * per POST /api/mcp; the returned callback receives a fresh McpServer\r\n * instance and registers tools/prompts/resources scoped to this request.\r\n */\r\n buildInitializeServer: InitializeServerForRequest\r\n /**\r\n * Origins permitted to send the `Origin` header. Server-to-server callers\r\n * (no Origin) are always allowed. An empty / unset list means \"no browsers\".\r\n * `*` is intentionally not honoured — be explicit.\r\n */\r\n allowedOrigins?: string[]\r\n /**\r\n * The Payload `serverURL`. When set, the request `Host` header must match\r\n * this origin's host (DNS-rebinding mitigation). When unset, host check is\r\n * skipped (useful in dev where the host varies).\r\n */\r\n serverURL?: string\r\n /** Forwarded to mcp-handler. Default false. */\r\n verboseLogs?: boolean\r\n}\r\n\r\ninterface JsonRpcErrorBody {\r\n jsonrpc: '2.0'\r\n error: { code: number; message: string }\r\n id: null\r\n}\r\n\r\nfunction jsonRpcError(message: string, code: number, status: number): Response {\r\n const body: JsonRpcErrorBody = { jsonrpc: '2.0', id: null, error: { code, message } }\r\n return new Response(JSON.stringify(body), {\r\n status,\r\n headers: { 'content-type': 'application/json' },\r\n })\r\n}\r\n\r\nfunction isOriginAllowed(origin: string | null, allowedOrigins: string[] | undefined): boolean {\r\n if (!origin) return true // server-to-server\r\n if (!allowedOrigins || allowedOrigins.length === 0) return false\r\n return allowedOrigins.includes(origin)\r\n}\r\n\r\nfunction isHostAllowed(host: string | null, serverURL: string | undefined): boolean {\r\n if (!serverURL) return true // dev / no serverURL configured\r\n if (!host) return false\r\n let expected: string\r\n try {\r\n expected = new URL(serverURL).host\r\n } catch {\r\n return true // misconfigured serverURL — fail open rather than break boot\r\n }\r\n return host === expected\r\n}\r\n\r\n/**\r\n * Builds the POST + GET endpoints for `/api/mcp`. Designed to be pushed into\r\n * `incomingConfig.endpoints` (additive — existing endpoints are preserved).\r\n *\r\n * Pure function; can be unit-tested by invoking the returned handlers with a\r\n * minimally-shaped PayloadRequest.\r\n */\r\nexport function createMcpEndpoints(options: CreateMcpEndpointsOptions): Endpoint[] {\r\n const { buildInitializeServer, allowedOrigins, serverURL, verboseLogs = false } = options\r\n\r\n const postHandler = async (req: PayloadRequest): Promise<Response> => {\r\n const headers = req.headers as Headers | undefined\r\n const origin = headers?.get('origin') ?? null\r\n const host = headers?.get('host') ?? null\r\n\r\n if (!isHostAllowed(host, serverURL)) {\r\n return jsonRpcError('Invalid host', -32600, 400)\r\n }\r\n if (!isOriginAllowed(origin, allowedOrigins)) {\r\n return jsonRpcError('Origin not allowed', -32600, 403)\r\n }\r\n\r\n // Auth gate. The bearer strategy populates req.user._mcpKey on a valid\r\n // Authorization: Bearer <key>; missing context means the request did not\r\n // present a recognized MCP API key. We refuse before constructing any\r\n // mcp-handler — refusing here prevents tools/list and tool dispatch from\r\n // running on unauthenticated requests.\r\n if (!getApiKeyContext(req)) {\r\n return jsonRpcError('Unauthorized: MCP API key required', -32001, 401)\r\n }\r\n\r\n if (!req.url) return jsonRpcError('Missing request URL', -32600, 400)\r\n\r\n const handler = createMcpHandler(buildInitializeServer(req), undefined, {\r\n basePath: MCP_HANDLER_BASE_PATH,\r\n disableSse: true,\r\n verboseLogs,\r\n })\r\n\r\n const fetchRequest = new Request(req.url, {\r\n method: req.method ?? 'POST',\r\n headers: req.headers as HeadersInit,\r\n body: req.body as BodyInit | null | undefined,\r\n // duplex is required for streaming bodies in newer node fetch\r\n ...({ duplex: 'half' } as Record<string, unknown>),\r\n })\r\n\r\n return handler(fetchRequest)\r\n }\r\n\r\n const getHandler = async (): Promise<Response> => {\r\n return jsonRpcError('POST required for MCP requests', -32600, 405)\r\n }\r\n\r\n return [\r\n { path: MCP_ENDPOINT_PATH, method: 'post', handler: postHandler },\r\n { path: MCP_ENDPOINT_PATH, method: 'get', handler: getHandler },\r\n ]\r\n}\r\n"],"names":["createMcpHandler","getApiKeyContext","MCP_BASE_PATH","MCP_ENDPOINT_PATH","MCP_HANDLER_BASE_PATH","jsonRpcError","message","code","status","body","jsonrpc","id","error","Response","JSON","stringify","headers","isOriginAllowed","origin","allowedOrigins","length","includes","isHostAllowed","host","serverURL","expected","URL","createMcpEndpoints","options","buildInitializeServer","verboseLogs","postHandler","req","get","url","handler","undefined","basePath","disableSse","fetchRequest","Request","method","duplex","getHandler","path"],"mappings":"AAEA,SAASA,gBAAgB,QAAQ,cAAa;AAC9C,SAASC,gBAAgB,QAAQ,kBAAiB;AAElD,OAAO,MAAMC,gBAAgB,WAAU;AACvC,OAAO,MAAMC,oBAAoB,OAAM;AACvC;;;;CAIC,GACD,MAAMC,wBAAwB;AAmC9B,SAASC,aAAaC,OAAe,EAAEC,IAAY,EAAEC,MAAc;IACjE,MAAMC,OAAyB;QAAEC,SAAS;QAAOC,IAAI;QAAMC,OAAO;YAAEL;YAAMD;QAAQ;IAAE;IACpF,OAAO,IAAIO,SAASC,KAAKC,SAAS,CAACN,OAAO;QACxCD;QACAQ,SAAS;YAAE,gBAAgB;QAAmB;IAChD;AACF;AAEA,SAASC,gBAAgBC,MAAqB,EAAEC,cAAoC;IAClF,IAAI,CAACD,QAAQ,OAAO,KAAK,mBAAmB;;IAC5C,IAAI,CAACC,kBAAkBA,eAAeC,MAAM,KAAK,GAAG,OAAO;IAC3D,OAAOD,eAAeE,QAAQ,CAACH;AACjC;AAEA,SAASI,cAAcC,IAAmB,EAAEC,SAA6B;IACvE,IAAI,CAACA,WAAW,OAAO,KAAK,gCAAgC;;IAC5D,IAAI,CAACD,MAAM,OAAO;IAClB,IAAIE;IACJ,IAAI;QACFA,WAAW,IAAIC,IAAIF,WAAWD,IAAI;IACpC,EAAE,OAAM;QACN,OAAO,KAAK,6DAA6D;;IAC3E;IACA,OAAOA,SAASE;AAClB;AAEA;;;;;;CAMC,GACD,OAAO,SAASE,mBAAmBC,OAAkC;IACnE,MAAM,EAAEC,qBAAqB,EAAEV,cAAc,EAAEK,SAAS,EAAEM,cAAc,KAAK,EAAE,GAAGF;IAElF,MAAMG,cAAc,OAAOC;QACzB,MAAMhB,UAAUgB,IAAIhB,OAAO;QAC3B,MAAME,SAASF,SAASiB,IAAI,aAAa;QACzC,MAAMV,OAAOP,SAASiB,IAAI,WAAW;QAErC,IAAI,CAACX,cAAcC,MAAMC,YAAY;YACnC,OAAOnB,aAAa,gBAAgB,CAAC,OAAO;QAC9C;QACA,IAAI,CAACY,gBAAgBC,QAAQC,iBAAiB;YAC5C,OAAOd,aAAa,sBAAsB,CAAC,OAAO;QACpD;QAEA,uEAAuE;QACvE,yEAAyE;QACzE,sEAAsE;QACtE,yEAAyE;QACzE,uCAAuC;QACvC,IAAI,CAACJ,iBAAiB+B,MAAM;YAC1B,OAAO3B,aAAa,sCAAsC,CAAC,OAAO;QACpE;QAEA,IAAI,CAAC2B,IAAIE,GAAG,EAAE,OAAO7B,aAAa,uBAAuB,CAAC,OAAO;QAEjE,MAAM8B,UAAUnC,iBAAiB6B,sBAAsBG,MAAMI,WAAW;YACtEC,UAAUjC;YACVkC,YAAY;YACZR;QACF;QAEA,MAAMS,eAAe,IAAIC,QAAQR,IAAIE,GAAG,EAAE;YACxCO,QAAQT,IAAIS,MAAM,IAAI;YACtBzB,SAASgB,IAAIhB,OAAO;YACpBP,MAAMuB,IAAIvB,IAAI;YACd,8DAA8D;YAC9D,GAAI;gBAAEiC,QAAQ;YAAO,CAAC;QACxB;QAEA,OAAOP,QAAQI;IACjB;IAEA,MAAMI,aAAa;QACjB,OAAOtC,aAAa,kCAAkC,CAAC,OAAO;IAChE;IAEA,OAAO;QACL;YAAEuC,MAAMzC;YAAmBsC,QAAQ;YAAQN,SAASJ;QAAY;QAChE;YAAEa,MAAMzC;YAAmBsC,QAAQ;YAAON,SAASQ;QAAW;KAC/D;AACH"}
package/dist/hash.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Computes the HMAC index used to look up an API-key row by its plaintext.
3
+ *
4
+ * Formula: `HMAC-SHA-256(payloadSecret).update(plaintext)` returned as hex.
5
+ *
6
+ * This matches the formula used by `@payloadcms/plugin-mcp` v0.3.x and by
7
+ * Payload's own `useAPIKey: true` collection auth — keeping the storage shape
8
+ * identical so v0.4 can authenticate rows created under v0.3.x without
9
+ * re-issuing keys (R11).
10
+ */
11
+ export declare function hashKey(plaintext: string, payloadSecret: string): string;
12
+ /**
13
+ * Constant-time comparison of two hex-encoded hashes.
14
+ * Returns false on any length mismatch without throwing.
15
+ */
16
+ export declare function verifyHash(presented: string, stored: string): boolean;
17
+ /**
18
+ * Extracts the plaintext token from an `Authorization: Bearer <token>` header.
19
+ * Returns null for any other shape (missing, wrong scheme, empty token).
20
+ */
21
+ export declare function extractBearerToken(headerValue: string | null | undefined): string | null;
package/dist/hash.js ADDED
@@ -0,0 +1,36 @@
1
+ import crypto from 'crypto';
2
+ /**
3
+ * Computes the HMAC index used to look up an API-key row by its plaintext.
4
+ *
5
+ * Formula: `HMAC-SHA-256(payloadSecret).update(plaintext)` returned as hex.
6
+ *
7
+ * This matches the formula used by `@payloadcms/plugin-mcp` v0.3.x and by
8
+ * Payload's own `useAPIKey: true` collection auth — keeping the storage shape
9
+ * identical so v0.4 can authenticate rows created under v0.3.x without
10
+ * re-issuing keys (R11).
11
+ */ export function hashKey(plaintext, payloadSecret) {
12
+ return crypto.createHmac('sha256', payloadSecret).update(plaintext).digest('hex');
13
+ }
14
+ /**
15
+ * Constant-time comparison of two hex-encoded hashes.
16
+ * Returns false on any length mismatch without throwing.
17
+ */ export function verifyHash(presented, stored) {
18
+ if (typeof presented !== 'string' || typeof stored !== 'string') return false;
19
+ if (presented.length !== stored.length) return false;
20
+ const a = Buffer.from(presented, 'hex');
21
+ const b = Buffer.from(stored, 'hex');
22
+ if (a.length !== b.length || a.length === 0) return false;
23
+ return crypto.timingSafeEqual(a, b);
24
+ }
25
+ const BEARER_PREFIX = 'Bearer ';
26
+ /**
27
+ * Extracts the plaintext token from an `Authorization: Bearer <token>` header.
28
+ * Returns null for any other shape (missing, wrong scheme, empty token).
29
+ */ export function extractBearerToken(headerValue) {
30
+ if (typeof headerValue !== 'string') return null;
31
+ if (!headerValue.startsWith(BEARER_PREFIX)) return null;
32
+ const token = headerValue.slice(BEARER_PREFIX.length).trim();
33
+ return token.length > 0 ? token : null;
34
+ }
35
+
36
+ //# sourceMappingURL=hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hash.ts"],"sourcesContent":["import crypto from 'crypto'\r\n\r\n/**\r\n * Computes the HMAC index used to look up an API-key row by its plaintext.\r\n *\r\n * Formula: `HMAC-SHA-256(payloadSecret).update(plaintext)` returned as hex.\r\n *\r\n * This matches the formula used by `@payloadcms/plugin-mcp` v0.3.x and by\r\n * Payload's own `useAPIKey: true` collection auth — keeping the storage shape\r\n * identical so v0.4 can authenticate rows created under v0.3.x without\r\n * re-issuing keys (R11).\r\n */\r\nexport function hashKey(plaintext: string, payloadSecret: string): string {\r\n return crypto.createHmac('sha256', payloadSecret).update(plaintext).digest('hex')\r\n}\r\n\r\n/**\r\n * Constant-time comparison of two hex-encoded hashes.\r\n * Returns false on any length mismatch without throwing.\r\n */\r\nexport function verifyHash(presented: string, stored: string): boolean {\r\n if (typeof presented !== 'string' || typeof stored !== 'string') return false\r\n if (presented.length !== stored.length) return false\r\n const a = Buffer.from(presented, 'hex')\r\n const b = Buffer.from(stored, 'hex')\r\n if (a.length !== b.length || a.length === 0) return false\r\n return crypto.timingSafeEqual(a, b)\r\n}\r\n\r\nconst BEARER_PREFIX = 'Bearer '\r\n\r\n/**\r\n * Extracts the plaintext token from an `Authorization: Bearer <token>` header.\r\n * Returns null for any other shape (missing, wrong scheme, empty token).\r\n */\r\nexport function extractBearerToken(headerValue: string | null | undefined): string | null {\r\n if (typeof headerValue !== 'string') return null\r\n if (!headerValue.startsWith(BEARER_PREFIX)) return null\r\n const token = headerValue.slice(BEARER_PREFIX.length).trim()\r\n return token.length > 0 ? token : null\r\n}\r\n"],"names":["crypto","hashKey","plaintext","payloadSecret","createHmac","update","digest","verifyHash","presented","stored","length","a","Buffer","from","b","timingSafeEqual","BEARER_PREFIX","extractBearerToken","headerValue","startsWith","token","slice","trim"],"mappings":"AAAA,OAAOA,YAAY,SAAQ;AAE3B;;;;;;;;;CASC,GACD,OAAO,SAASC,QAAQC,SAAiB,EAAEC,aAAqB;IAC9D,OAAOH,OAAOI,UAAU,CAAC,UAAUD,eAAeE,MAAM,CAACH,WAAWI,MAAM,CAAC;AAC7E;AAEA;;;CAGC,GACD,OAAO,SAASC,WAAWC,SAAiB,EAAEC,MAAc;IAC1D,IAAI,OAAOD,cAAc,YAAY,OAAOC,WAAW,UAAU,OAAO;IACxE,IAAID,UAAUE,MAAM,KAAKD,OAAOC,MAAM,EAAE,OAAO;IAC/C,MAAMC,IAAIC,OAAOC,IAAI,CAACL,WAAW;IACjC,MAAMM,IAAIF,OAAOC,IAAI,CAACJ,QAAQ;IAC9B,IAAIE,EAAED,MAAM,KAAKI,EAAEJ,MAAM,IAAIC,EAAED,MAAM,KAAK,GAAG,OAAO;IACpD,OAAOV,OAAOe,eAAe,CAACJ,GAAGG;AACnC;AAEA,MAAME,gBAAgB;AAEtB;;;CAGC,GACD,OAAO,SAASC,mBAAmBC,WAAsC;IACvE,IAAI,OAAOA,gBAAgB,UAAU,OAAO;IAC5C,IAAI,CAACA,YAAYC,UAAU,CAACH,gBAAgB,OAAO;IACnD,MAAMI,QAAQF,YAAYG,KAAK,CAACL,cAAcN,MAAM,EAAEY,IAAI;IAC1D,OAAOF,MAAMV,MAAM,GAAG,IAAIU,QAAQ;AACpC"}
package/dist/index.d.ts CHANGED
@@ -1,19 +1,19 @@
1
1
  import type { Plugin } from 'payload';
2
2
  import type { ContentToolkitOptions } from './types';
3
3
  /**
4
- * Payload MCP Toolkit
4
+ * payload-mcp-toolkit — standalone Payload v3 MCP plugin.
5
5
  *
6
- * Layered on top of the official @payloadcms/plugin-mcp. The toolkit
7
- * introspects your Payload config and registers schema-aware prompts,
8
- * resources, and tools so AI clients can drive the CMS without
9
- * hand-built plumbing.
6
+ * Owns the `/api/mcp` endpoint, the `payload-mcp-api-keys` collection,
7
+ * bearer authentication via Payload's `auth.strategies` extension point,
8
+ * and the per-tool scope check. Upstream `@payloadcms/plugin-mcp` is no
9
+ * longer required (and is incompatible — see `assertNoUpstreamPlugin`).
10
10
  *
11
11
  * Zero-config usage:
12
12
  * ```ts
13
- * plugins: [contentToolkitPlugin()]
13
+ * plugins: [mcpToolkitPlugin()]
14
14
  * ```
15
15
  *
16
- * Every option below is an optional escape hatch — see ContentToolkitOptions.
16
+ * See `ContentToolkitOptions` for the (entirely optional) escape hatches.
17
17
  */
18
- export declare function contentToolkitPlugin(options?: ContentToolkitOptions): Plugin;
19
- export type { ContentToolkitOptions, DomainPrompt, CollectionSchema, BlockCatalog, BlockSchema, BlockNestingMap, BlockNestingEdge, RelationshipEdge, FieldSchema, } from './types';
18
+ export declare function mcpToolkitPlugin(options?: ContentToolkitOptions): Plugin;
19
+ export type { ContentToolkitOptions, DomainPrompt, CollectionSchema, GlobalSchema, BlockCatalog, BlockSchema, BlockNestingMap, BlockNestingEdge, RelationshipEdge, FieldSchema, CollectionAction, GlobalAction, KeyScopes, ScopePreset, } from './types';