@payloadcms/plugin-mcp 4.0.0-internal.a0ef1b8 → 4.0.0-internal.a2d053c

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 (259) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/defaultAccess.d.ts +3 -0
  3. package/dist/defaultAccess.d.ts.map +1 -0
  4. package/dist/defaultAccess.js +3 -0
  5. package/dist/defaultAccess.js.map +1 -0
  6. package/dist/defineTool.d.ts +8 -8
  7. package/dist/defineTool.d.ts.map +1 -1
  8. package/dist/endpoint/access.d.ts +12 -5
  9. package/dist/endpoint/access.d.ts.map +1 -1
  10. package/dist/endpoint/access.js +95 -88
  11. package/dist/endpoint/access.js.map +1 -1
  12. package/dist/endpoint/index.d.ts +1 -1
  13. package/dist/endpoint/index.d.ts.map +1 -1
  14. package/dist/endpoint/index.js +109 -16
  15. package/dist/endpoint/index.js.map +1 -1
  16. package/dist/endpoint/oauth.d.ts +39 -0
  17. package/dist/endpoint/oauth.d.ts.map +1 -0
  18. package/dist/endpoint/oauth.js +94 -0
  19. package/dist/endpoint/oauth.js.map +1 -0
  20. package/dist/endpoint/oauthLoginToken.d.ts +21 -0
  21. package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
  22. package/dist/endpoint/oauthLoginToken.js +79 -0
  23. package/dist/endpoint/oauthLoginToken.js.map +1 -0
  24. package/dist/endpoint/oauthServer.d.ts +12 -0
  25. package/dist/endpoint/oauthServer.d.ts.map +1 -0
  26. package/dist/endpoint/oauthServer.js +362 -0
  27. package/dist/endpoint/oauthServer.js.map +1 -0
  28. package/dist/exports/client.d.ts +1 -1
  29. package/dist/exports/client.d.ts.map +1 -1
  30. package/dist/exports/client.js +1 -1
  31. package/dist/exports/client.js.map +1 -1
  32. package/dist/exports/internal.d.ts +2 -0
  33. package/dist/exports/internal.d.ts.map +1 -0
  34. package/dist/exports/internal.js +3 -0
  35. package/dist/exports/internal.js.map +1 -0
  36. package/dist/index.d.ts +12 -4
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +30 -22
  39. package/dist/index.js.map +1 -1
  40. package/dist/mcp/buildMcpServer.d.ts +6 -6
  41. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  42. package/dist/mcp/buildMcpServer.js +146 -81
  43. package/dist/mcp/buildMcpServer.js.map +1 -1
  44. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  45. package/dist/mcp/builtin/collections/authTools.js +123 -61
  46. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  47. package/dist/mcp/builtin/collections/countTool.d.ts +2 -0
  48. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -0
  49. package/dist/mcp/builtin/collections/countTool.js +63 -0
  50. package/dist/mcp/builtin/collections/countTool.js.map +1 -0
  51. package/dist/mcp/builtin/collections/countVersionsTool.d.ts +2 -0
  52. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -0
  53. package/dist/mcp/builtin/collections/countVersionsTool.js +60 -0
  54. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -0
  55. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  56. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  57. package/dist/mcp/builtin/collections/createTool.js +114 -68
  58. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  59. package/dist/mcp/builtin/collections/deleteTool.d.ts +1 -1
  60. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  61. package/dist/mcp/builtin/collections/deleteTool.js +55 -61
  62. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  63. package/dist/mcp/builtin/collections/duplicateTool.d.ts +2 -0
  64. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -0
  65. package/dist/mcp/builtin/collections/duplicateTool.js +90 -0
  66. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -0
  67. package/dist/mcp/builtin/collections/fileInput.d.ts +28 -0
  68. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -0
  69. package/dist/mcp/builtin/collections/fileInput.js +88 -0
  70. package/dist/mcp/builtin/collections/fileInput.js.map +1 -0
  71. package/dist/mcp/builtin/collections/findDistinctTool.d.ts +2 -0
  72. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -0
  73. package/dist/mcp/builtin/collections/findDistinctTool.js +77 -0
  74. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -0
  75. package/dist/mcp/builtin/collections/findTool.d.ts +1 -1
  76. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  77. package/dist/mcp/builtin/collections/findTool.js +62 -75
  78. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  79. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts +2 -0
  80. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -0
  81. package/dist/mcp/builtin/collections/findVersionByIDTool.js +74 -0
  82. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -0
  83. package/dist/mcp/builtin/collections/findVersionsTool.d.ts +2 -0
  84. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -0
  85. package/dist/mcp/builtin/collections/findVersionsTool.js +84 -0
  86. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -0
  87. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts +2 -0
  88. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -0
  89. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +103 -0
  90. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -0
  91. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts +2 -0
  92. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -0
  93. package/dist/mcp/builtin/collections/restoreVersionTool.js +69 -0
  94. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -0
  95. package/dist/mcp/builtin/collections/updateTool.d.ts +1 -1
  96. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  97. package/dist/mcp/builtin/collections/updateTool.js +118 -137
  98. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  99. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
  100. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
  101. package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
  102. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
  103. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  104. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  105. package/dist/mcp/builtin/formatEntityError.js +55 -0
  106. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  107. package/dist/mcp/builtin/getConfigInfoTool.d.ts +2 -0
  108. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -0
  109. package/dist/mcp/builtin/getConfigInfoTool.js +69 -0
  110. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -0
  111. package/dist/mcp/builtin/globals/countVersionsTool.d.ts +2 -0
  112. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -0
  113. package/dist/mcp/builtin/globals/countVersionsTool.js +60 -0
  114. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -0
  115. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  116. package/dist/mcp/builtin/globals/findTool.js +27 -36
  117. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  118. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts +2 -0
  119. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -0
  120. package/dist/mcp/builtin/globals/findVersionByIDTool.js +68 -0
  121. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -0
  122. package/dist/mcp/builtin/globals/findVersionsTool.d.ts +2 -0
  123. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -0
  124. package/dist/mcp/builtin/globals/findVersionsTool.js +78 -0
  125. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -0
  126. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts +2 -0
  127. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -0
  128. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +81 -0
  129. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -0
  130. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts +2 -0
  131. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -0
  132. package/dist/mcp/builtin/globals/restoreVersionTool.js +70 -0
  133. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -0
  134. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  135. package/dist/mcp/builtin/globals/updateTool.js +44 -62
  136. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  137. package/dist/mcp/builtinTools.d.ts +150 -19
  138. package/dist/mcp/builtinTools.d.ts.map +1 -1
  139. package/dist/mcp/builtinTools.js +125 -20
  140. package/dist/mcp/builtinTools.js.map +1 -1
  141. package/dist/mcp/sanitizeMCPConfig.d.ts +0 -1
  142. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  143. package/dist/mcp/sanitizeMCPConfig.js +127 -58
  144. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  145. package/dist/stdio.d.ts +3 -3
  146. package/dist/stdio.d.ts.map +1 -1
  147. package/dist/stdio.js +86 -47
  148. package/dist/stdio.js.map +1 -1
  149. package/dist/types.d.ts +205 -114
  150. package/dist/types.d.ts.map +1 -1
  151. package/dist/types.js +2 -4
  152. package/dist/types.js.map +1 -1
  153. package/dist/utils/camelCase.d.ts.map +1 -1
  154. package/dist/utils/getPluginConfig.d.ts +1 -1
  155. package/dist/utils/getPluginConfig.js +1 -1
  156. package/dist/utils/getPluginConfig.js.map +1 -1
  157. package/dist/utils/resolveProjectRoot.d.ts.map +1 -1
  158. package/dist/utils/seal.d.ts +18 -0
  159. package/dist/utils/seal.d.ts.map +1 -0
  160. package/dist/utils/seal.js +27 -0
  161. package/dist/utils/seal.js.map +1 -0
  162. package/dist/utils/toStandardSchema.d.ts.map +1 -1
  163. package/dist/utils/toStandardSchema.js.map +1 -1
  164. package/package.json +27 -37
  165. package/src/defaultAccess.ts +3 -0
  166. package/src/endpoint/access.ts +99 -101
  167. package/src/endpoint/index.ts +95 -15
  168. package/src/endpoint/oauth.ts +92 -0
  169. package/src/endpoint/oauthLoginToken.ts +91 -0
  170. package/src/endpoint/oauthServer.ts +396 -0
  171. package/src/exports/client.ts +2 -1
  172. package/src/exports/internal.ts +1 -0
  173. package/src/index.ts +52 -25
  174. package/src/mcp/buildMcpServer.ts +181 -116
  175. package/src/mcp/builtin/collections/authTools.ts +124 -77
  176. package/src/mcp/builtin/collections/countTool.ts +61 -0
  177. package/src/mcp/builtin/collections/countVersionsTool.ts +60 -0
  178. package/src/mcp/builtin/collections/createTool.ts +115 -88
  179. package/src/mcp/builtin/collections/deleteTool.ts +49 -75
  180. package/src/mcp/builtin/collections/duplicateTool.ts +84 -0
  181. package/src/mcp/builtin/collections/fileInput.spec.ts +93 -0
  182. package/src/mcp/builtin/collections/fileInput.ts +127 -0
  183. package/src/mcp/builtin/collections/findDistinctTool.ts +67 -0
  184. package/src/mcp/builtin/collections/findTool.ts +58 -111
  185. package/src/mcp/builtin/collections/findVersionByIDTool.ts +66 -0
  186. package/src/mcp/builtin/collections/findVersionsTool.ts +83 -0
  187. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +91 -0
  188. package/src/mcp/builtin/collections/restoreVersionTool.ts +65 -0
  189. package/src/mcp/builtin/collections/updateTool.ts +106 -143
  190. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
  191. package/src/mcp/builtin/formatEntityError.ts +77 -0
  192. package/src/mcp/builtin/getConfigInfoTool.ts +68 -0
  193. package/src/mcp/builtin/globals/countVersionsTool.ts +60 -0
  194. package/src/mcp/builtin/globals/findTool.ts +28 -58
  195. package/src/mcp/builtin/globals/findVersionByIDTool.ts +64 -0
  196. package/src/mcp/builtin/globals/findVersionsTool.ts +69 -0
  197. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +73 -0
  198. package/src/mcp/builtin/globals/restoreVersionTool.ts +68 -0
  199. package/src/mcp/builtin/globals/updateTool.ts +52 -80
  200. package/src/mcp/builtinTools.ts +118 -41
  201. package/src/mcp/sanitizeMCPConfig.ts +121 -78
  202. package/src/stdio.ts +80 -47
  203. package/src/types.ts +223 -119
  204. package/src/utils/getPluginConfig.ts +1 -1
  205. package/src/utils/seal.ts +46 -0
  206. package/src/utils/toStandardSchema.ts +1 -1
  207. package/dist/@types/assets.d.js +0 -2
  208. package/dist/@types/assets.d.js.map +0 -1
  209. package/dist/collection/getAccessField.d.ts +0 -12
  210. package/dist/collection/getAccessField.d.ts.map +0 -1
  211. package/dist/collection/getAccessField.js +0 -57
  212. package/dist/collection/getAccessField.js.map +0 -1
  213. package/dist/collection/index.d.ts +0 -6
  214. package/dist/collection/index.d.ts.map +0 -1
  215. package/dist/collection/index.js +0 -60
  216. package/dist/collection/index.js.map +0 -1
  217. package/dist/components/AccessField/index.client.d.ts +0 -10
  218. package/dist/components/AccessField/index.client.d.ts.map +0 -1
  219. package/dist/components/AccessField/index.client.js +0 -305
  220. package/dist/components/AccessField/index.client.js.map +0 -1
  221. package/dist/components/AccessField/index.css +0 -93
  222. package/dist/utils/getVirtualFieldNames.d.ts +0 -14
  223. package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
  224. package/dist/utils/getVirtualFieldNames.js +0 -35
  225. package/dist/utils/getVirtualFieldNames.js.map +0 -1
  226. package/dist/utils/localAPIDefaults.d.ts +0 -20
  227. package/dist/utils/localAPIDefaults.d.ts.map +0 -1
  228. package/dist/utils/localAPIDefaults.js +0 -19
  229. package/dist/utils/localAPIDefaults.js.map +0 -1
  230. package/dist/utils/schemaConversion/buildToolInput.d.ts +0 -29
  231. package/dist/utils/schemaConversion/buildToolInput.d.ts.map +0 -1
  232. package/dist/utils/schemaConversion/buildToolInput.js +0 -51
  233. package/dist/utils/schemaConversion/buildToolInput.js.map +0 -1
  234. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts +0 -7
  235. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts.map +0 -1
  236. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js +0 -20
  237. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js.map +0 -1
  238. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -15
  239. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
  240. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -464
  241. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
  242. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -158
  243. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
  244. package/dist/utils/transformPointDataToPayload.d.ts +0 -7
  245. package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
  246. package/dist/utils/transformPointDataToPayload.js +0 -28
  247. package/dist/utils/transformPointDataToPayload.js.map +0 -1
  248. package/src/@types/assets.d.ts +0 -3
  249. package/src/collection/getAccessField.ts +0 -64
  250. package/src/collection/index.ts +0 -64
  251. package/src/components/AccessField/index.client.tsx +0 -344
  252. package/src/components/AccessField/index.css +0 -93
  253. package/src/utils/getVirtualFieldNames.ts +0 -53
  254. package/src/utils/localAPIDefaults.ts +0 -22
  255. package/src/utils/schemaConversion/buildToolInput.ts +0 -68
  256. package/src/utils/schemaConversion/removeVirtualFieldsFromSchema.ts +0 -27
  257. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -103
  258. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -529
  259. package/src/utils/transformPointDataToPayload.ts +0 -40
@@ -0,0 +1,91 @@
1
+ import type { PayloadRequest } from 'payload'
2
+
3
+ import { APIError, createAuthToken, executeAuthStrategies, redeemRefreshToken } 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 redeemRefreshToken({ 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 })
@@ -0,0 +1,396 @@
1
+ import type {
2
+ OAuthClientInformationFull,
3
+ OAuthTokens,
4
+ OpenIdProviderDiscoveryMetadata,
5
+ } from '@modelcontextprotocol/server'
6
+ import type { Endpoint, PayloadHandler, PayloadRequest } from 'payload'
7
+
8
+ import { OAuthErrorCode } from '@modelcontextprotocol/server'
9
+ import { createHash } from 'crypto'
10
+ import { formatAdminURL } from 'payload/shared'
11
+
12
+ import type { MCPBuiltinOAuthConfig, MCPOAuthTokens } from '../types.js'
13
+
14
+ import { getPluginConfig } from '../utils/getPluginConfig.js'
15
+ import { seal, unseal } from '../utils/seal.js'
16
+ import { getMCPURL } from './oauth.js'
17
+ import { issueLoginToken, refreshLoginToken } from './oauthLoginToken.js'
18
+
19
+ type Client = { name: string; redirectURIs: string[] }
20
+
21
+ type Consent = {
22
+ clientID: string
23
+ codeChallenge: string
24
+ collection: string
25
+ redirectURI: string
26
+ state: null | string
27
+ userID: number | string
28
+ }
29
+
30
+ type Code = {
31
+ clientID: string
32
+ codeChallenge: string
33
+ redirectURI: string
34
+ tokens: MCPOAuthTokens
35
+ }
36
+
37
+ type RefreshToken = {
38
+ clientID: string
39
+ refreshToken: string
40
+ }
41
+
42
+ /**
43
+ * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is
44
+ * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and
45
+ * the client receives the tokens `issueToken` creates.
46
+ *
47
+ * Clients, consent forms, authorization codes, and refresh tokens are encrypted and handed to the
48
+ * client instead of being stored. Only the default tokens write to the database, to the user's
49
+ * login sessions.
50
+ */
51
+ export const oauthServerEndpoints: Endpoint[] = [
52
+ {
53
+ disableAuth: true,
54
+ handler: (req) => withIssuer(req, (issuer) => metadataResponse(issuer)),
55
+ method: 'get',
56
+ // OpenID discovery is the only metadata location MCP clients check below the issuer's path,
57
+ // which lets this work under /api without a route at the site root
58
+ path: '/mcp/oauth/.well-known/openid-configuration',
59
+ },
60
+ {
61
+ disableAuth: true,
62
+ handler: (req) => withIssuer(req, () => Response.json({ keys: [] })),
63
+ method: 'get',
64
+ path: '/mcp/oauth/jwks',
65
+ },
66
+ {
67
+ disableAuth: true,
68
+ handler: (req) => withIssuer(req, () => register(req)),
69
+ method: 'post',
70
+ path: '/mcp/oauth/register',
71
+ },
72
+ {
73
+ handler: (req) => withIssuer(req, () => authorize(req)),
74
+ method: 'get',
75
+ path: '/mcp/oauth/authorize',
76
+ },
77
+ {
78
+ handler: (req) => withIssuer(req, () => approve(req)),
79
+ method: 'post',
80
+ path: '/mcp/oauth/authorize',
81
+ },
82
+ {
83
+ disableAuth: true,
84
+ handler: (req) => withIssuer(req, () => token(req)),
85
+ method: 'post',
86
+ path: '/mcp/oauth/token',
87
+ },
88
+ ]
89
+
90
+ /** The authorization server URL, or `undefined` when Payload isn't the authorization server. */
91
+ const getIssuer = (req: PayloadRequest): string | undefined =>
92
+ getPluginConfig({ config: req.payload.config }).oauth?.type === 'builtin'
93
+ ? `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth`
94
+ : undefined
95
+
96
+ const withIssuer = (
97
+ req: PayloadRequest,
98
+ handler: (issuer: string) => Promise<Response> | Response,
99
+ ): Promise<Response> | Response => {
100
+ const issuer = getIssuer(req)
101
+ return issuer ? handler(issuer) : new Response(null, { status: 404 })
102
+ }
103
+
104
+ /**
105
+ * The user logged in to this browser, who can allow MCP clients. Tokens and API keys in the
106
+ * `Authorization` header can't, or an MCP client could use its access token to approve itself a
107
+ * new, separate login.
108
+ */
109
+ const getBrowserUser = (req: PayloadRequest): PayloadRequest['user'] =>
110
+ req.headers.has('Authorization') ? null : req.user
111
+
112
+ const metadataResponse = (issuer: string): Response =>
113
+ Response.json(
114
+ {
115
+ authorization_endpoint: `${issuer}/authorize`,
116
+ code_challenge_methods_supported: ['S256'],
117
+ grant_types_supported: ['authorization_code', 'refresh_token'],
118
+ // Required by OpenID discovery. No ID tokens are issued.
119
+ id_token_signing_alg_values_supported: ['RS256'],
120
+ issuer,
121
+ jwks_uri: `${issuer}/jwks`,
122
+ registration_endpoint: `${issuer}/register`,
123
+ response_types_supported: ['code'],
124
+ subject_types_supported: ['public'],
125
+ token_endpoint: `${issuer}/token`,
126
+ token_endpoint_auth_methods_supported: ['none'],
127
+ } satisfies OpenIdProviderDiscoveryMetadata,
128
+ // Not cached, since without `serverURL` its URLs come from the request
129
+ { headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' } },
130
+ )
131
+
132
+ const register: PayloadHandler = async (req) => {
133
+ const { client_name, redirect_uris } = await req.json!().catch(() => ({}))
134
+
135
+ if (
136
+ !Array.isArray(redirect_uris) ||
137
+ !redirect_uris.length ||
138
+ !redirect_uris.every(
139
+ (uri) => typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'),
140
+ )
141
+ ) {
142
+ return Response.json({ error: OAuthErrorCode.InvalidRedirectUri }, { status: 400 })
143
+ }
144
+
145
+ const client: Client = {
146
+ name: typeof client_name === 'string' ? client_name : 'An MCP client',
147
+ redirectURIs: redirect_uris,
148
+ }
149
+
150
+ return Response.json(
151
+ {
152
+ client_id: seal({ type: 'client', data: client, req }),
153
+ client_name: client.name,
154
+ grant_types: ['authorization_code', 'refresh_token'],
155
+ redirect_uris: client.redirectURIs,
156
+ response_types: ['code'],
157
+ token_endpoint_auth_method: 'none',
158
+ } satisfies OAuthClientInformationFull,
159
+ { status: 201 },
160
+ )
161
+ }
162
+
163
+ const authorize: PayloadHandler = (req) => {
164
+ const url = new URL(req.url!)
165
+ const params = url.searchParams
166
+ const clientID = params.get('client_id')
167
+ const client = unseal<Client>({ type: 'client', req, value: clientID })
168
+ const redirectURI = params.get('redirect_uri')
169
+
170
+ // Never redirect to an address the client didn't register
171
+ if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {
172
+ return page({
173
+ content: '<p>Start the sign-in again from your MCP client.</p>',
174
+ status: 400,
175
+ title: 'This sign-in link is invalid',
176
+ })
177
+ }
178
+
179
+ const state = params.get('state')
180
+ const codeChallenge = params.get('code_challenge')
181
+
182
+ if (
183
+ params.get('response_type') !== 'code' ||
184
+ params.get('code_challenge_method') !== 'S256' ||
185
+ !codeChallenge
186
+ ) {
187
+ return redirect({ params: { error: OAuthErrorCode.InvalidRequest, state }, redirectURI })
188
+ }
189
+
190
+ const user = getBrowserUser(req)
191
+
192
+ if (!user) {
193
+ const loginURL = formatAdminURL({ adminRoute: req.payload.config.routes.admin, path: '/login' })
194
+
195
+ return new Response(null, {
196
+ headers: {
197
+ Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`,
198
+ },
199
+ status: 302,
200
+ })
201
+ }
202
+
203
+ const consent: Consent = {
204
+ clientID: clientID!,
205
+ codeChallenge,
206
+ collection: user.collection,
207
+ redirectURI,
208
+ state,
209
+ userID: user.id,
210
+ }
211
+
212
+ return page({
213
+ content: `<p>It will be able to use this app's MCP tools as ${escapeHTML(String(user.email ?? user.id))}, and you will return to ${escapeHTML(new URL(redirectURI).host || redirectURI)}.</p>
214
+ <form method="post">
215
+ <input name="consent" type="hidden" value="${seal({ type: 'consent', data: consent, expiresIn: 20 * 60 * 60, req })}">
216
+ <button name="decision" type="submit" value="allow">Allow</button>
217
+ <button name="decision" type="submit" value="deny">Deny</button>
218
+ </form>`,
219
+ status: 200,
220
+ title: `Allow ${client.name} to use your account?`,
221
+ })
222
+ }
223
+
224
+ const approve: PayloadHandler = async (req) => {
225
+ const form = new URLSearchParams(await req.text!())
226
+ const consent = unseal<Consent>({ type: 'consent', req, value: form.get('consent') })
227
+ const user = getBrowserUser(req)
228
+
229
+ // The form is only valid for the user it was shown to
230
+ if (!consent || user?.id !== consent.userID || user.collection !== consent.collection) {
231
+ return page({
232
+ content: '<p>Start the sign-in again from your MCP client.</p>',
233
+ status: 400,
234
+ title: 'This sign-in request expired',
235
+ })
236
+ }
237
+
238
+ const { clientID, codeChallenge, redirectURI, state } = consent
239
+
240
+ if (form.get('decision') !== 'allow') {
241
+ return redirect({ params: { error: OAuthErrorCode.AccessDenied, state }, redirectURI })
242
+ }
243
+
244
+ const code: Code = {
245
+ clientID,
246
+ codeChallenge,
247
+ redirectURI,
248
+ tokens: await getTokenHooks(req).issueToken({ req }),
249
+ }
250
+
251
+ return redirect({
252
+ params: { code: seal({ type: 'code', data: code, expiresIn: 60, req }), state },
253
+ redirectURI,
254
+ })
255
+ }
256
+
257
+ const token: PayloadHandler = async (req) => {
258
+ const form = new URLSearchParams(await req.text!())
259
+ const clientID = form.get('client_id')
260
+ const tokens =
261
+ clientID &&
262
+ (form.get('grant_type') === 'refresh_token'
263
+ ? await refresh({ clientID, form, req })
264
+ : redeemCode({ clientID, form, req }))
265
+
266
+ if (!clientID || !tokens) {
267
+ return Response.json({ error: OAuthErrorCode.InvalidGrant }, { status: 400 })
268
+ }
269
+
270
+ return Response.json(
271
+ {
272
+ access_token: tokens.accessToken,
273
+ expires_in: tokens.expiresIn,
274
+ refresh_token:
275
+ tokens.refreshToken &&
276
+ seal({
277
+ type: 'refresh',
278
+ data: { clientID, refreshToken: tokens.refreshToken } satisfies RefreshToken,
279
+ req,
280
+ }),
281
+ token_type: 'Bearer',
282
+ } satisfies OAuthTokens,
283
+ { headers: { 'Cache-Control': 'no-store' } },
284
+ )
285
+ }
286
+
287
+ const redeemCode = ({
288
+ clientID,
289
+ form,
290
+ req,
291
+ }: {
292
+ clientID: string
293
+ form: URLSearchParams
294
+ req: PayloadRequest
295
+ }): MCPOAuthTokens | undefined => {
296
+ const code = unseal<Code>({ type: 'code', req, value: form.get('code') })
297
+
298
+ if (
299
+ form.get('grant_type') === 'authorization_code' &&
300
+ code?.clientID === clientID &&
301
+ code.redirectURI === form.get('redirect_uri') &&
302
+ // PKCE: only the client that started the sign-in knows the verifier
303
+ createHash('sha256')
304
+ .update(form.get('code_verifier') ?? '')
305
+ .digest('base64url') === code.codeChallenge
306
+ ) {
307
+ return code.tokens
308
+ }
309
+ }
310
+
311
+ const refresh = async ({
312
+ clientID,
313
+ form,
314
+ req,
315
+ }: {
316
+ clientID: string
317
+ form: URLSearchParams
318
+ req: PayloadRequest
319
+ }): Promise<MCPOAuthTokens | null | undefined> => {
320
+ const refreshToken = unseal<RefreshToken>({
321
+ type: 'refresh',
322
+ req,
323
+ value: form.get('refresh_token'),
324
+ })
325
+
326
+ // Only the client a refresh token was issued to can use it
327
+ if (refreshToken?.clientID === clientID) {
328
+ return await getTokenHooks(req).refreshToken?.({
329
+ refreshToken: refreshToken.refreshToken,
330
+ req,
331
+ })
332
+ }
333
+ }
334
+
335
+ /** `issueToken` and `refreshToken`, or the defaults for Payload login tokens. */
336
+ const getTokenHooks = (req: PayloadRequest) => {
337
+ const oauth = getPluginConfig({ config: req.payload.config }).oauth as MCPBuiltinOAuthConfig
338
+
339
+ return oauth.issueToken
340
+ ? { issueToken: oauth.issueToken, refreshToken: oauth.refreshToken }
341
+ : { issueToken: issueLoginToken, refreshToken: refreshLoginToken }
342
+ }
343
+
344
+ const redirect = ({
345
+ params,
346
+ redirectURI,
347
+ }: {
348
+ params: Record<string, null | string>
349
+ redirectURI: string
350
+ }): Response => {
351
+ const location = new URL(redirectURI)
352
+
353
+ for (const [key, value] of Object.entries(params)) {
354
+ if (value) {
355
+ location.searchParams.set(key, value)
356
+ }
357
+ }
358
+
359
+ return new Response(null, { headers: { Location: location.href }, status: 302 })
360
+ }
361
+
362
+ const page = ({
363
+ content,
364
+ status,
365
+ title,
366
+ }: {
367
+ content: string
368
+ status: number
369
+ title: string
370
+ }): Response =>
371
+ new Response(
372
+ `<!doctype html>
373
+ <html lang="en">
374
+ <head>
375
+ <meta charset="utf-8">
376
+ <meta name="viewport" content="width=device-width, initial-scale=1">
377
+ <title>${escapeHTML(title)}</title>
378
+ <style>
379
+ body { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }
380
+ button { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }
381
+ </style>
382
+ </head>
383
+ <body><h1>${escapeHTML(title)}</h1>${content}</body>
384
+ </html>`,
385
+ {
386
+ headers: {
387
+ // Stop other sites from framing the page to trick users into clicking Allow
388
+ 'Content-Security-Policy': "frame-ancestors 'none'",
389
+ 'Content-Type': 'text/html; charset=utf-8',
390
+ },
391
+ status,
392
+ },
393
+ )
394
+
395
+ const escapeHTML = (value: string): string =>
396
+ value.replace(/[&<>"']/g, (character) => `&#${character.charCodeAt(0)};`)
@@ -1,2 +1,3 @@
1
1
  'use client'
2
- export { AccessField } from '../components/AccessField/index.client.js'
2
+
3
+ export {}
@@ -0,0 +1 @@
1
+ export { filterMCPItems, getAuthorizedMCP } from '../endpoint/access.js'
package/src/index.ts CHANGED
@@ -1,52 +1,63 @@
1
- import { defaultUserCollection, definePlugin } from 'payload'
1
+ import { definePlugin } from 'payload'
2
2
 
3
- import type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'
3
+ import type {
4
+ AuthorizedMCP,
5
+ MCPBuiltinOAuthConfig,
6
+ MCPExternalOAuthConfig,
7
+ MCPOAuthConfig,
8
+ MCPOAuthTokens,
9
+ MCPPluginConfig,
10
+ SanitizedMCPPluginConfig,
11
+ } from './types.js'
4
12
 
5
- import { getAPIKeysCollection } from './collection/index.js'
6
13
  import { mcpEndpoint } from './endpoint/index.js'
14
+ import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js'
15
+ import { oauthServerEndpoints } from './endpoint/oauthServer.js'
7
16
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'
8
17
 
9
18
  declare module 'payload' {
10
- export interface PayloadRequest {
11
- payloadAPI: 'GraphQL' | 'local' | 'MCP' | 'REST'
19
+ export interface PayloadRequestAPI {
20
+ MCP: true
12
21
  }
13
22
  interface RegisteredPlugins {
14
23
  /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */
15
24
  '@payloadcms/plugin-mcp': MCPPluginConfig
16
25
  }
26
+ export interface RequestContext {
27
+ /** Set on MCP requests before they're authenticated. */
28
+ mcp?: {
29
+ /** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */
30
+ resource: string
31
+ }
32
+ }
17
33
  }
18
34
 
19
- export type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig }
35
+ export type {
36
+ AuthorizedMCP,
37
+ MCPBuiltinOAuthConfig,
38
+ MCPExternalOAuthConfig,
39
+ MCPOAuthConfig,
40
+ MCPOAuthTokens,
41
+ MCPPluginConfig,
42
+ SanitizedMCPPluginConfig,
43
+ }
20
44
 
21
45
  export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'
46
+ export { oauthProtectedResourceHandler } from './endpoint/oauth.js'
22
47
 
23
48
  export const mcpPlugin = definePlugin<MCPPluginConfig>({
24
49
  slug: '@payloadcms/plugin-mcp',
25
50
  order: 10,
26
- plugin: ({ config, plugins, ...rawConfig }) => {
27
- // Our `payload-mcp-api-keys` is auth-enabled; if it'd be the only auth
28
- // collection, Payload's later sanitize would pick it as `admin.user`.
29
- // Pre-seed the default user collection to prevent that.
30
- if (!config.admin?.user) {
31
- const firstCollectionWithAuth = (config.collections ?? []).find(({ auth }) => Boolean(auth))
32
- if (!firstCollectionWithAuth) {
33
- ;(config.collections ??= []).push(defaultUserCollection)
34
- }
35
- }
36
-
37
- const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: rawConfig })
51
+ plugin: ({ config, options, plugins }) => {
52
+ const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: options })
38
53
 
39
54
  // Stash the sanitized config on plugin options so `getPluginConfig()` reads it.
40
55
  const registered = plugins['@payloadcms/plugin-mcp']
41
56
  if (registered) {
42
57
  // @ts-expect-error
43
- registered.sanitizedOptions = pluginConfig as unknown as typeof registered.options
58
+ registered.sanitizedOptions = pluginConfig
44
59
  }
45
60
 
46
- ;(config.collections ??= []).push(getAPIKeysCollection({ pluginConfig }))
47
-
48
- // Keep the API-keys collection registered even when disabled, so DB schema
49
- // and generated types don't drift between enabled/disabled environments.
50
61
  if (pluginConfig.disabled) {
51
62
  return config
52
63
  }
@@ -55,8 +66,24 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
55
66
  ...config,
56
67
  endpoints: [
57
68
  ...(config.endpoints ?? []),
58
- // Payload prefixes /api, so the full path is /api/mcp.
59
- { handler: mcpEndpoint, method: 'post', path: '/mcp' },
69
+ // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
70
+ // request with dev auto-login turned off, so Payload doesn't authenticate it first.
71
+ { disableAuth: true, handler: mcpEndpoint, method: 'post', path: '/mcp' },
72
+ // Streamable HTTP's optional server=>client GET stream. We don't offer
73
+ // one, so answer 405 per spec-— clients then skip it cleanly
74
+ {
75
+ disableAuth: true,
76
+ handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),
77
+ method: 'get',
78
+ path: '/mcp',
79
+ },
80
+ {
81
+ disableAuth: true,
82
+ handler: oauthProtectedResourceEndpoint,
83
+ method: 'get',
84
+ path: '/mcp/oauth-protected-resource',
85
+ },
86
+ ...oauthServerEndpoints,
60
87
  ],
61
88
  }
62
89
  },