@payloadcms/plugin-mcp 4.0.0-internal.45cfa3d → 4.0.0-internal.4a6b4c3

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 (193) hide show
  1. package/dist/defaultAccess.d.ts.map +1 -1
  2. package/dist/defineTool.d.ts +8 -8
  3. package/dist/defineTool.d.ts.map +1 -1
  4. package/dist/endpoint/access.d.ts +2 -7
  5. package/dist/endpoint/access.d.ts.map +1 -1
  6. package/dist/endpoint/access.js +21 -16
  7. package/dist/endpoint/access.js.map +1 -1
  8. package/dist/endpoint/index.d.ts.map +1 -1
  9. package/dist/endpoint/index.js +46 -4
  10. package/dist/endpoint/index.js.map +1 -1
  11. package/dist/endpoint/oauth.d.ts +35 -0
  12. package/dist/endpoint/oauth.d.ts.map +1 -0
  13. package/dist/endpoint/oauth.js +82 -0
  14. package/dist/endpoint/oauth.js.map +1 -0
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +12 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/mcp/buildMcpServer.d.ts +2 -2
  20. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  21. package/dist/mcp/buildMcpServer.js +36 -24
  22. package/dist/mcp/buildMcpServer.js.map +1 -1
  23. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  24. package/dist/mcp/builtin/collections/authTools.js +80 -56
  25. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  26. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -1
  27. package/dist/mcp/builtin/collections/countTool.js +10 -16
  28. package/dist/mcp/builtin/collections/countTool.js.map +1 -1
  29. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -1
  30. package/dist/mcp/builtin/collections/countVersionsTool.js +10 -15
  31. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -1
  32. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  33. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  34. package/dist/mcp/builtin/collections/createTool.js +104 -54
  35. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  36. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  37. package/dist/mcp/builtin/collections/deleteTool.js +45 -45
  38. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  39. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  40. package/dist/mcp/builtin/collections/duplicateTool.js +32 -39
  41. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  42. package/dist/mcp/builtin/collections/fileInput.d.ts +28 -0
  43. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -0
  44. package/dist/mcp/builtin/collections/fileInput.js +88 -0
  45. package/dist/mcp/builtin/collections/fileInput.js.map +1 -0
  46. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
  47. package/dist/mcp/builtin/collections/findDistinctTool.js +12 -28
  48. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
  49. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  50. package/dist/mcp/builtin/collections/findTool.js +29 -48
  51. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  52. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
  53. package/dist/mcp/builtin/collections/findVersionByIDTool.js +18 -28
  54. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
  55. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
  56. package/dist/mcp/builtin/collections/findVersionsTool.js +17 -32
  57. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
  58. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  59. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +68 -12
  60. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  61. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
  62. package/dist/mcp/builtin/collections/restoreVersionTool.js +15 -28
  63. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  64. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  65. package/dist/mcp/builtin/collections/updateTool.js +67 -87
  66. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  67. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
  68. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
  69. package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
  70. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
  71. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  72. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  73. package/dist/mcp/builtin/formatEntityError.js +55 -0
  74. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  75. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
  76. package/dist/mcp/builtin/getConfigInfoTool.js +5 -7
  77. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
  78. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
  79. package/dist/mcp/builtin/globals/countVersionsTool.js +10 -15
  80. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
  81. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  82. package/dist/mcp/builtin/globals/findTool.js +12 -18
  83. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  84. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
  85. package/dist/mcp/builtin/globals/findVersionByIDTool.js +14 -26
  86. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
  87. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
  88. package/dist/mcp/builtin/globals/findVersionsTool.js +14 -31
  89. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
  90. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  91. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +46 -12
  92. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  93. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
  94. package/dist/mcp/builtin/globals/restoreVersionTool.js +16 -24
  95. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
  96. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  97. package/dist/mcp/builtin/globals/updateTool.js +28 -37
  98. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  99. package/dist/mcp/builtinTools.d.ts +6 -0
  100. package/dist/mcp/builtinTools.d.ts.map +1 -1
  101. package/dist/mcp/builtinTools.js +9 -3
  102. package/dist/mcp/builtinTools.js.map +1 -1
  103. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  104. package/dist/mcp/sanitizeMCPConfig.js +5 -1
  105. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  106. package/dist/stdio.d.ts.map +1 -1
  107. package/dist/stdio.js +6 -4
  108. package/dist/stdio.js.map +1 -1
  109. package/dist/types.d.ts +95 -15
  110. package/dist/types.d.ts.map +1 -1
  111. package/dist/types.js +2 -4
  112. package/dist/types.js.map +1 -1
  113. package/dist/utils/camelCase.d.ts.map +1 -1
  114. package/dist/utils/resolveProjectRoot.d.ts.map +1 -1
  115. package/dist/utils/toStandardSchema.d.ts.map +1 -1
  116. package/package.json +6 -8
  117. package/src/endpoint/access.ts +17 -16
  118. package/src/endpoint/index.ts +41 -3
  119. package/src/endpoint/oauth.ts +84 -0
  120. package/src/index.ts +19 -2
  121. package/src/mcp/buildMcpServer.ts +40 -30
  122. package/src/mcp/builtin/collections/authTools.ts +80 -69
  123. package/src/mcp/builtin/collections/countTool.ts +10 -23
  124. package/src/mcp/builtin/collections/countVersionsTool.ts +10 -23
  125. package/src/mcp/builtin/collections/createTool.ts +110 -77
  126. package/src/mcp/builtin/collections/deleteTool.ts +36 -65
  127. package/src/mcp/builtin/collections/duplicateTool.ts +36 -87
  128. package/src/mcp/builtin/collections/fileInput.spec.ts +93 -0
  129. package/src/mcp/builtin/collections/fileInput.ts +127 -0
  130. package/src/mcp/builtin/collections/findDistinctTool.ts +12 -64
  131. package/src/mcp/builtin/collections/findTool.ts +25 -108
  132. package/src/mcp/builtin/collections/findVersionByIDTool.ts +16 -60
  133. package/src/mcp/builtin/collections/findVersionsTool.ts +15 -87
  134. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +60 -15
  135. package/src/mcp/builtin/collections/restoreVersionTool.ts +15 -58
  136. package/src/mcp/builtin/collections/updateTool.ts +63 -101
  137. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
  138. package/src/mcp/builtin/formatEntityError.ts +77 -0
  139. package/src/mcp/builtin/getConfigInfoTool.ts +4 -5
  140. package/src/mcp/builtin/globals/countVersionsTool.ts +10 -22
  141. package/src/mcp/builtin/globals/findTool.ts +14 -53
  142. package/src/mcp/builtin/globals/findVersionByIDTool.ts +14 -54
  143. package/src/mcp/builtin/globals/findVersionsTool.ts +15 -94
  144. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +41 -9
  145. package/src/mcp/builtin/globals/restoreVersionTool.ts +17 -48
  146. package/src/mcp/builtin/globals/updateTool.ts +39 -62
  147. package/src/mcp/builtinTools.ts +9 -2
  148. package/src/mcp/sanitizeMCPConfig.ts +5 -1
  149. package/src/stdio.ts +4 -3
  150. package/src/types.ts +93 -15
  151. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
  152. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
  153. package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
  154. package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
  155. package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
  156. package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
  157. package/dist/mcp/builtin/validateEntityData.js +0 -82
  158. package/dist/mcp/builtin/validateEntityData.js.map +0 -1
  159. package/dist/utils/getVirtualFieldNames.d.ts +0 -14
  160. package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
  161. package/dist/utils/getVirtualFieldNames.js +0 -35
  162. package/dist/utils/getVirtualFieldNames.js.map +0 -1
  163. package/dist/utils/localAPIDefaults.d.ts +0 -20
  164. package/dist/utils/localAPIDefaults.d.ts.map +0 -1
  165. package/dist/utils/localAPIDefaults.js +0 -19
  166. package/dist/utils/localAPIDefaults.js.map +0 -1
  167. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -11
  168. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
  169. package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -32
  170. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
  171. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
  172. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
  173. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
  174. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
  175. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
  176. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
  177. package/dist/utils/transformPointDataToPayload.d.ts +0 -7
  178. package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
  179. package/dist/utils/transformPointDataToPayload.js +0 -28
  180. package/dist/utils/transformPointDataToPayload.js.map +0 -1
  181. package/dist/utils/whereSchema.d.ts +0 -9
  182. package/dist/utils/whereSchema.d.ts.map +0 -1
  183. package/dist/utils/whereSchema.js +0 -13
  184. package/dist/utils/whereSchema.js.map +0 -1
  185. package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
  186. package/src/mcp/builtin/validateEntityData.ts +0 -132
  187. package/src/utils/getVirtualFieldNames.ts +0 -53
  188. package/src/utils/localAPIDefaults.ts +0 -22
  189. package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -62
  190. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
  191. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
  192. package/src/utils/transformPointDataToPayload.ts +0 -40
  193. package/src/utils/whereSchema.ts +0 -24
@@ -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,84 @@
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
+ bearerAuthChallengeResponse(
44
+ new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')),
45
+ {
46
+ requiredScopes: getPluginConfig({ config: req.payload.config }).oauth?.scopes,
47
+ resourceMetadataUrl: `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth-protected-resource`,
48
+ },
49
+ )
50
+
51
+ /**
52
+ * The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when
53
+ * they sign in, and authorization servers that support it put it in the access token's audience.
54
+ */
55
+ export const getMCPResource = ({ config, url }: { config: SanitizedConfig; url: string }): string =>
56
+ getPluginConfig({ config }).oauth?.resource ?? getMCPURL({ config, url })
57
+
58
+ const getMCPURL = ({ config, url }: { config: SanitizedConfig; url: string }): string =>
59
+ new URL(`${config.routes.api}/mcp`, config.serverURL || url).href
60
+
61
+ const getMetadataResponse = async ({ req }: { req: PayloadRequest }): Promise<Response> => {
62
+ const { config } = req.payload
63
+ const pluginConfig = getPluginConfig({ config })
64
+
65
+ if (!pluginConfig.oauth) {
66
+ return new Response(null, { status: 404 })
67
+ }
68
+
69
+ const { authorizationServers, scopes } = pluginConfig.oauth
70
+
71
+ return Response.json(
72
+ {
73
+ authorization_servers:
74
+ typeof authorizationServers === 'function'
75
+ ? await authorizationServers({ req })
76
+ : authorizationServers,
77
+ bearer_methods_supported: ['header'],
78
+ resource: getMCPResource({ config, url: req.url! }),
79
+ scopes_supported: scopes,
80
+ } satisfies OAuthProtectedResourceMetadata,
81
+ // Public document, readable by browser-based MCP clients on any origin
82
+ { headers: { 'Access-Control-Allow-Origin': '*' } },
83
+ )
84
+ }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { definePlugin } from 'payload'
3
3
  import type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'
4
4
 
5
5
  import { mcpEndpoint } from './endpoint/index.js'
6
+ import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js'
6
7
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'
7
8
 
8
9
  declare module 'payload' {
@@ -13,11 +14,19 @@ declare module 'payload' {
13
14
  /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */
14
15
  '@payloadcms/plugin-mcp': MCPPluginConfig
15
16
  }
17
+ export interface RequestContext {
18
+ /** Set on MCP requests before they're authenticated. */
19
+ mcp?: {
20
+ /** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */
21
+ resource: string
22
+ }
23
+ }
16
24
  }
17
25
 
18
26
  export type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig }
19
27
 
20
28
  export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'
29
+ export { oauthProtectedResourceHandler } from './endpoint/oauth.js'
21
30
 
22
31
  export const mcpPlugin = definePlugin<MCPPluginConfig>({
23
32
  slug: '@payloadcms/plugin-mcp',
@@ -40,15 +49,23 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
40
49
  ...config,
41
50
  endpoints: [
42
51
  ...(config.endpoints ?? []),
43
- // Payload prefixes /api, so the full path is /api/mcp.
44
- { handler: mcpEndpoint, method: 'post', path: '/mcp' },
52
+ // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
53
+ // request with dev auto-login turned off, so Payload doesn't authenticate it first.
54
+ { disableAuth: true, handler: mcpEndpoint, method: 'post', path: '/mcp' },
45
55
  // Streamable HTTP's optional server=>client GET stream. We don't offer
46
56
  // one, so answer 405 per spec-— clients then skip it cleanly
47
57
  {
58
+ disableAuth: true,
48
59
  handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),
49
60
  method: 'get',
50
61
  path: '/mcp',
51
62
  },
63
+ {
64
+ disableAuth: true,
65
+ handler: oauthProtectedResourceEndpoint,
66
+ method: 'get',
67
+ path: '/mcp/oauth-protected-resource',
68
+ },
52
69
  ],
53
70
  }
54
71
  },
@@ -1,6 +1,7 @@
1
+ import type { PayloadRequest } from 'payload'
2
+
1
3
  import { McpServer, type ServerContext } from '@modelcontextprotocol/server'
2
- import { APIError, type PayloadRequest } from 'payload'
3
- import { z } from 'zod'
4
+ import { APIError, strictObject, z } from 'payload'
4
5
 
5
6
  import type {
6
7
  AuthorizedMCP,
@@ -24,7 +25,7 @@ import { toStandardSchema } from '../utils/toStandardSchema.js'
24
25
  *
25
26
  * `req` is the request context handlers see. For HTTP it's the live
26
27
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
27
- * synthesized one built via `createLocalReq`.
28
+ * synthesized one built via `createPayloadRequest`.
28
29
  */
29
30
  export const buildMcpServer = ({
30
31
  authorizedMCP,
@@ -35,6 +36,8 @@ export const buildMcpServer = ({
35
36
  pluginConfig: SanitizedMCPPluginConfig
36
37
  req: PayloadRequest
37
38
  }): McpServer => {
39
+ z.config(z.locales.en())
40
+
38
41
  const serverOptions = pluginConfig.mcp?.serverOptions || {}
39
42
  const server = new McpServer(
40
43
  { name: 'Payload MCP Server', version: '1.0.0', ...serverOptions.serverInfo },
@@ -68,7 +71,7 @@ export const buildMcpServer = ({
68
71
 
69
72
  /**
70
73
  * Runs a collection/global tool call:
71
- * - reads `collectionSlug` / `globalSlug` from the input
74
+ * - reads `slug` from the input
72
75
  * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug
73
76
  * - runs the tool handler and finalizes its response
74
77
  */
@@ -82,16 +85,15 @@ export const buildMcpServer = ({
82
85
  serverContext: ServerContext
83
86
  }): Promise<MCPToolResponse> => {
84
87
  const entity = item.type === 'collectionTool' ? 'collection' : 'global'
85
- const slugKey = item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug'
86
88
  const toolInput = (input ?? {}) as Record<string, unknown>
87
- const slug = toolInput[slugKey] as string | undefined
89
+ const slug = toolInput.slug as string | undefined
88
90
 
89
91
  if (!slug) {
90
92
  return {
91
93
  content: [
92
94
  {
93
95
  type: 'text',
94
- text: `Error: "${item.mcpName}" requires ${slugKey}. Use getConfigInfo to inspect ${entity} slugs.`,
96
+ text: `Error: "${item.mcpName}" requires slug. Use getConfigInfo to inspect ${entity} slugs.`,
95
97
  },
96
98
  ],
97
99
  isError: true,
@@ -126,8 +128,8 @@ export const buildMcpServer = ({
126
128
  serverContext,
127
129
  }
128
130
  const response = await (match.type === 'collectionTool'
129
- ? match.tool.handler({ ...handlerArgs, collectionSlug: slug })
130
- : match.tool.handler({ ...handlerArgs, globalSlug: slug }))
131
+ ? match.tool.handler({ ...handlerArgs, slug })
132
+ : match.tool.handler({ ...handlerArgs, slug }))
131
133
 
132
134
  return finalizeToolResponse({
133
135
  input: toolInput,
@@ -149,10 +151,7 @@ export const buildMcpServer = ({
149
151
  }
150
152
  registeredEntityTools.add(item.mcpName)
151
153
 
152
- const inputSchema = withSlugInput({
153
- name: item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug',
154
- input: item.tool.input,
155
- })
154
+ const inputSchema = withSlugInput({ input: item.tool.input })
156
155
 
157
156
  server.registerTool(
158
157
  item.mcpName,
@@ -246,39 +245,50 @@ export const buildMcpServer = ({
246
245
  return server
247
246
  }
248
247
 
249
- const withSlugInput = ({
250
- name,
251
- input,
252
- }: {
253
- input?: ToolInputSchema
254
- name: 'collectionSlug' | 'globalSlug'
255
- }): ToolInputSchema => {
256
- const description = name === 'collectionSlug' ? 'The collection slug' : 'The global slug'
257
- const slugSchema = z.string().describe(description)
248
+ const withSlugInput = ({ input }: { input?: ToolInputSchema }): ToolInputSchema => {
249
+ const description = 'The target slug.'
250
+ const slugSchema = z.string().check(z.describe(description))
258
251
 
259
252
  if (!input) {
260
- return z.object({ [name]: slugSchema })
253
+ return strictObject({ slug: slugSchema }) as unknown as ToolInputSchema
261
254
  }
262
255
 
263
- if (input instanceof z.ZodObject) {
264
- return input.extend({ [name]: slugSchema })
256
+ if (typeof input === 'object' && input !== null && '~standard' in input) {
257
+ const schema = input['~standard'].jsonSchema.input({ target: 'draft-2020-12' })
258
+ const properties = schema.properties as Record<string, unknown> | undefined
259
+
260
+ if (properties?.slug) {
261
+ return input
262
+ }
263
+
264
+ return addSlugToJSONSchema({ description, schema: schema as JsonSchemaType })
265
265
  }
266
266
 
267
- const schema = input as {
267
+ return addSlugToJSONSchema({ description, schema: input })
268
+ }
269
+
270
+ const addSlugToJSONSchema = ({
271
+ description,
272
+ schema,
273
+ }: {
274
+ description: string
275
+ schema: JsonSchemaType
276
+ }): JsonSchemaType => {
277
+ const objectSchema = schema as {
268
278
  properties?: Record<string, JsonSchemaType>
269
279
  required?: string[]
270
280
  } & JsonSchemaType
271
281
 
272
282
  return {
273
- ...schema,
283
+ ...objectSchema,
274
284
  type: 'object',
275
285
  properties: {
276
- ...schema.properties,
277
- [name]: {
286
+ ...objectSchema.properties,
287
+ slug: {
278
288
  type: 'string',
279
289
  description,
280
290
  },
281
291
  },
282
- required: Array.from(new Set([name, ...(schema.required ?? [])])),
292
+ required: Array.from(new Set(['slug', ...(objectSchema.required ?? [])])),
283
293
  }
284
294
  }