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.
- package/README.md +232 -151
- package/dist/__tests__/api-keys.test.js +292 -0
- package/dist/__tests__/api-keys.test.js.map +1 -0
- package/dist/__tests__/auth-strategy.test.js +681 -0
- package/dist/__tests__/auth-strategy.test.js.map +1 -0
- package/dist/__tests__/conflict-detection.test.js +69 -0
- package/dist/__tests__/conflict-detection.test.js.map +1 -0
- package/dist/__tests__/delete-document.test.js +70 -0
- package/dist/__tests__/delete-document.test.js.map +1 -0
- package/dist/__tests__/endpoint.test.js +143 -0
- package/dist/__tests__/endpoint.test.js.map +1 -0
- package/dist/__tests__/find-document.test.js +178 -0
- package/dist/__tests__/find-document.test.js.map +1 -0
- package/dist/__tests__/find-global.test.js +173 -0
- package/dist/__tests__/find-global.test.js.map +1 -0
- package/dist/__tests__/global-versions.test.js +183 -0
- package/dist/__tests__/global-versions.test.js.map +1 -0
- package/dist/__tests__/hash.test.js +58 -0
- package/dist/__tests__/hash.test.js.map +1 -0
- package/dist/__tests__/index-integration.test.js +191 -0
- package/dist/__tests__/index-integration.test.js.map +1 -0
- package/dist/__tests__/introspection.test.js +201 -1
- package/dist/__tests__/introspection.test.js.map +1 -1
- package/dist/__tests__/patch-global-layout.test.js +474 -0
- package/dist/__tests__/patch-global-layout.test.js.map +1 -0
- package/dist/__tests__/patch-layout.test.js +171 -0
- package/dist/__tests__/patch-layout.test.js.map +1 -0
- package/dist/__tests__/registry.test.js +795 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/resources.test.js +139 -0
- package/dist/__tests__/resources.test.js.map +1 -0
- package/dist/__tests__/update-global.test.js +157 -0
- package/dist/__tests__/update-global.test.js.map +1 -0
- package/dist/api-keys.d.ts +46 -0
- package/dist/api-keys.js +272 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/auth-strategy.d.ts +85 -0
- package/dist/auth-strategy.js +219 -0
- package/dist/auth-strategy.js.map +1 -0
- package/dist/components/CollectionScopesMatrix.d.ts +8 -0
- package/dist/components/CollectionScopesMatrix.js +32 -0
- package/dist/components/CollectionScopesMatrix.js.map +1 -0
- package/dist/components/GlobalScopesMatrix.d.ts +8 -0
- package/dist/components/GlobalScopesMatrix.js +28 -0
- package/dist/components/GlobalScopesMatrix.js.map +1 -0
- package/dist/components/ScopesTable.d.ts +19 -0
- package/dist/components/ScopesTable.js +285 -0
- package/dist/components/ScopesTable.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +4 -0
- package/dist/components/index.js.map +1 -0
- package/dist/conflict-detection.d.ts +13 -0
- package/dist/conflict-detection.js +41 -0
- package/dist/conflict-detection.js.map +1 -0
- package/dist/draft-workflow.d.ts +46 -48
- package/dist/draft-workflow.js +53 -135
- package/dist/draft-workflow.js.map +1 -1
- package/dist/endpoint.d.ts +35 -0
- package/dist/endpoint.js +105 -0
- package/dist/endpoint.js.map +1 -0
- package/dist/hash.d.ts +21 -0
- package/dist/hash.js +36 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +9 -9
- package/dist/index.js +167 -69
- package/dist/index.js.map +1 -1
- package/dist/introspection.d.ts +17 -3
- package/dist/introspection.js +95 -36
- package/dist/introspection.js.map +1 -1
- package/dist/prompts.js +5 -5
- package/dist/prompts.js.map +1 -1
- package/dist/registry.d.ts +50 -0
- package/dist/registry.js +169 -0
- package/dist/registry.js.map +1 -0
- package/dist/resources.d.ts +5 -3
- package/dist/resources.js +23 -11
- package/dist/resources.js.map +1 -1
- package/dist/scope/audit-log.d.ts +18 -0
- package/dist/scope/audit-log.js +50 -0
- package/dist/scope/audit-log.js.map +1 -0
- package/dist/scope/policy.d.ts +73 -0
- package/dist/scope/policy.js +218 -0
- package/dist/scope/policy.js.map +1 -0
- package/dist/tools/_helpers.d.ts +28 -1
- package/dist/tools/_helpers.js +83 -0
- package/dist/tools/_helpers.js.map +1 -1
- package/dist/tools/_layout-helpers.d.ts +43 -0
- package/dist/tools/_layout-helpers.js +159 -0
- package/dist/tools/_layout-helpers.js.map +1 -0
- package/dist/tools/create-document.d.ts +5 -5
- package/dist/tools/create-document.js +25 -21
- package/dist/tools/create-document.js.map +1 -1
- package/dist/tools/delete-document.d.ts +25 -0
- package/dist/tools/delete-document.js +49 -0
- package/dist/tools/delete-document.js.map +1 -0
- package/dist/tools/find-document.d.ts +33 -0
- package/dist/tools/find-document.js +97 -0
- package/dist/tools/find-document.js.map +1 -0
- package/dist/tools/find-global.d.ts +26 -0
- package/dist/tools/find-global.js +122 -0
- package/dist/tools/find-global.js.map +1 -0
- package/dist/tools/global-versions.d.ts +39 -0
- package/dist/tools/global-versions.js +132 -0
- package/dist/tools/global-versions.js.map +1 -0
- package/dist/tools/patch-global-layout.d.ts +31 -0
- package/dist/tools/patch-global-layout.js +127 -0
- package/dist/tools/patch-global-layout.js.map +1 -0
- package/dist/tools/patch-layout.d.ts +5 -8
- package/dist/tools/patch-layout.js +18 -100
- package/dist/tools/patch-layout.js.map +1 -1
- package/dist/tools/publish-draft.d.ts +5 -4
- package/dist/tools/publish-draft.js +6 -1
- package/dist/tools/publish-draft.js.map +1 -1
- package/dist/tools/publish-global-draft.d.ts +20 -0
- package/dist/tools/publish-global-draft.js +50 -0
- package/dist/tools/publish-global-draft.js.map +1 -0
- package/dist/tools/resolve-reference.d.ts +5 -4
- package/dist/tools/resolve-reference.js +4 -0
- package/dist/tools/resolve-reference.js.map +1 -1
- package/dist/tools/safe-delete.d.ts +5 -5
- package/dist/tools/safe-delete.js +20 -15
- package/dist/tools/safe-delete.js.map +1 -1
- package/dist/tools/schedule-publish.d.ts +5 -5
- package/dist/tools/schedule-publish.js +23 -19
- package/dist/tools/schedule-publish.js.map +1 -1
- package/dist/tools/search-content.d.ts +5 -9
- package/dist/tools/search-content.js +16 -12
- package/dist/tools/search-content.js.map +1 -1
- package/dist/tools/update-document.d.ts +5 -5
- package/dist/tools/update-document.js +10 -5
- package/dist/tools/update-document.js.map +1 -1
- package/dist/tools/update-global.d.ts +27 -0
- package/dist/tools/update-global.js +72 -0
- package/dist/tools/update-global.js.map +1 -0
- package/dist/tools/upload-media.d.ts +5 -4
- package/dist/tools/upload-media.js +6 -1
- package/dist/tools/upload-media.js.map +1 -1
- package/dist/tools/versions.d.ts +10 -9
- package/dist/tools/versions.js +15 -7
- package/dist/tools/versions.js.map +1 -1
- package/dist/types.d.ts +56 -3
- package/dist/types.js +13 -6
- package/dist/types.js.map +1 -1
- package/package.json +11 -4
package/dist/draft-workflow.js
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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 (
|
|
32
|
+
if (excluded.has(collection.slug)) continue;
|
|
115
33
|
// Auth-enabled collections are users — never expose them via MCP.
|
|
116
|
-
if (collection.auth)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
draftCollections.add(collection.slug);
|
|
34
|
+
if (collection.auth) {
|
|
35
|
+
excluded.add(collection.slug);
|
|
36
|
+
continue;
|
|
120
37
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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":"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"}
|
|
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[];
|
package/dist/endpoint.js
ADDED
|
@@ -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
|
package/dist/hash.js.map
ADDED
|
@@ -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
|
|
4
|
+
* payload-mcp-toolkit — standalone Payload v3 MCP plugin.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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: [
|
|
13
|
+
* plugins: [mcpToolkitPlugin()]
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
*
|
|
16
|
+
* See `ContentToolkitOptions` for the (entirely optional) escape hatches.
|
|
17
17
|
*/
|
|
18
|
-
export declare function
|
|
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';
|