@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.
Files changed (205) hide show
  1. package/bin.js +0 -7
  2. package/dist/endpoint/access.d.ts.map +1 -1
  3. package/dist/endpoint/access.js +25 -5
  4. package/dist/endpoint/access.js.map +1 -1
  5. package/dist/endpoint/index.d.ts.map +1 -1
  6. package/dist/endpoint/index.js +46 -4
  7. package/dist/endpoint/index.js.map +1 -1
  8. package/dist/endpoint/oauth.d.ts +39 -0
  9. package/dist/endpoint/oauth.d.ts.map +1 -0
  10. package/dist/endpoint/oauth.js +94 -0
  11. package/dist/endpoint/oauth.js.map +1 -0
  12. package/dist/endpoint/oauthLoginToken.d.ts +21 -0
  13. package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
  14. package/dist/endpoint/oauthLoginToken.js +79 -0
  15. package/dist/endpoint/oauthLoginToken.js.map +1 -0
  16. package/dist/endpoint/oauthServer.d.ts +12 -0
  17. package/dist/endpoint/oauthServer.d.ts.map +1 -0
  18. package/dist/endpoint/oauthServer.js +362 -0
  19. package/dist/endpoint/oauthServer.js.map +1 -0
  20. package/dist/index.d.ts +10 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/buildMcpServer.d.ts +2 -2
  25. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  26. package/dist/mcp/buildMcpServer.js +36 -24
  27. package/dist/mcp/buildMcpServer.js.map +1 -1
  28. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  29. package/dist/mcp/builtin/collections/authTools.js +80 -56
  30. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  31. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -1
  32. package/dist/mcp/builtin/collections/countTool.js +9 -14
  33. package/dist/mcp/builtin/collections/countTool.js.map +1 -1
  34. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -1
  35. package/dist/mcp/builtin/collections/countVersionsTool.js +9 -13
  36. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -1
  37. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  38. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  39. package/dist/mcp/builtin/collections/createTool.js +104 -63
  40. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  41. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  42. package/dist/mcp/builtin/collections/deleteTool.js +44 -43
  43. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  44. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  45. package/dist/mcp/builtin/collections/duplicateTool.js +31 -37
  46. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  47. package/dist/mcp/builtin/collections/fileInput.d.ts +21 -13
  48. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
  49. package/dist/mcp/builtin/collections/fileInput.js +52 -64
  50. package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
  51. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
  52. package/dist/mcp/builtin/collections/findDistinctTool.js +11 -26
  53. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
  54. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  55. package/dist/mcp/builtin/collections/findTool.js +28 -46
  56. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  57. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
  58. package/dist/mcp/builtin/collections/findVersionByIDTool.js +17 -26
  59. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
  60. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
  61. package/dist/mcp/builtin/collections/findVersionsTool.js +16 -30
  62. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
  63. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  64. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +33 -16
  65. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  66. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
  67. package/dist/mcp/builtin/collections/restoreVersionTool.js +14 -26
  68. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  69. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  70. package/dist/mcp/builtin/collections/updateTool.js +58 -90
  71. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  72. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
  73. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
  74. package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
  75. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
  76. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  77. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  78. package/dist/mcp/builtin/formatEntityError.js +55 -0
  79. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  80. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
  81. package/dist/mcp/builtin/getConfigInfoTool.js +3 -2
  82. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
  83. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
  84. package/dist/mcp/builtin/globals/countVersionsTool.js +9 -13
  85. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
  86. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  87. package/dist/mcp/builtin/globals/findTool.js +10 -16
  88. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  89. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
  90. package/dist/mcp/builtin/globals/findVersionByIDTool.js +13 -24
  91. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
  92. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
  93. package/dist/mcp/builtin/globals/findVersionsTool.js +13 -29
  94. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
  95. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  96. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +29 -13
  97. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  98. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
  99. package/dist/mcp/builtin/globals/restoreVersionTool.js +15 -22
  100. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
  101. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  102. package/dist/mcp/builtin/globals/updateTool.js +27 -36
  103. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  104. package/dist/mcp/builtinTools.d.ts +6 -0
  105. package/dist/mcp/builtinTools.d.ts.map +1 -1
  106. package/dist/mcp/builtinTools.js +9 -3
  107. package/dist/mcp/builtinTools.js.map +1 -1
  108. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  109. package/dist/mcp/sanitizeMCPConfig.js +7 -1
  110. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  111. package/dist/stdio.d.ts.map +1 -1
  112. package/dist/stdio.js +6 -4
  113. package/dist/stdio.js.map +1 -1
  114. package/dist/types.d.ts +96 -7
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/types.js.map +1 -1
  117. package/dist/utils/seal.d.ts +18 -0
  118. package/dist/utils/seal.d.ts.map +1 -0
  119. package/dist/utils/seal.js +27 -0
  120. package/dist/utils/seal.js.map +1 -0
  121. package/dist/utils/toStandardSchema.js.map +1 -1
  122. package/package.json +23 -28
  123. package/src/endpoint/access.ts +24 -6
  124. package/src/endpoint/index.ts +41 -3
  125. package/src/endpoint/oauth.ts +92 -0
  126. package/src/endpoint/oauthLoginToken.ts +91 -0
  127. package/src/endpoint/oauthServer.ts +396 -0
  128. package/src/index.ts +40 -5
  129. package/src/mcp/buildMcpServer.ts +40 -30
  130. package/src/mcp/builtin/collections/authTools.ts +82 -71
  131. package/src/mcp/builtin/collections/countTool.ts +9 -21
  132. package/src/mcp/builtin/collections/countVersionsTool.ts +9 -18
  133. package/src/mcp/builtin/collections/createTool.ts +108 -86
  134. package/src/mcp/builtin/collections/deleteTool.ts +37 -65
  135. package/src/mcp/builtin/collections/duplicateTool.ts +36 -86
  136. package/src/mcp/builtin/collections/fileInput.spec.ts +20 -6
  137. package/src/mcp/builtin/collections/fileInput.ts +64 -76
  138. package/src/mcp/builtin/collections/findDistinctTool.ts +11 -63
  139. package/src/mcp/builtin/collections/findTool.ts +24 -106
  140. package/src/mcp/builtin/collections/findVersionByIDTool.ts +15 -58
  141. package/src/mcp/builtin/collections/findVersionsTool.ts +14 -85
  142. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +28 -13
  143. package/src/mcp/builtin/collections/restoreVersionTool.ts +15 -57
  144. package/src/mcp/builtin/collections/updateTool.ts +57 -111
  145. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
  146. package/src/mcp/builtin/formatEntityError.ts +77 -0
  147. package/src/mcp/builtin/getConfigInfoTool.ts +2 -1
  148. package/src/mcp/builtin/globals/countVersionsTool.ts +9 -17
  149. package/src/mcp/builtin/globals/findTool.ts +13 -52
  150. package/src/mcp/builtin/globals/findVersionByIDTool.ts +13 -52
  151. package/src/mcp/builtin/globals/findVersionsTool.ts +14 -92
  152. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +21 -11
  153. package/src/mcp/builtin/globals/restoreVersionTool.ts +14 -45
  154. package/src/mcp/builtin/globals/updateTool.ts +38 -61
  155. package/src/mcp/builtinTools.ts +9 -2
  156. package/src/mcp/sanitizeMCPConfig.ts +5 -1
  157. package/src/stdio.ts +4 -3
  158. package/src/types.ts +98 -7
  159. package/src/utils/seal.ts +46 -0
  160. package/src/utils/toStandardSchema.ts +1 -1
  161. package/dist/mcp/builtin/collections/fileInput.spec.js +0 -72
  162. package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
  163. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
  164. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
  165. package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
  166. package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
  167. package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
  168. package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
  169. package/dist/mcp/builtin/validateEntityData.js +0 -82
  170. package/dist/mcp/builtin/validateEntityData.js.map +0 -1
  171. package/dist/utils/getVirtualFieldNames.d.ts +0 -14
  172. package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
  173. package/dist/utils/getVirtualFieldNames.js +0 -35
  174. package/dist/utils/getVirtualFieldNames.js.map +0 -1
  175. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +0 -28
  176. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +0 -1
  177. package/dist/utils/schemaConversion/filterFieldsByAccess.js +0 -71
  178. package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +0 -1
  179. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -13
  180. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
  181. package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -66
  182. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
  183. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
  184. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
  185. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
  186. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
  187. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
  188. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
  189. package/dist/utils/transformPointDataToPayload.d.ts +0 -7
  190. package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
  191. package/dist/utils/transformPointDataToPayload.js +0 -28
  192. package/dist/utils/transformPointDataToPayload.js.map +0 -1
  193. package/dist/utils/whereSchema.d.ts +0 -9
  194. package/dist/utils/whereSchema.d.ts.map +0 -1
  195. package/dist/utils/whereSchema.js +0 -13
  196. package/dist/utils/whereSchema.js.map +0 -1
  197. package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
  198. package/src/mcp/builtin/validateEntityData.ts +0 -132
  199. package/src/utils/getVirtualFieldNames.ts +0 -53
  200. package/src/utils/schemaConversion/filterFieldsByAccess.ts +0 -123
  201. package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -96
  202. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
  203. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
  204. package/src/utils/transformPointDataToPayload.ts +0 -40
  205. package/src/utils/whereSchema.ts +0 -24
@@ -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
- req.user = (await req.payload.auth({ headers, req })).user
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
- if (hasAuthorization && !req.user) {
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({ collectionSlug: item.collectionSlug, permissions, req }))
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 }))
@@ -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 })