@payloadcms/plugin-mcp 4.0.0-internal.21664f3 → 4.0.0-internal.22d1431
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/bin.js +0 -7
- package/dist/endpoint/access.d.ts.map +1 -1
- package/dist/endpoint/access.js +25 -5
- package/dist/endpoint/access.js.map +1 -1
- package/dist/endpoint/index.d.ts.map +1 -1
- package/dist/endpoint/index.js +46 -4
- package/dist/endpoint/index.js.map +1 -1
- package/dist/endpoint/oauth.d.ts +39 -0
- package/dist/endpoint/oauth.d.ts.map +1 -0
- package/dist/endpoint/oauth.js +94 -0
- package/dist/endpoint/oauth.js.map +1 -0
- package/dist/endpoint/oauthLoginToken.d.ts +21 -0
- package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
- package/dist/endpoint/oauthLoginToken.js +79 -0
- package/dist/endpoint/oauthLoginToken.js.map +1 -0
- package/dist/endpoint/oauthServer.d.ts +12 -0
- package/dist/endpoint/oauthServer.d.ts.map +1 -0
- package/dist/endpoint/oauthServer.js +362 -0
- package/dist/endpoint/oauthServer.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/buildMcpServer.d.ts +2 -2
- package/dist/mcp/buildMcpServer.d.ts.map +1 -1
- package/dist/mcp/buildMcpServer.js +36 -24
- package/dist/mcp/buildMcpServer.js.map +1 -1
- package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/authTools.js +80 -56
- package/dist/mcp/builtin/collections/authTools.js.map +1 -1
- package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/countTool.js +9 -14
- package/dist/mcp/builtin/collections/countTool.js.map +1 -1
- package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/countVersionsTool.js +9 -13
- package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
- package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/createTool.js +104 -63
- package/dist/mcp/builtin/collections/createTool.js.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.js +44 -43
- package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.js +31 -37
- package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.d.ts +21 -13
- package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.js +52 -64
- package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
- package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findDistinctTool.js +11 -26
- package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findTool.js +28 -46
- package/dist/mcp/builtin/collections/findTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findVersionByIDTool.js +17 -26
- package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findVersionsTool.js +16 -30
- package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +33 -16
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/restoreVersionTool.js +14 -26
- package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.js +58 -90
- package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
- package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
- package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
- package/dist/mcp/builtin/formatEntityError.js +55 -0
- package/dist/mcp/builtin/formatEntityError.js.map +1 -0
- package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
- package/dist/mcp/builtin/getConfigInfoTool.js +3 -2
- package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
- package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/countVersionsTool.js +9 -13
- package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findTool.js +10 -16
- package/dist/mcp/builtin/globals/findTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findVersionByIDTool.js +13 -24
- package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findVersionsTool.js +13 -29
- package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +29 -13
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/restoreVersionTool.js +15 -22
- package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.js +27 -36
- package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
- package/dist/mcp/builtinTools.d.ts +6 -0
- package/dist/mcp/builtinTools.d.ts.map +1 -1
- package/dist/mcp/builtinTools.js +9 -3
- package/dist/mcp/builtinTools.js.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.js +7 -1
- package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +6 -4
- package/dist/stdio.js.map +1 -1
- package/dist/types.d.ts +96 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/seal.d.ts +18 -0
- package/dist/utils/seal.d.ts.map +1 -0
- package/dist/utils/seal.js +27 -0
- package/dist/utils/seal.js.map +1 -0
- package/dist/utils/toStandardSchema.js.map +1 -1
- package/package.json +23 -28
- package/src/endpoint/access.ts +24 -6
- package/src/endpoint/index.ts +41 -3
- package/src/endpoint/oauth.ts +92 -0
- package/src/endpoint/oauthLoginToken.ts +91 -0
- package/src/endpoint/oauthServer.ts +396 -0
- package/src/index.ts +40 -5
- package/src/mcp/buildMcpServer.ts +40 -30
- package/src/mcp/builtin/collections/authTools.ts +82 -71
- package/src/mcp/builtin/collections/countTool.ts +9 -21
- package/src/mcp/builtin/collections/countVersionsTool.ts +9 -18
- package/src/mcp/builtin/collections/createTool.ts +108 -86
- package/src/mcp/builtin/collections/deleteTool.ts +37 -65
- package/src/mcp/builtin/collections/duplicateTool.ts +36 -86
- package/src/mcp/builtin/collections/fileInput.spec.ts +20 -6
- package/src/mcp/builtin/collections/fileInput.ts +64 -76
- package/src/mcp/builtin/collections/findDistinctTool.ts +11 -63
- package/src/mcp/builtin/collections/findTool.ts +24 -106
- package/src/mcp/builtin/collections/findVersionByIDTool.ts +15 -58
- package/src/mcp/builtin/collections/findVersionsTool.ts +14 -85
- package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +28 -13
- package/src/mcp/builtin/collections/restoreVersionTool.ts +15 -57
- package/src/mcp/builtin/collections/updateTool.ts +57 -111
- package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
- package/src/mcp/builtin/formatEntityError.ts +77 -0
- package/src/mcp/builtin/getConfigInfoTool.ts +2 -1
- package/src/mcp/builtin/globals/countVersionsTool.ts +9 -17
- package/src/mcp/builtin/globals/findTool.ts +13 -52
- package/src/mcp/builtin/globals/findVersionByIDTool.ts +13 -52
- package/src/mcp/builtin/globals/findVersionsTool.ts +14 -92
- package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +21 -11
- package/src/mcp/builtin/globals/restoreVersionTool.ts +14 -45
- package/src/mcp/builtin/globals/updateTool.ts +38 -61
- package/src/mcp/builtinTools.ts +9 -2
- package/src/mcp/sanitizeMCPConfig.ts +5 -1
- package/src/stdio.ts +4 -3
- package/src/types.ts +98 -7
- package/src/utils/seal.ts +46 -0
- package/src/utils/toStandardSchema.ts +1 -1
- package/dist/mcp/builtin/collections/fileInput.spec.js +0 -72
- package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
- package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
- package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
- package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
- package/dist/mcp/builtin/validateEntityData.js +0 -82
- package/dist/mcp/builtin/validateEntityData.js.map +0 -1
- package/dist/utils/getVirtualFieldNames.d.ts +0 -14
- package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
- package/dist/utils/getVirtualFieldNames.js +0 -35
- package/dist/utils/getVirtualFieldNames.js.map +0 -1
- package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +0 -28
- package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +0 -1
- package/dist/utils/schemaConversion/filterFieldsByAccess.js +0 -71
- package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +0 -1
- package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -13
- package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -66
- package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
- package/dist/utils/transformPointDataToPayload.d.ts +0 -7
- package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
- package/dist/utils/transformPointDataToPayload.js +0 -28
- package/dist/utils/transformPointDataToPayload.js.map +0 -1
- package/dist/utils/whereSchema.d.ts +0 -9
- package/dist/utils/whereSchema.d.ts.map +0 -1
- package/dist/utils/whereSchema.js +0 -13
- package/dist/utils/whereSchema.js.map +0 -1
- package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
- package/src/mcp/builtin/validateEntityData.ts +0 -132
- package/src/utils/getVirtualFieldNames.ts +0 -53
- package/src/utils/schemaConversion/filterFieldsByAccess.ts +0 -123
- package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -96
- package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
- package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
- package/src/utils/transformPointDataToPayload.ts +0 -40
- package/src/utils/whereSchema.ts +0 -24
package/src/endpoint/access.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { PayloadRequest, SanitizedPermissions } from 'payload'
|
|
2
2
|
|
|
3
|
-
import { getAccessResults, UnauthorizedError } from 'payload'
|
|
3
|
+
import { executeAuthStrategies, getAccessResults, UnauthorizedError } from 'payload'
|
|
4
4
|
|
|
5
5
|
import type { AuthorizedMCP, MCPItem } from '../types.js'
|
|
6
6
|
|
|
7
7
|
import { getPluginConfig } from '../utils/getPluginConfig.js'
|
|
8
|
+
import { authenticateLoginToken } from './oauthLoginToken.js'
|
|
8
9
|
|
|
9
10
|
export type GetAuthorizedMCPArgs = {
|
|
10
11
|
overrideAccess: boolean
|
|
@@ -34,9 +35,27 @@ export const getAuthorizedMCP: (args: GetAuthorizedMCPArgs) => Promise<Authorize
|
|
|
34
35
|
const hasAuthorization = headers.has('Authorization')
|
|
35
36
|
|
|
36
37
|
headers.set('DisableAutologin', 'true')
|
|
37
|
-
|
|
38
|
+
// Not `payload.auth()`, which also computes permissions that filterMCPItems computes anyway
|
|
39
|
+
req.user = (await executeAuthStrategies({ headers, payload: req.payload, req })).user
|
|
40
|
+
|
|
41
|
+
// Only bearer tokens that no auth strategy accepted get here, e.g. an OAuth access token.
|
|
42
|
+
// API keys use the `<collection> API-Key <key>` scheme instead of `Bearer`, so they never do.
|
|
43
|
+
const bearerToken = headers.get('Authorization')?.match(/^Bearer (.+)$/)?.[1]
|
|
44
|
+
// Sealed Payload login tokens from the built-in OAuth server
|
|
45
|
+
if (
|
|
46
|
+
!req.user &&
|
|
47
|
+
bearerToken &&
|
|
48
|
+
pluginConfig.oauth?.type === 'builtin' &&
|
|
49
|
+
!pluginConfig.oauth.issueToken
|
|
50
|
+
) {
|
|
51
|
+
req.user = await authenticateLoginToken({ req, token: bearerToken })
|
|
52
|
+
}
|
|
53
|
+
if (!req.user && bearerToken && pluginConfig.oauth?.authenticate) {
|
|
54
|
+
req.user = await pluginConfig.oauth.authenticate({ req, token: bearerToken })
|
|
55
|
+
}
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
// With OAuth, the endpoint answers with a challenge that starts the client's sign-in
|
|
58
|
+
if (hasAuthorization && !req.user && !pluginConfig.oauth) {
|
|
40
59
|
throw new UnauthorizedError(req.t)
|
|
41
60
|
}
|
|
42
61
|
}
|
|
@@ -95,12 +114,11 @@ const checkItemAccess = async ({
|
|
|
95
114
|
case 'collectionTool':
|
|
96
115
|
return (
|
|
97
116
|
!item.tool.access ||
|
|
98
|
-
(await item.tool.access({
|
|
117
|
+
(await item.tool.access({ slug: item.collectionSlug, permissions, req }))
|
|
99
118
|
)
|
|
100
119
|
case 'globalTool':
|
|
101
120
|
return (
|
|
102
|
-
!item.tool.access ||
|
|
103
|
-
(await item.tool.access({ globalSlug: item.globalSlug, permissions, req }))
|
|
121
|
+
!item.tool.access || (await item.tool.access({ slug: item.globalSlug, permissions, req }))
|
|
104
122
|
)
|
|
105
123
|
case 'prompt':
|
|
106
124
|
return !item.prompt.access || (await item.prompt.access({ permissions, req }))
|
package/src/endpoint/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PayloadHandler, PayloadRequest } from 'payload'
|
|
|
3
3
|
import {
|
|
4
4
|
createMcpHandler,
|
|
5
5
|
isLegacyRequest,
|
|
6
|
+
readRequestBody,
|
|
6
7
|
WebStandardStreamableHTTPServerTransport,
|
|
7
8
|
} from '@modelcontextprotocol/server'
|
|
8
9
|
import { APIError } from 'payload'
|
|
@@ -10,6 +11,7 @@ import { APIError } from 'payload'
|
|
|
10
11
|
import { buildMcpServer } from '../mcp/buildMcpServer.js'
|
|
11
12
|
import { getPluginConfig } from '../utils/getPluginConfig.js'
|
|
12
13
|
import { getAuthorizedMCP } from './access.js'
|
|
14
|
+
import { getMCPResource, oauthChallenge } from './oauth.js'
|
|
13
15
|
|
|
14
16
|
export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
15
17
|
if (!req.url) {
|
|
@@ -17,6 +19,8 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
req.payloadAPI = 'MCP' as const
|
|
22
|
+
// Lets auth strategies check that an OAuth access token was issued for this MCP server
|
|
23
|
+
req.context.mcp = { resource: getMCPResource({ config: req.payload.config, url: req.url }) }
|
|
20
24
|
|
|
21
25
|
const pluginConfig = getPluginConfig({ config: req.payload.config })
|
|
22
26
|
const overrideAccessParam = new URL(req.url).searchParams.get('overrideAccess')
|
|
@@ -33,14 +37,22 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })
|
|
40
|
+
|
|
41
|
+
if (pluginConfig.oauth && !req.user) {
|
|
42
|
+
return oauthChallenge({ req })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const maxRequestBodySize = pluginConfig.mcp?.maxRequestBodySize
|
|
36
46
|
// Payload augments the original web-standard Request in place.
|
|
37
47
|
const mcpRequest = req as PayloadRequest & Request
|
|
48
|
+
const parsedBody = await parseRequestBody({ maxRequestBodySize, request: mcpRequest })
|
|
38
49
|
|
|
39
50
|
// Keep the old JSON-only, stateless behavior because the SDK's 2025 fallback uses SSE.
|
|
40
|
-
if (await isLegacyRequest(mcpRequest)) {
|
|
51
|
+
if (await isLegacyRequest(mcpRequest, parsedBody, { maxRequestBodySize })) {
|
|
41
52
|
const server = buildMcpServer({ authorizedMCP, pluginConfig, req })
|
|
42
53
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
43
54
|
enableJsonResponse: true,
|
|
55
|
+
maxRequestBodySize,
|
|
44
56
|
sessionIdGenerator: undefined, // stateless mode
|
|
45
57
|
})
|
|
46
58
|
transport.onerror = (err) => {
|
|
@@ -49,7 +61,7 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
|
49
61
|
|
|
50
62
|
try {
|
|
51
63
|
await server.connect(transport)
|
|
52
|
-
return await transport.handleRequest(mcpRequest)
|
|
64
|
+
return await transport.handleRequest(mcpRequest, { parsedBody })
|
|
53
65
|
} finally {
|
|
54
66
|
await server.close().catch((err) => {
|
|
55
67
|
req.payload.logger.error({ err, msg: 'Error closing MCP server' })
|
|
@@ -59,6 +71,7 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
|
59
71
|
|
|
60
72
|
const handler = createMcpHandler(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {
|
|
61
73
|
legacy: 'reject',
|
|
74
|
+
maxRequestBodySize,
|
|
62
75
|
// SDK subscriptions always use SSE, so disable them to keep every response JSON-only.
|
|
63
76
|
maxSubscriptions: 0,
|
|
64
77
|
onerror: (err) => {
|
|
@@ -68,10 +81,35 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
|
|
|
68
81
|
})
|
|
69
82
|
|
|
70
83
|
try {
|
|
71
|
-
return await handler.fetch(mcpRequest)
|
|
84
|
+
return await handler.fetch(mcpRequest, { parsedBody })
|
|
72
85
|
} finally {
|
|
73
86
|
await handler.close().catch((err) => {
|
|
74
87
|
req.payload.logger.error({ err, msg: 'Error closing modern MCP handler' })
|
|
75
88
|
})
|
|
76
89
|
}
|
|
77
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parses a POST body once so the era check and the SDK handler can share it. The SDK
|
|
94
|
+
* skips its size limit for a pre-parsed body, so this reads with the same limit. It reads
|
|
95
|
+
* a clone and returns `undefined` for any body the SDK should reject (too large, unreadable,
|
|
96
|
+
* or not JSON), leaving the original request for the SDK to read and answer.
|
|
97
|
+
*/
|
|
98
|
+
const parseRequestBody = async ({
|
|
99
|
+
maxRequestBodySize,
|
|
100
|
+
request,
|
|
101
|
+
}: {
|
|
102
|
+
maxRequestBodySize?: number
|
|
103
|
+
request: Request
|
|
104
|
+
}): Promise<unknown> => {
|
|
105
|
+
if (request.method.toUpperCase() !== 'POST') {
|
|
106
|
+
return undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const body = await readRequestBody(request.clone(), maxRequestBodySize)
|
|
111
|
+
return body.tooLarge ? undefined : JSON.parse(body.text)
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server'
|
|
2
|
+
import type { PayloadHandler, PayloadRequest, SanitizedConfig } from 'payload'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
bearerAuthChallengeResponse,
|
|
6
|
+
OAuthError,
|
|
7
|
+
OAuthErrorCode,
|
|
8
|
+
} from '@modelcontextprotocol/server'
|
|
9
|
+
import { createPayloadRequest, getPayload } from 'payload'
|
|
10
|
+
|
|
11
|
+
import { getPluginConfig } from '../utils/getPluginConfig.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Serves the OAuth protected resource metadata (RFC 9728) at `/api/mcp/oauth-protected-resource`.
|
|
15
|
+
* The OAuth challenge links here, so MCP clients find the authorization server without setup.
|
|
16
|
+
*/
|
|
17
|
+
export const oauthProtectedResourceEndpoint: PayloadHandler = (req) => getMetadataResponse({ req })
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Serves the same metadata for clients that ignore the challenge's link and only look at
|
|
21
|
+
* `/.well-known/oauth-protected-resource`. Mount it at the site root, e.g. in Next.js at
|
|
22
|
+
* `app/.well-known/oauth-protected-resource/[[...path]]/route.ts`:
|
|
23
|
+
*
|
|
24
|
+
* ```ts
|
|
25
|
+
* import config from '@payload-config'
|
|
26
|
+
* import { oauthProtectedResourceHandler } from '@payloadcms/plugin-mcp'
|
|
27
|
+
*
|
|
28
|
+
* export const GET = oauthProtectedResourceHandler(config)
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const oauthProtectedResourceHandler =
|
|
32
|
+
(config: Promise<SanitizedConfig> | SanitizedConfig) =>
|
|
33
|
+
async (request: Request): Promise<Response> =>
|
|
34
|
+
getMetadataResponse({
|
|
35
|
+
req: await createPayloadRequest({ payload: await getPayload({ config }), req: request }),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Answers an unauthenticated MCP request with a challenge linking to the protected resource
|
|
40
|
+
* metadata, which starts the client's OAuth flow.
|
|
41
|
+
*/
|
|
42
|
+
export const oauthChallenge = ({ req }: { req: PayloadRequest }): Response => {
|
|
43
|
+
const { oauth } = getPluginConfig({ config: req.payload.config })
|
|
44
|
+
const response = bearerAuthChallengeResponse(
|
|
45
|
+
new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')),
|
|
46
|
+
{
|
|
47
|
+
requiredScopes: oauth?.type === 'external' ? oauth.scopes : undefined,
|
|
48
|
+
resourceMetadataUrl: `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth-protected-resource`,
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Lets browser-based MCP clients read the challenge, when Payload's `cors` setting allows them
|
|
53
|
+
response.headers.set('Access-Control-Expose-Headers', 'WWW-Authenticate')
|
|
54
|
+
|
|
55
|
+
return response
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when
|
|
60
|
+
* they sign in, and authorization servers that support it put it in the access token's audience.
|
|
61
|
+
*/
|
|
62
|
+
export const getMCPResource = ({ config, url }: { config: SanitizedConfig; url: string }): string =>
|
|
63
|
+
getPluginConfig({ config }).oauth?.resource ?? getMCPURL({ config, url })
|
|
64
|
+
|
|
65
|
+
export const getMCPURL = ({ config, url }: { config: SanitizedConfig; url: string }): string =>
|
|
66
|
+
new URL(`${config.routes.api}/mcp`, config.serverURL || url).href
|
|
67
|
+
|
|
68
|
+
const getMetadataResponse = async ({ req }: { req: PayloadRequest }): Promise<Response> => {
|
|
69
|
+
const { config } = req.payload
|
|
70
|
+
const { oauth } = getPluginConfig({ config })
|
|
71
|
+
|
|
72
|
+
if (!oauth) {
|
|
73
|
+
return new Response(null, { status: 404 })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Response.json(
|
|
77
|
+
{
|
|
78
|
+
authorization_servers:
|
|
79
|
+
oauth.type === 'builtin'
|
|
80
|
+
? [`${getMCPURL({ config, url: req.url! })}/oauth`]
|
|
81
|
+
: typeof oauth.authorizationServers === 'function'
|
|
82
|
+
? await oauth.authorizationServers({ req })
|
|
83
|
+
: oauth.authorizationServers,
|
|
84
|
+
bearer_methods_supported: ['header'],
|
|
85
|
+
resource: getMCPResource({ config, url: req.url! }),
|
|
86
|
+
scopes_supported: oauth.type === 'external' ? oauth.scopes : undefined,
|
|
87
|
+
} satisfies OAuthProtectedResourceMetadata,
|
|
88
|
+
// Public document, readable by browser-based MCP clients on any origin. Not cached, since
|
|
89
|
+
// without `serverURL` its URLs come from the request.
|
|
90
|
+
{ headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' } },
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { APIError, createAuthToken, executeAuthStrategies, refreshAuthToken } from 'payload'
|
|
4
|
+
|
|
5
|
+
import type { MCPOAuthTokens } from '../types.js'
|
|
6
|
+
|
|
7
|
+
import { seal, unseal } from '../utils/seal.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The default `issueToken`: a Payload login token, sealed so it only works for MCP, where
|
|
11
|
+
* `authenticateLoginToken` unseals it. Otherwise an MCP client could use it as the user's admin
|
|
12
|
+
* panel login. When the collection uses sessions, the client also gets a refresh token.
|
|
13
|
+
*/
|
|
14
|
+
export const issueLoginToken = async ({
|
|
15
|
+
req,
|
|
16
|
+
}: {
|
|
17
|
+
req: PayloadRequest
|
|
18
|
+
}): Promise<MCPOAuthTokens> => {
|
|
19
|
+
const collectionConfig = req.payload.collections[req.user!.collection]!.config
|
|
20
|
+
|
|
21
|
+
if (collectionConfig.auth.disableLocalStrategy) {
|
|
22
|
+
throw new APIError(
|
|
23
|
+
`MCP clients can't get Payload login tokens for the "${collectionConfig.slug}" collection because its local strategy is disabled. Pass oauth.issueToken to the MCP plugin.`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The full document, like the one a password login starts its session on
|
|
28
|
+
const user = (await req.payload.db.findOne<NonNullable<PayloadRequest['user']>>({
|
|
29
|
+
collection: collectionConfig.slug,
|
|
30
|
+
req,
|
|
31
|
+
where: { id: { equals: req.user!.id } },
|
|
32
|
+
}))!
|
|
33
|
+
|
|
34
|
+
const { refreshToken, token } = await createAuthToken({
|
|
35
|
+
collectionConfig,
|
|
36
|
+
email: user.email!,
|
|
37
|
+
req,
|
|
38
|
+
shouldCreateRefreshToken: true,
|
|
39
|
+
user,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
accessToken: sealLoginToken({ req, token }),
|
|
44
|
+
expiresIn: collectionConfig.auth.tokenExpiration,
|
|
45
|
+
refreshToken,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The default `refreshToken`. Payload logs out the session when a refresh token is reused. */
|
|
50
|
+
export const refreshLoginToken = async ({
|
|
51
|
+
refreshToken,
|
|
52
|
+
req,
|
|
53
|
+
}: {
|
|
54
|
+
refreshToken: string
|
|
55
|
+
req: PayloadRequest
|
|
56
|
+
}): Promise<MCPOAuthTokens | null> => {
|
|
57
|
+
const refreshed = await refreshAuthToken({ refreshToken, req })
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
refreshed && {
|
|
61
|
+
accessToken: sealLoginToken({ req, token: refreshed.token }),
|
|
62
|
+
expiresIn: refreshed.collectionConfig.auth.tokenExpiration,
|
|
63
|
+
refreshToken: refreshed.refreshToken,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Finds the user of an MCP request's access token from `issueLoginToken`. */
|
|
69
|
+
export const authenticateLoginToken = async ({
|
|
70
|
+
req,
|
|
71
|
+
token,
|
|
72
|
+
}: {
|
|
73
|
+
req: PayloadRequest
|
|
74
|
+
token: string
|
|
75
|
+
}): Promise<PayloadRequest['user']> => {
|
|
76
|
+
const loginToken = unseal<{ token: string }>({ type: 'access', req, value: token })
|
|
77
|
+
|
|
78
|
+
if (!loginToken) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const headers = new Headers({
|
|
83
|
+
Authorization: `JWT ${loginToken.token}`,
|
|
84
|
+
DisableAutologin: 'true',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return (await executeAuthStrategies({ headers, payload: req.payload, req })).user
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const sealLoginToken = ({ req, token }: { req: PayloadRequest; token: string }): string =>
|
|
91
|
+
seal({ type: 'access', data: { token }, req })
|