@payloadcms/plugin-mcp 4.0.0-internal.167c7cd → 4.0.0-internal.16f218d

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 (196) 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 +45 -4
  7. package/dist/endpoint/index.js.map +1 -1
  8. package/dist/endpoint/oauth.d.ts +35 -0
  9. package/dist/endpoint/oauth.d.ts.map +1 -0
  10. package/dist/endpoint/oauth.js +96 -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 +361 -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.map +1 -1
  38. package/dist/mcp/builtin/collections/createTool.js +48 -49
  39. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  40. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  41. package/dist/mcp/builtin/collections/deleteTool.js +44 -43
  42. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  43. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  44. package/dist/mcp/builtin/collections/duplicateTool.js +31 -37
  45. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  46. package/dist/mcp/builtin/collections/fileInput.d.ts +21 -21
  47. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
  48. package/dist/mcp/builtin/collections/fileInput.js +34 -70
  49. package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
  50. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
  51. package/dist/mcp/builtin/collections/findDistinctTool.js +11 -26
  52. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
  53. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  54. package/dist/mcp/builtin/collections/findTool.js +28 -46
  55. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  56. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
  57. package/dist/mcp/builtin/collections/findVersionByIDTool.js +17 -26
  58. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
  59. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
  60. package/dist/mcp/builtin/collections/findVersionsTool.js +16 -30
  61. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
  62. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  63. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +30 -14
  64. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  65. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
  66. package/dist/mcp/builtin/collections/restoreVersionTool.js +14 -26
  67. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  68. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  69. package/dist/mcp/builtin/collections/updateTool.js +55 -87
  70. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  71. package/dist/mcp/builtin/collections/uploadInstructionsTool.js +11 -11
  72. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -1
  73. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  74. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  75. package/dist/mcp/builtin/formatEntityError.js +55 -0
  76. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  77. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
  78. package/dist/mcp/builtin/getConfigInfoTool.js +3 -2
  79. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
  80. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
  81. package/dist/mcp/builtin/globals/countVersionsTool.js +9 -13
  82. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
  83. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  84. package/dist/mcp/builtin/globals/findTool.js +10 -16
  85. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  86. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
  87. package/dist/mcp/builtin/globals/findVersionByIDTool.js +13 -24
  88. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
  89. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
  90. package/dist/mcp/builtin/globals/findVersionsTool.js +13 -29
  91. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
  92. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  93. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +29 -13
  94. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  95. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
  96. package/dist/mcp/builtin/globals/restoreVersionTool.js +15 -22
  97. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
  98. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  99. package/dist/mcp/builtin/globals/updateTool.js +27 -36
  100. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  101. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  102. package/dist/mcp/sanitizeMCPConfig.js +3 -0
  103. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  104. package/dist/stdio.js +4 -3
  105. package/dist/stdio.js.map +1 -1
  106. package/dist/types.d.ts +99 -7
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/seal.d.ts +18 -0
  110. package/dist/utils/seal.d.ts.map +1 -0
  111. package/dist/utils/seal.js +27 -0
  112. package/dist/utils/seal.js.map +1 -0
  113. package/dist/utils/toStandardSchema.js.map +1 -1
  114. package/package.json +23 -28
  115. package/src/endpoint/access.ts +24 -6
  116. package/src/endpoint/index.ts +41 -3
  117. package/src/endpoint/oauth.ts +100 -0
  118. package/src/endpoint/oauthLoginToken.ts +91 -0
  119. package/src/endpoint/oauthServer.ts +396 -0
  120. package/src/index.ts +40 -5
  121. package/src/mcp/buildMcpServer.ts +40 -30
  122. package/src/mcp/builtin/collections/authTools.ts +82 -71
  123. package/src/mcp/builtin/collections/countTool.ts +9 -21
  124. package/src/mcp/builtin/collections/countVersionsTool.ts +9 -18
  125. package/src/mcp/builtin/collections/createTool.ts +71 -96
  126. package/src/mcp/builtin/collections/deleteTool.ts +37 -65
  127. package/src/mcp/builtin/collections/duplicateTool.ts +36 -86
  128. package/src/mcp/builtin/collections/fileInput.spec.ts +2 -2
  129. package/src/mcp/builtin/collections/fileInput.ts +44 -81
  130. package/src/mcp/builtin/collections/findDistinctTool.ts +11 -63
  131. package/src/mcp/builtin/collections/findTool.ts +24 -106
  132. package/src/mcp/builtin/collections/findVersionByIDTool.ts +15 -58
  133. package/src/mcp/builtin/collections/findVersionsTool.ts +14 -85
  134. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +23 -12
  135. package/src/mcp/builtin/collections/restoreVersionTool.ts +15 -57
  136. package/src/mcp/builtin/collections/updateTool.ts +55 -109
  137. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +12 -12
  138. package/src/mcp/builtin/formatEntityError.ts +77 -0
  139. package/src/mcp/builtin/getConfigInfoTool.ts +2 -1
  140. package/src/mcp/builtin/globals/countVersionsTool.ts +9 -17
  141. package/src/mcp/builtin/globals/findTool.ts +13 -52
  142. package/src/mcp/builtin/globals/findVersionByIDTool.ts +13 -52
  143. package/src/mcp/builtin/globals/findVersionsTool.ts +14 -92
  144. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +21 -11
  145. package/src/mcp/builtin/globals/restoreVersionTool.ts +14 -45
  146. package/src/mcp/builtin/globals/updateTool.ts +38 -61
  147. package/src/mcp/sanitizeMCPConfig.ts +1 -0
  148. package/src/stdio.ts +2 -2
  149. package/src/types.ts +99 -7
  150. package/src/utils/seal.ts +46 -0
  151. package/src/utils/toStandardSchema.ts +1 -1
  152. package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
  153. package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
  154. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
  155. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
  156. package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
  157. package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
  158. package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
  159. package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
  160. package/dist/mcp/builtin/validateEntityData.js +0 -82
  161. package/dist/mcp/builtin/validateEntityData.js.map +0 -1
  162. package/dist/utils/getVirtualFieldNames.d.ts +0 -14
  163. package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
  164. package/dist/utils/getVirtualFieldNames.js +0 -35
  165. package/dist/utils/getVirtualFieldNames.js.map +0 -1
  166. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +0 -28
  167. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +0 -1
  168. package/dist/utils/schemaConversion/filterFieldsByAccess.js +0 -71
  169. package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +0 -1
  170. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -13
  171. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
  172. package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -66
  173. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
  174. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
  175. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
  176. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
  177. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
  178. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
  179. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
  180. package/dist/utils/transformPointDataToPayload.d.ts +0 -7
  181. package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
  182. package/dist/utils/transformPointDataToPayload.js +0 -28
  183. package/dist/utils/transformPointDataToPayload.js.map +0 -1
  184. package/dist/utils/whereSchema.d.ts +0 -9
  185. package/dist/utils/whereSchema.d.ts.map +0 -1
  186. package/dist/utils/whereSchema.js +0 -13
  187. package/dist/utils/whereSchema.js.map +0 -1
  188. package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
  189. package/src/mcp/builtin/validateEntityData.ts +0 -132
  190. package/src/utils/getVirtualFieldNames.ts +0 -53
  191. package/src/utils/schemaConversion/filterFieldsByAccess.ts +0 -123
  192. package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -96
  193. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
  194. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
  195. package/src/utils/transformPointDataToPayload.ts +0 -40
  196. package/src/utils/whereSchema.ts +0 -24
@@ -0,0 +1,361 @@
1
+ import { OAuthErrorCode } from '@modelcontextprotocol/server';
2
+ import { createHash } from 'crypto';
3
+ import { formatAdminURL } from 'payload/shared';
4
+ import { getPluginConfig } from '../utils/getPluginConfig.js';
5
+ import { seal, unseal } from '../utils/seal.js';
6
+ import { getMCPResource } from './oauth.js';
7
+ import { issueLoginToken, refreshLoginToken } from './oauthLoginToken.js';
8
+ /**
9
+ * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is
10
+ * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and
11
+ * the client receives the tokens `issueToken` creates.
12
+ *
13
+ * Clients, consent forms, authorization codes, and refresh tokens are encrypted and handed to the
14
+ * client instead of being stored. Only the default tokens write to the database, to the user's
15
+ * login sessions.
16
+ */ export const oauthServerEndpoints = [
17
+ {
18
+ disableAuth: true,
19
+ handler: (req)=>withIssuer(req, (issuer)=>metadataResponse(issuer)),
20
+ method: 'get',
21
+ // OpenID discovery is the only metadata location MCP clients check below the issuer's path,
22
+ // which lets this work under /api without a route at the site root
23
+ path: '/mcp/oauth/.well-known/openid-configuration'
24
+ },
25
+ {
26
+ disableAuth: true,
27
+ handler: (req)=>withIssuer(req, ()=>Response.json({
28
+ keys: []
29
+ })),
30
+ method: 'get',
31
+ path: '/mcp/oauth/jwks'
32
+ },
33
+ {
34
+ disableAuth: true,
35
+ handler: (req)=>withIssuer(req, ()=>register(req)),
36
+ method: 'post',
37
+ path: '/mcp/oauth/register'
38
+ },
39
+ {
40
+ handler: (req)=>withIssuer(req, ()=>authorize(req)),
41
+ method: 'get',
42
+ path: '/mcp/oauth/authorize'
43
+ },
44
+ {
45
+ handler: (req)=>withIssuer(req, ()=>approve(req)),
46
+ method: 'post',
47
+ path: '/mcp/oauth/authorize'
48
+ },
49
+ {
50
+ disableAuth: true,
51
+ handler: (req)=>withIssuer(req, ()=>token(req)),
52
+ method: 'post',
53
+ path: '/mcp/oauth/token'
54
+ }
55
+ ];
56
+ /** The authorization server URL, or `undefined` when Payload isn't the authorization server. */ const getIssuer = (req)=>getPluginConfig({
57
+ config: req.payload.config
58
+ }).oauth?.type === 'builtin' ? `${getMCPResource({
59
+ req
60
+ })}/oauth` : undefined;
61
+ const withIssuer = (req, handler)=>{
62
+ const issuer = getIssuer(req);
63
+ return issuer ? handler(issuer) : new Response(null, {
64
+ status: 404
65
+ });
66
+ };
67
+ /**
68
+ * The user logged in to this browser, who can allow MCP clients. Tokens and API keys in the
69
+ * `Authorization` header can't, or an MCP client could use its access token to approve itself a
70
+ * new, separate login.
71
+ */ const getBrowserUser = (req)=>req.headers.has('Authorization') ? null : req.user;
72
+ const metadataResponse = (issuer)=>Response.json({
73
+ authorization_endpoint: `${issuer}/authorize`,
74
+ code_challenge_methods_supported: [
75
+ 'S256'
76
+ ],
77
+ grant_types_supported: [
78
+ 'authorization_code',
79
+ 'refresh_token'
80
+ ],
81
+ // Required by OpenID discovery. No ID tokens are issued.
82
+ id_token_signing_alg_values_supported: [
83
+ 'RS256'
84
+ ],
85
+ issuer,
86
+ jwks_uri: `${issuer}/jwks`,
87
+ registration_endpoint: `${issuer}/register`,
88
+ response_types_supported: [
89
+ 'code'
90
+ ],
91
+ subject_types_supported: [
92
+ 'public'
93
+ ],
94
+ token_endpoint: `${issuer}/token`,
95
+ token_endpoint_auth_methods_supported: [
96
+ 'none'
97
+ ]
98
+ }, // Not cached, since without `serverURL` its URLs come from the request
99
+ {
100
+ headers: {
101
+ 'Access-Control-Allow-Origin': '*',
102
+ 'Cache-Control': 'no-store'
103
+ }
104
+ });
105
+ const register = async (req)=>{
106
+ const { client_name, redirect_uris } = await req.json().catch(()=>({}));
107
+ if (!Array.isArray(redirect_uris) || !redirect_uris.length || !redirect_uris.every((uri)=>typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'))) {
108
+ return Response.json({
109
+ error: OAuthErrorCode.InvalidRedirectUri
110
+ }, {
111
+ status: 400
112
+ });
113
+ }
114
+ const client = {
115
+ name: typeof client_name === 'string' ? client_name : 'An MCP client',
116
+ redirectURIs: redirect_uris
117
+ };
118
+ return Response.json({
119
+ client_id: seal({
120
+ type: 'client',
121
+ data: client,
122
+ req
123
+ }),
124
+ client_name: client.name,
125
+ grant_types: [
126
+ 'authorization_code',
127
+ 'refresh_token'
128
+ ],
129
+ redirect_uris: client.redirectURIs,
130
+ response_types: [
131
+ 'code'
132
+ ],
133
+ token_endpoint_auth_method: 'none'
134
+ }, {
135
+ status: 201
136
+ });
137
+ };
138
+ const authorize = (req)=>{
139
+ const url = new URL(req.url);
140
+ const params = url.searchParams;
141
+ const clientID = params.get('client_id');
142
+ const client = unseal({
143
+ type: 'client',
144
+ req,
145
+ value: clientID
146
+ });
147
+ const redirectURI = params.get('redirect_uri');
148
+ // Never redirect to an address the client didn't register
149
+ if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {
150
+ return page({
151
+ content: '<p>Start the sign-in again from your MCP client.</p>',
152
+ status: 400,
153
+ title: 'This sign-in link is invalid'
154
+ });
155
+ }
156
+ const state = params.get('state');
157
+ const codeChallenge = params.get('code_challenge');
158
+ if (params.get('response_type') !== 'code' || params.get('code_challenge_method') !== 'S256' || !codeChallenge) {
159
+ return redirect({
160
+ params: {
161
+ error: OAuthErrorCode.InvalidRequest,
162
+ state
163
+ },
164
+ redirectURI
165
+ });
166
+ }
167
+ const user = getBrowserUser(req);
168
+ if (!user) {
169
+ const loginURL = formatAdminURL({
170
+ adminRoute: req.payload.config.routes.admin,
171
+ path: '/login'
172
+ });
173
+ return new Response(null, {
174
+ headers: {
175
+ Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`
176
+ },
177
+ status: 302
178
+ });
179
+ }
180
+ const consent = {
181
+ clientID: clientID,
182
+ codeChallenge,
183
+ collection: user.collection,
184
+ redirectURI,
185
+ state,
186
+ userID: user.id
187
+ };
188
+ return page({
189
+ 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>
190
+ <form method="post">
191
+ <input name="consent" type="hidden" value="${seal({
192
+ type: 'consent',
193
+ data: consent,
194
+ expiresIn: 20 * 60 * 60,
195
+ req
196
+ })}">
197
+ <button name="decision" type="submit" value="allow">Allow</button>
198
+ <button name="decision" type="submit" value="deny">Deny</button>
199
+ </form>`,
200
+ status: 200,
201
+ title: `Allow ${client.name} to use your account?`
202
+ });
203
+ };
204
+ const approve = async (req)=>{
205
+ const form = new URLSearchParams(await req.text());
206
+ const consent = unseal({
207
+ type: 'consent',
208
+ req,
209
+ value: form.get('consent')
210
+ });
211
+ const user = getBrowserUser(req);
212
+ // The form is only valid for the user it was shown to
213
+ if (!consent || user?.id !== consent.userID || user.collection !== consent.collection) {
214
+ return page({
215
+ content: '<p>Start the sign-in again from your MCP client.</p>',
216
+ status: 400,
217
+ title: 'This sign-in request expired'
218
+ });
219
+ }
220
+ const { clientID, codeChallenge, redirectURI, state } = consent;
221
+ if (form.get('decision') !== 'allow') {
222
+ return redirect({
223
+ params: {
224
+ error: OAuthErrorCode.AccessDenied,
225
+ state
226
+ },
227
+ redirectURI
228
+ });
229
+ }
230
+ const code = {
231
+ clientID,
232
+ codeChallenge,
233
+ redirectURI,
234
+ tokens: await getTokenHooks(req).issueToken({
235
+ req
236
+ })
237
+ };
238
+ return redirect({
239
+ params: {
240
+ code: seal({
241
+ type: 'code',
242
+ data: code,
243
+ expiresIn: 60,
244
+ req
245
+ }),
246
+ state
247
+ },
248
+ redirectURI
249
+ });
250
+ };
251
+ const token = async (req)=>{
252
+ const form = new URLSearchParams(await req.text());
253
+ const clientID = form.get('client_id');
254
+ const tokens = clientID && (form.get('grant_type') === 'refresh_token' ? await refresh({
255
+ clientID,
256
+ form,
257
+ req
258
+ }) : redeemCode({
259
+ clientID,
260
+ form,
261
+ req
262
+ }));
263
+ if (!clientID || !tokens) {
264
+ return Response.json({
265
+ error: OAuthErrorCode.InvalidGrant
266
+ }, {
267
+ status: 400
268
+ });
269
+ }
270
+ return Response.json({
271
+ access_token: tokens.accessToken,
272
+ expires_in: tokens.expiresIn,
273
+ refresh_token: tokens.refreshToken && seal({
274
+ type: 'refresh',
275
+ data: {
276
+ clientID,
277
+ refreshToken: tokens.refreshToken
278
+ },
279
+ req
280
+ }),
281
+ token_type: 'Bearer'
282
+ }, {
283
+ headers: {
284
+ 'Cache-Control': 'no-store'
285
+ }
286
+ });
287
+ };
288
+ const redeemCode = ({ clientID, form, req })=>{
289
+ const code = unseal({
290
+ type: 'code',
291
+ req,
292
+ value: form.get('code')
293
+ });
294
+ if (form.get('grant_type') === 'authorization_code' && code?.clientID === clientID && code.redirectURI === form.get('redirect_uri') && // PKCE: only the client that started the sign-in knows the verifier
295
+ createHash('sha256').update(form.get('code_verifier') ?? '').digest('base64url') === code.codeChallenge) {
296
+ return code.tokens;
297
+ }
298
+ };
299
+ const refresh = async ({ clientID, form, req })=>{
300
+ const refreshToken = unseal({
301
+ type: 'refresh',
302
+ req,
303
+ value: form.get('refresh_token')
304
+ });
305
+ // Only the client a refresh token was issued to can use it
306
+ if (refreshToken?.clientID === clientID) {
307
+ return await getTokenHooks(req).refreshToken?.({
308
+ refreshToken: refreshToken.refreshToken,
309
+ req
310
+ });
311
+ }
312
+ };
313
+ /** `issueToken` and `refreshToken`, or the defaults for Payload login tokens. */ const getTokenHooks = (req)=>{
314
+ const oauth = getPluginConfig({
315
+ config: req.payload.config
316
+ }).oauth;
317
+ return oauth.issueToken ? {
318
+ issueToken: oauth.issueToken,
319
+ refreshToken: oauth.refreshToken
320
+ } : {
321
+ issueToken: issueLoginToken,
322
+ refreshToken: refreshLoginToken
323
+ };
324
+ };
325
+ const redirect = ({ params, redirectURI })=>{
326
+ const location = new URL(redirectURI);
327
+ for (const [key, value] of Object.entries(params)){
328
+ if (value) {
329
+ location.searchParams.set(key, value);
330
+ }
331
+ }
332
+ return new Response(null, {
333
+ headers: {
334
+ Location: location.href
335
+ },
336
+ status: 302
337
+ });
338
+ };
339
+ const page = ({ content, status, title })=>new Response(`<!doctype html>
340
+ <html lang="en">
341
+ <head>
342
+ <meta charset="utf-8">
343
+ <meta name="viewport" content="width=device-width, initial-scale=1">
344
+ <title>${escapeHTML(title)}</title>
345
+ <style>
346
+ body { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }
347
+ button { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }
348
+ </style>
349
+ </head>
350
+ <body><h1>${escapeHTML(title)}</h1>${content}</body>
351
+ </html>`, {
352
+ headers: {
353
+ // Stop other sites from framing the page to trick users into clicking Allow
354
+ 'Content-Security-Policy': "frame-ancestors 'none'",
355
+ 'Content-Type': 'text/html; charset=utf-8'
356
+ },
357
+ status
358
+ });
359
+ const escapeHTML = (value)=>value.replace(/[&<>"']/g, (character)=>`&#${character.charCodeAt(0)};`);
360
+
361
+ //# sourceMappingURL=oauthServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoint/oauthServer.ts"],"sourcesContent":["import type {\n OAuthClientInformationFull,\n OAuthTokens,\n OpenIdProviderDiscoveryMetadata,\n} from '@modelcontextprotocol/server'\nimport type { Endpoint, PayloadHandler, PayloadRequest } from 'payload'\n\nimport { OAuthErrorCode } from '@modelcontextprotocol/server'\nimport { createHash } from 'crypto'\nimport { formatAdminURL } from 'payload/shared'\n\nimport type { MCPBuiltinOAuthConfig, MCPOAuthTokens } from '../types.js'\n\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\nimport { seal, unseal } from '../utils/seal.js'\nimport { getMCPResource } from './oauth.js'\nimport { issueLoginToken, refreshLoginToken } from './oauthLoginToken.js'\n\ntype Client = { name: string; redirectURIs: string[] }\n\ntype Consent = {\n clientID: string\n codeChallenge: string\n collection: string\n redirectURI: string\n state: null | string\n userID: number | string\n}\n\ntype Code = {\n clientID: string\n codeChallenge: string\n redirectURI: string\n tokens: MCPOAuthTokens\n}\n\ntype RefreshToken = {\n clientID: string\n refreshToken: string\n}\n\n/**\n * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is\n * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and\n * the client receives the tokens `issueToken` creates.\n *\n * Clients, consent forms, authorization codes, and refresh tokens are encrypted and handed to the\n * client instead of being stored. Only the default tokens write to the database, to the user's\n * login sessions.\n */\nexport const oauthServerEndpoints: Endpoint[] = [\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, (issuer) => metadataResponse(issuer)),\n method: 'get',\n // OpenID discovery is the only metadata location MCP clients check below the issuer's path,\n // which lets this work under /api without a route at the site root\n path: '/mcp/oauth/.well-known/openid-configuration',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => Response.json({ keys: [] })),\n method: 'get',\n path: '/mcp/oauth/jwks',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => register(req)),\n method: 'post',\n path: '/mcp/oauth/register',\n },\n {\n handler: (req) => withIssuer(req, () => authorize(req)),\n method: 'get',\n path: '/mcp/oauth/authorize',\n },\n {\n handler: (req) => withIssuer(req, () => approve(req)),\n method: 'post',\n path: '/mcp/oauth/authorize',\n },\n {\n disableAuth: true,\n handler: (req) => withIssuer(req, () => token(req)),\n method: 'post',\n path: '/mcp/oauth/token',\n },\n]\n\n/** The authorization server URL, or `undefined` when Payload isn't the authorization server. */\nconst getIssuer = (req: PayloadRequest): string | undefined =>\n getPluginConfig({ config: req.payload.config }).oauth?.type === 'builtin'\n ? `${getMCPResource({ req })}/oauth`\n : undefined\n\nconst withIssuer = (\n req: PayloadRequest,\n handler: (issuer: string) => Promise<Response> | Response,\n): Promise<Response> | Response => {\n const issuer = getIssuer(req)\n return issuer ? handler(issuer) : new Response(null, { status: 404 })\n}\n\n/**\n * The user logged in to this browser, who can allow MCP clients. Tokens and API keys in the\n * `Authorization` header can't, or an MCP client could use its access token to approve itself a\n * new, separate login.\n */\nconst getBrowserUser = (req: PayloadRequest): PayloadRequest['user'] =>\n req.headers.has('Authorization') ? null : req.user\n\nconst metadataResponse = (issuer: string): Response =>\n Response.json(\n {\n authorization_endpoint: `${issuer}/authorize`,\n code_challenge_methods_supported: ['S256'],\n grant_types_supported: ['authorization_code', 'refresh_token'],\n // Required by OpenID discovery. No ID tokens are issued.\n id_token_signing_alg_values_supported: ['RS256'],\n issuer,\n jwks_uri: `${issuer}/jwks`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: ['code'],\n subject_types_supported: ['public'],\n token_endpoint: `${issuer}/token`,\n token_endpoint_auth_methods_supported: ['none'],\n } satisfies OpenIdProviderDiscoveryMetadata,\n // Not cached, since without `serverURL` its URLs come from the request\n { headers: { 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' } },\n )\n\nconst register: PayloadHandler = async (req) => {\n const { client_name, redirect_uris } = await req.json!().catch(() => ({}))\n\n if (\n !Array.isArray(redirect_uris) ||\n !redirect_uris.length ||\n !redirect_uris.every(\n (uri) => typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'),\n )\n ) {\n return Response.json({ error: OAuthErrorCode.InvalidRedirectUri }, { status: 400 })\n }\n\n const client: Client = {\n name: typeof client_name === 'string' ? client_name : 'An MCP client',\n redirectURIs: redirect_uris,\n }\n\n return Response.json(\n {\n client_id: seal({ type: 'client', data: client, req }),\n client_name: client.name,\n grant_types: ['authorization_code', 'refresh_token'],\n redirect_uris: client.redirectURIs,\n response_types: ['code'],\n token_endpoint_auth_method: 'none',\n } satisfies OAuthClientInformationFull,\n { status: 201 },\n )\n}\n\nconst authorize: PayloadHandler = (req) => {\n const url = new URL(req.url!)\n const params = url.searchParams\n const clientID = params.get('client_id')\n const client = unseal<Client>({ type: 'client', req, value: clientID })\n const redirectURI = params.get('redirect_uri')\n\n // Never redirect to an address the client didn't register\n if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {\n return page({\n content: '<p>Start the sign-in again from your MCP client.</p>',\n status: 400,\n title: 'This sign-in link is invalid',\n })\n }\n\n const state = params.get('state')\n const codeChallenge = params.get('code_challenge')\n\n if (\n params.get('response_type') !== 'code' ||\n params.get('code_challenge_method') !== 'S256' ||\n !codeChallenge\n ) {\n return redirect({ params: { error: OAuthErrorCode.InvalidRequest, state }, redirectURI })\n }\n\n const user = getBrowserUser(req)\n\n if (!user) {\n const loginURL = formatAdminURL({ adminRoute: req.payload.config.routes.admin, path: '/login' })\n\n return new Response(null, {\n headers: {\n Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`,\n },\n status: 302,\n })\n }\n\n const consent: Consent = {\n clientID: clientID!,\n codeChallenge,\n collection: user.collection,\n redirectURI,\n state,\n userID: user.id,\n }\n\n return page({\n 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>\n<form method=\"post\">\n<input name=\"consent\" type=\"hidden\" value=\"${seal({ type: 'consent', data: consent, expiresIn: 20 * 60 * 60, req })}\">\n<button name=\"decision\" type=\"submit\" value=\"allow\">Allow</button>\n<button name=\"decision\" type=\"submit\" value=\"deny\">Deny</button>\n</form>`,\n status: 200,\n title: `Allow ${client.name} to use your account?`,\n })\n}\n\nconst approve: PayloadHandler = async (req) => {\n const form = new URLSearchParams(await req.text!())\n const consent = unseal<Consent>({ type: 'consent', req, value: form.get('consent') })\n const user = getBrowserUser(req)\n\n // The form is only valid for the user it was shown to\n if (!consent || user?.id !== consent.userID || user.collection !== consent.collection) {\n return page({\n content: '<p>Start the sign-in again from your MCP client.</p>',\n status: 400,\n title: 'This sign-in request expired',\n })\n }\n\n const { clientID, codeChallenge, redirectURI, state } = consent\n\n if (form.get('decision') !== 'allow') {\n return redirect({ params: { error: OAuthErrorCode.AccessDenied, state }, redirectURI })\n }\n\n const code: Code = {\n clientID,\n codeChallenge,\n redirectURI,\n tokens: await getTokenHooks(req).issueToken({ req }),\n }\n\n return redirect({\n params: { code: seal({ type: 'code', data: code, expiresIn: 60, req }), state },\n redirectURI,\n })\n}\n\nconst token: PayloadHandler = async (req) => {\n const form = new URLSearchParams(await req.text!())\n const clientID = form.get('client_id')\n const tokens =\n clientID &&\n (form.get('grant_type') === 'refresh_token'\n ? await refresh({ clientID, form, req })\n : redeemCode({ clientID, form, req }))\n\n if (!clientID || !tokens) {\n return Response.json({ error: OAuthErrorCode.InvalidGrant }, { status: 400 })\n }\n\n return Response.json(\n {\n access_token: tokens.accessToken,\n expires_in: tokens.expiresIn,\n refresh_token:\n tokens.refreshToken &&\n seal({\n type: 'refresh',\n data: { clientID, refreshToken: tokens.refreshToken } satisfies RefreshToken,\n req,\n }),\n token_type: 'Bearer',\n } satisfies OAuthTokens,\n { headers: { 'Cache-Control': 'no-store' } },\n )\n}\n\nconst redeemCode = ({\n clientID,\n form,\n req,\n}: {\n clientID: string\n form: URLSearchParams\n req: PayloadRequest\n}): MCPOAuthTokens | undefined => {\n const code = unseal<Code>({ type: 'code', req, value: form.get('code') })\n\n if (\n form.get('grant_type') === 'authorization_code' &&\n code?.clientID === clientID &&\n code.redirectURI === form.get('redirect_uri') &&\n // PKCE: only the client that started the sign-in knows the verifier\n createHash('sha256')\n .update(form.get('code_verifier') ?? '')\n .digest('base64url') === code.codeChallenge\n ) {\n return code.tokens\n }\n}\n\nconst refresh = async ({\n clientID,\n form,\n req,\n}: {\n clientID: string\n form: URLSearchParams\n req: PayloadRequest\n}): Promise<MCPOAuthTokens | null | undefined> => {\n const refreshToken = unseal<RefreshToken>({\n type: 'refresh',\n req,\n value: form.get('refresh_token'),\n })\n\n // Only the client a refresh token was issued to can use it\n if (refreshToken?.clientID === clientID) {\n return await getTokenHooks(req).refreshToken?.({\n refreshToken: refreshToken.refreshToken,\n req,\n })\n }\n}\n\n/** `issueToken` and `refreshToken`, or the defaults for Payload login tokens. */\nconst getTokenHooks = (req: PayloadRequest) => {\n const oauth = getPluginConfig({ config: req.payload.config }).oauth as MCPBuiltinOAuthConfig\n\n return oauth.issueToken\n ? { issueToken: oauth.issueToken, refreshToken: oauth.refreshToken }\n : { issueToken: issueLoginToken, refreshToken: refreshLoginToken }\n}\n\nconst redirect = ({\n params,\n redirectURI,\n}: {\n params: Record<string, null | string>\n redirectURI: string\n}): Response => {\n const location = new URL(redirectURI)\n\n for (const [key, value] of Object.entries(params)) {\n if (value) {\n location.searchParams.set(key, value)\n }\n }\n\n return new Response(null, { headers: { Location: location.href }, status: 302 })\n}\n\nconst page = ({\n content,\n status,\n title,\n}: {\n content: string\n status: number\n title: string\n}): Response =>\n new Response(\n `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHTML(title)}</title>\n<style>\nbody { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }\nbutton { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }\n</style>\n</head>\n<body><h1>${escapeHTML(title)}</h1>${content}</body>\n</html>`,\n {\n headers: {\n // Stop other sites from framing the page to trick users into clicking Allow\n 'Content-Security-Policy': \"frame-ancestors 'none'\",\n 'Content-Type': 'text/html; charset=utf-8',\n },\n status,\n },\n )\n\nconst escapeHTML = (value: string): string =>\n value.replace(/[&<>\"']/g, (character) => `&#${character.charCodeAt(0)};`)\n"],"names":["OAuthErrorCode","createHash","formatAdminURL","getPluginConfig","seal","unseal","getMCPResource","issueLoginToken","refreshLoginToken","oauthServerEndpoints","disableAuth","handler","req","withIssuer","issuer","metadataResponse","method","path","Response","json","keys","register","authorize","approve","token","getIssuer","config","payload","oauth","type","undefined","status","getBrowserUser","headers","has","user","authorization_endpoint","code_challenge_methods_supported","grant_types_supported","id_token_signing_alg_values_supported","jwks_uri","registration_endpoint","response_types_supported","subject_types_supported","token_endpoint","token_endpoint_auth_methods_supported","client_name","redirect_uris","catch","Array","isArray","length","every","uri","URL","canParse","includes","error","InvalidRedirectUri","client","name","redirectURIs","client_id","data","grant_types","response_types","token_endpoint_auth_method","url","params","searchParams","clientID","get","value","redirectURI","page","content","title","state","codeChallenge","redirect","InvalidRequest","loginURL","adminRoute","routes","admin","Location","encodeURIComponent","pathname","search","consent","collection","userID","id","escapeHTML","String","email","host","expiresIn","form","URLSearchParams","text","AccessDenied","code","tokens","getTokenHooks","issueToken","refresh","redeemCode","InvalidGrant","access_token","accessToken","expires_in","refresh_token","refreshToken","token_type","update","digest","location","key","Object","entries","set","href","replace","character","charCodeAt"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js';
1
+ import type { AuthorizedMCP, MCPBuiltinOAuthConfig, MCPExternalOAuthConfig, MCPOAuthConfig, MCPOAuthTokens, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js';
2
2
  declare module 'payload' {
3
3
  interface PayloadRequestAPI {
4
4
  MCP: true;
@@ -7,8 +7,16 @@ declare module 'payload' {
7
7
  /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */
8
8
  '@payloadcms/plugin-mcp': MCPPluginConfig;
9
9
  }
10
+ interface RequestContext {
11
+ /** Set on MCP requests before they're authenticated. */
12
+ mcp?: {
13
+ /** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */
14
+ resource: string;
15
+ };
16
+ }
10
17
  }
11
- export type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig };
18
+ export type { AuthorizedMCP, MCPBuiltinOAuthConfig, MCPExternalOAuthConfig, MCPOAuthConfig, MCPOAuthTokens, MCPPluginConfig, SanitizedMCPPluginConfig, };
12
19
  export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js';
20
+ export { oauthProtectedResourceHandler } from './endpoint/oauth.js';
13
21
  export declare const mcpPlugin: (options: MCPPluginConfig) => import("payload").Plugin;
14
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAK1F,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAiB,iBAAiB;QAChC,GAAG,EAAE,IAAI,CAAA;KACV;IACD,UAAU,iBAAiB;QACzB,uFAAuF;QACvF,wBAAwB,EAAE,eAAe,CAAA;KAC1C;CACF;AAED,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB,EAAE,CAAA;AAExE,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAElG,eAAO,MAAM,SAAS,wDAiCpB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,eAAe,EACf,wBAAwB,EACzB,MAAM,YAAY,CAAA;AAOnB,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAiB,iBAAiB;QAChC,GAAG,EAAE,IAAI,CAAA;KACV;IACD,UAAU,iBAAiB;QACzB,uFAAuF;QACvF,wBAAwB,EAAE,eAAe,CAAA;KAC1C;IACD,UAAiB,cAAc;QAC7B,wDAAwD;QACxD,GAAG,CAAC,EAAE;YACJ,oFAAoF;YACpF,QAAQ,EAAE,MAAM,CAAA;SACjB,CAAA;KACF;CACF;AAED,YAAY,EACV,aAAa,EACb,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,cAAc,EACd,eAAe,EACf,wBAAwB,GACzB,CAAA;AAED,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAClG,OAAO,EAAE,6BAA6B,EAAE,MAAM,qBAAqB,CAAA;AAEnE,eAAO,MAAM,SAAS,wDA0CpB,CAAA"}
package/dist/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { definePlugin } from 'payload';
2
2
  import { mcpEndpoint } from './endpoint/index.js';
3
+ import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js';
4
+ import { oauthServerEndpoints } from './endpoint/oauthServer.js';
3
5
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js';
4
6
  export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js';
7
+ export { oauthProtectedResourceHandler } from './endpoint/oauth.js';
5
8
  export const mcpPlugin = definePlugin({
6
9
  slug: '@payloadcms/plugin-mcp',
7
10
  order: 10,
@@ -23,8 +26,10 @@ export const mcpPlugin = definePlugin({
23
26
  ...config,
24
27
  endpoints: [
25
28
  ...config.endpoints ?? [],
26
- // Payload prefixes /api, so the full path is /api/mcp.
29
+ // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
30
+ // request with dev auto-login turned off, so Payload doesn't authenticate it first.
27
31
  {
32
+ disableAuth: true,
28
33
  handler: mcpEndpoint,
29
34
  method: 'post',
30
35
  path: '/mcp'
@@ -32,6 +37,7 @@ export const mcpPlugin = definePlugin({
32
37
  // Streamable HTTP's optional server=>client GET stream. We don't offer
33
38
  // one, so answer 405 per spec-— clients then skip it cleanly
34
39
  {
40
+ disableAuth: true,
35
41
  handler: ()=>new Response(null, {
36
42
  headers: {
37
43
  Allow: 'POST'
@@ -40,7 +46,14 @@ export const mcpPlugin = definePlugin({
40
46
  }),
41
47
  method: 'get',
42
48
  path: '/mcp'
43
- }
49
+ },
50
+ {
51
+ disableAuth: true,
52
+ handler: oauthProtectedResourceEndpoint,
53
+ method: 'get',
54
+ path: '/mcp/oauth-protected-resource'
55
+ },
56
+ ...oauthServerEndpoints
44
57
  ]
45
58
  };
46
59
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { definePlugin } from 'payload'\n\nimport type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'\n\nimport { mcpEndpoint } from './endpoint/index.js'\nimport { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'\n\ndeclare module 'payload' {\n export interface PayloadRequestAPI {\n MCP: true\n }\n interface RegisteredPlugins {\n /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */\n '@payloadcms/plugin-mcp': MCPPluginConfig\n }\n}\n\nexport type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig }\n\nexport { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'\n\nexport const mcpPlugin = definePlugin<MCPPluginConfig>({\n slug: '@payloadcms/plugin-mcp',\n order: 10,\n plugin: ({ config, options, plugins }) => {\n const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: options })\n\n // Stash the sanitized config on plugin options so `getPluginConfig()` reads it.\n const registered = plugins['@payloadcms/plugin-mcp']\n if (registered) {\n // @ts-expect-error\n registered.sanitizedOptions = pluginConfig as unknown as typeof registered.options\n }\n\n if (pluginConfig.disabled) {\n return config\n }\n\n return {\n ...config,\n endpoints: [\n ...(config.endpoints ?? []),\n // Payload prefixes /api, so the full path is /api/mcp.\n { handler: mcpEndpoint, method: 'post', path: '/mcp' },\n // Streamable HTTP's optional server=>client GET stream. We don't offer\n // one, so answer 405 per spec-— clients then skip it cleanly\n {\n handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),\n method: 'get',\n path: '/mcp',\n },\n ],\n }\n },\n})\n"],"names":["definePlugin","mcpEndpoint","sanitizeMCPConfig","defineCollectionTool","defineGlobalTool","definePrompt","defineTool","mcpPlugin","slug","order","plugin","config","options","plugins","pluginConfig","registered","sanitizedOptions","disabled","endpoints","handler","method","path","Response","headers","Allow","status"],"mappings":"AAAA,SAASA,YAAY,QAAQ,UAAS;AAItC,SAASC,WAAW,QAAQ,sBAAqB;AACjD,SAASC,iBAAiB,QAAQ,6BAA4B;AAc9D,SAASC,oBAAoB,EAAEC,gBAAgB,EAAEC,YAAY,EAAEC,UAAU,QAAQ,kBAAiB;AAElG,OAAO,MAAMC,YAAYP,aAA8B;IACrDQ,MAAM;IACNC,OAAO;IACPC,QAAQ,CAAC,EAAEC,MAAM,EAAEC,OAAO,EAAEC,OAAO,EAAE;QACnC,MAAMC,eAAeZ,kBAAkB;YAAES;YAAQG,cAAcF;QAAQ;QAEvE,gFAAgF;QAChF,MAAMG,aAAaF,OAAO,CAAC,yBAAyB;QACpD,IAAIE,YAAY;YACd,mBAAmB;YACnBA,WAAWC,gBAAgB,GAAGF;QAChC;QAEA,IAAIA,aAAaG,QAAQ,EAAE;YACzB,OAAON;QACT;QAEA,OAAO;YACL,GAAGA,MAAM;YACTO,WAAW;mBACLP,OAAOO,SAAS,IAAI,EAAE;gBAC1B,uDAAuD;gBACvD;oBAAEC,SAASlB;oBAAamB,QAAQ;oBAAQC,MAAM;gBAAO;gBACrD,uEAAuE;gBACvE,6DAA6D;gBAC7D;oBACEF,SAAS,IAAM,IAAIG,SAAS,MAAM;4BAAEC,SAAS;gCAAEC,OAAO;4BAAO;4BAAGC,QAAQ;wBAAI;oBAC5EL,QAAQ;oBACRC,MAAM;gBACR;aACD;QACH;IACF;AACF,GAAE"}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { definePlugin } from 'payload'\n\nimport type {\n AuthorizedMCP,\n MCPBuiltinOAuthConfig,\n MCPExternalOAuthConfig,\n MCPOAuthConfig,\n MCPOAuthTokens,\n MCPPluginConfig,\n SanitizedMCPPluginConfig,\n} from './types.js'\n\nimport { mcpEndpoint } from './endpoint/index.js'\nimport { oauthProtectedResourceEndpoint } from './endpoint/oauth.js'\nimport { oauthServerEndpoints } from './endpoint/oauthServer.js'\nimport { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'\n\ndeclare module 'payload' {\n export interface PayloadRequestAPI {\n MCP: true\n }\n interface RegisteredPlugins {\n /** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */\n '@payloadcms/plugin-mcp': MCPPluginConfig\n }\n export interface RequestContext {\n /** Set on MCP requests before they're authenticated. */\n mcp?: {\n /** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */\n resource: string\n }\n }\n}\n\nexport type {\n AuthorizedMCP,\n MCPBuiltinOAuthConfig,\n MCPExternalOAuthConfig,\n MCPOAuthConfig,\n MCPOAuthTokens,\n MCPPluginConfig,\n SanitizedMCPPluginConfig,\n}\n\nexport { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'\nexport { oauthProtectedResourceHandler } from './endpoint/oauth.js'\n\nexport const mcpPlugin = definePlugin<MCPPluginConfig>({\n slug: '@payloadcms/plugin-mcp',\n order: 10,\n plugin: ({ config, options, plugins }) => {\n const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: options })\n\n // Stash the sanitized config on plugin options so `getPluginConfig()` reads it.\n const registered = plugins['@payloadcms/plugin-mcp']\n if (registered) {\n // @ts-expect-error\n registered.sanitizedOptions = pluginConfig\n }\n\n if (pluginConfig.disabled) {\n return config\n }\n\n return {\n ...config,\n endpoints: [\n ...(config.endpoints ?? []),\n // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the\n // request with dev auto-login turned off, so Payload doesn't authenticate it first.\n { disableAuth: true, handler: mcpEndpoint, method: 'post', path: '/mcp' },\n // Streamable HTTP's optional server=>client GET stream. We don't offer\n // one, so answer 405 per spec-— clients then skip it cleanly\n {\n disableAuth: true,\n handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),\n method: 'get',\n path: '/mcp',\n },\n {\n disableAuth: true,\n handler: oauthProtectedResourceEndpoint,\n method: 'get',\n path: '/mcp/oauth-protected-resource',\n },\n ...oauthServerEndpoints,\n ],\n }\n },\n})\n"],"names":["definePlugin","mcpEndpoint","oauthProtectedResourceEndpoint","oauthServerEndpoints","sanitizeMCPConfig","defineCollectionTool","defineGlobalTool","definePrompt","defineTool","oauthProtectedResourceHandler","mcpPlugin","slug","order","plugin","config","options","plugins","pluginConfig","registered","sanitizedOptions","disabled","endpoints","disableAuth","handler","method","path","Response","headers","Allow","status"],"mappings":"AAAA,SAASA,YAAY,QAAQ,UAAS;AAYtC,SAASC,WAAW,QAAQ,sBAAqB;AACjD,SAASC,8BAA8B,QAAQ,sBAAqB;AACpE,SAASC,oBAAoB,QAAQ,4BAA2B;AAChE,SAASC,iBAAiB,QAAQ,6BAA4B;AA6B9D,SAASC,oBAAoB,EAAEC,gBAAgB,EAAEC,YAAY,EAAEC,UAAU,QAAQ,kBAAiB;AAClG,SAASC,6BAA6B,QAAQ,sBAAqB;AAEnE,OAAO,MAAMC,YAAYV,aAA8B;IACrDW,MAAM;IACNC,OAAO;IACPC,QAAQ,CAAC,EAAEC,MAAM,EAAEC,OAAO,EAAEC,OAAO,EAAE;QACnC,MAAMC,eAAeb,kBAAkB;YAAEU;YAAQG,cAAcF;QAAQ;QAEvE,gFAAgF;QAChF,MAAMG,aAAaF,OAAO,CAAC,yBAAyB;QACpD,IAAIE,YAAY;YACd,mBAAmB;YACnBA,WAAWC,gBAAgB,GAAGF;QAChC;QAEA,IAAIA,aAAaG,QAAQ,EAAE;YACzB,OAAON;QACT;QAEA,OAAO;YACL,GAAGA,MAAM;YACTO,WAAW;mBACLP,OAAOO,SAAS,IAAI,EAAE;gBAC1B,0FAA0F;gBAC1F,oFAAoF;gBACpF;oBAAEC,aAAa;oBAAMC,SAAStB;oBAAauB,QAAQ;oBAAQC,MAAM;gBAAO;gBACxE,uEAAuE;gBACvE,6DAA6D;gBAC7D;oBACEH,aAAa;oBACbC,SAAS,IAAM,IAAIG,SAAS,MAAM;4BAAEC,SAAS;gCAAEC,OAAO;4BAAO;4BAAGC,QAAQ;wBAAI;oBAC5EL,QAAQ;oBACRC,MAAM;gBACR;gBACA;oBACEH,aAAa;oBACbC,SAASrB;oBACTsB,QAAQ;oBACRC,MAAM;gBACR;mBACGtB;aACJ;QACH;IACF;AACF,GAAE"}
@@ -1,5 +1,5 @@
1
+ import type { PayloadRequest } from 'payload';
1
2
  import { McpServer } from '@modelcontextprotocol/server';
2
- import { type PayloadRequest } from 'payload';
3
3
  import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
4
4
  /**
5
5
  * Serving-entry-agnostic core: registers every authorized MCP item onto a fresh
@@ -9,7 +9,7 @@ import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
9
9
  *
10
10
  * `req` is the request context handlers see. For HTTP it's the live
11
11
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
12
- * synthesized one built via `createLocalReq`.
12
+ * synthesized one built via `createPayloadRequest`.
13
13
  */
14
14
  export declare const buildMcpServer: ({ authorizedMCP, pluginConfig, req, }: {
15
15
  authorizedMCP: AuthorizedMCP;
@@ -1 +1 @@
1
- {"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAA;AAC5E,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,SAAS,CAAA;AAGvD,OAAO,KAAK,EACV,aAAa,EAMb,wBAAwB,EAEzB,MAAM,aAAa,CAAA;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0CAIxB;IACD,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,wBAAwB,CAAA;IACtC,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,SAkNH,CAAA"}
1
+ {"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAA;AAG5E,OAAO,KAAK,EACV,aAAa,EAMb,wBAAwB,EAEzB,MAAM,aAAa,CAAA;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0CAIxB;IACD,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,wBAAwB,CAAA;IACtC,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,SAgNH,CAAA"}
@@ -1,6 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
- import { APIError } from 'payload';
3
- import { z } from 'zod';
2
+ import { APIError, strictObject, z } from 'payload';
4
3
  import { getLogger } from '../utils/getLogger.js';
5
4
  import { toStandardSchema } from '../utils/toStandardSchema.js';
6
5
  /**
@@ -11,8 +10,9 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
11
10
  *
12
11
  * `req` is the request context handlers see. For HTTP it's the live
13
12
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
14
- * synthesized one built via `createLocalReq`.
13
+ * synthesized one built via `createPayloadRequest`.
15
14
  */ export const buildMcpServer = ({ authorizedMCP, pluginConfig, req })=>{
15
+ z.config(z.locales.en());
16
16
  const serverOptions = pluginConfig.mcp?.serverOptions || {};
17
17
  const server = new McpServer({
18
18
  name: 'Payload MCP Server',
@@ -40,20 +40,19 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
40
40
  };
41
41
  /**
42
42
  * Runs a collection/global tool call:
43
- * - reads `collectionSlug` / `globalSlug` from the input
43
+ * - reads `slug` from the input
44
44
  * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug
45
45
  * - runs the tool handler and finalizes its response
46
46
  */ const callEntityTool = async ({ input, item, serverContext })=>{
47
47
  const entity = item.type === 'collectionTool' ? 'collection' : 'global';
48
- const slugKey = item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug';
49
48
  const toolInput = input ?? {};
50
- const slug = toolInput[slugKey];
49
+ const slug = toolInput.slug;
51
50
  if (!slug) {
52
51
  return {
53
52
  content: [
54
53
  {
55
54
  type: 'text',
56
- text: `Error: "${item.mcpName}" requires ${slugKey}. Use getConfigInfo to inspect ${entity} slugs.`
55
+ text: `Error: "${item.mcpName}" requires slug. Use getConfigInfo to inspect ${entity} slugs.`
57
56
  }
58
57
  ],
59
58
  isError: true
@@ -79,10 +78,10 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
79
78
  };
80
79
  const response = await (match.type === 'collectionTool' ? match.tool.handler({
81
80
  ...handlerArgs,
82
- collectionSlug: slug
81
+ slug
83
82
  }) : match.tool.handler({
84
83
  ...handlerArgs,
85
- globalSlug: slug
84
+ slug
86
85
  }));
87
86
  return finalizeToolResponse({
88
87
  input: toolInput,
@@ -103,7 +102,6 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
103
102
  }
104
103
  registeredEntityTools.add(item.mcpName);
105
104
  const inputSchema = withSlugInput({
106
- name: item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug',
107
105
  input: item.tool.input
108
106
  });
109
107
  server.registerTool(item.mcpName, {
@@ -189,33 +187,47 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
189
187
  }
190
188
  return server;
191
189
  };
192
- const withSlugInput = ({ name, input })=>{
193
- const description = name === 'collectionSlug' ? 'The collection slug' : 'The global slug';
194
- const slugSchema = z.string().describe(description);
190
+ const withSlugInput = ({ input })=>{
191
+ const description = 'The target slug.';
192
+ const slugSchema = z.string().check(z.describe(description));
195
193
  if (!input) {
196
- return z.object({
197
- [name]: slugSchema
194
+ return strictObject({
195
+ slug: slugSchema
198
196
  });
199
197
  }
200
- if (input instanceof z.ZodObject) {
201
- return input.extend({
202
- [name]: slugSchema
198
+ if (typeof input === 'object' && input !== null && '~standard' in input) {
199
+ const schema = input['~standard'].jsonSchema.input({
200
+ target: 'draft-2020-12'
201
+ });
202
+ const properties = schema.properties;
203
+ if (properties?.slug) {
204
+ return input;
205
+ }
206
+ return addSlugToJSONSchema({
207
+ description,
208
+ schema
203
209
  });
204
210
  }
205
- const schema = input;
211
+ return addSlugToJSONSchema({
212
+ description,
213
+ schema: input
214
+ });
215
+ };
216
+ const addSlugToJSONSchema = ({ description, schema })=>{
217
+ const objectSchema = schema;
206
218
  return {
207
- ...schema,
219
+ ...objectSchema,
208
220
  type: 'object',
209
221
  properties: {
210
- ...schema.properties,
211
- [name]: {
222
+ ...objectSchema.properties,
223
+ slug: {
212
224
  type: 'string',
213
225
  description
214
226
  }
215
227
  },
216
228
  required: Array.from(new Set([
217
- name,
218
- ...schema.required ?? []
229
+ 'slug',
230
+ ...objectSchema.required ?? []
219
231
  ]))
220
232
  };
221
233
  };