@payloadcms/plugin-mcp 4.0.0-internal.934870d → 4.0.0-internal.970009e

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 (64) hide show
  1. package/dist/endpoint/access.d.ts.map +1 -1
  2. package/dist/endpoint/access.js +15 -3
  3. package/dist/endpoint/access.js.map +1 -1
  4. package/dist/endpoint/index.d.ts.map +1 -1
  5. package/dist/endpoint/index.js +13 -0
  6. package/dist/endpoint/index.js.map +1 -1
  7. package/dist/endpoint/oauth.d.ts +39 -0
  8. package/dist/endpoint/oauth.d.ts.map +1 -0
  9. package/dist/endpoint/oauth.js +92 -0
  10. package/dist/endpoint/oauth.js.map +1 -0
  11. package/dist/endpoint/oauthLoginToken.d.ts +20 -0
  12. package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
  13. package/dist/endpoint/oauthLoginToken.js +158 -0
  14. package/dist/endpoint/oauthLoginToken.js.map +1 -0
  15. package/dist/endpoint/oauthServer.d.ts +12 -0
  16. package/dist/endpoint/oauthServer.d.ts.map +1 -0
  17. package/dist/endpoint/oauthServer.js +377 -0
  18. package/dist/endpoint/oauthServer.js.map +1 -0
  19. package/dist/index.d.ts +10 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +15 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp/buildMcpServer.js +1 -1
  24. package/dist/mcp/buildMcpServer.js.map +1 -1
  25. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  26. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  27. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  28. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  29. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  30. package/dist/mcp/builtin/collections/findTool.js +1 -1
  31. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  32. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  33. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  34. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -1
  35. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  36. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  37. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  38. package/dist/mcp/sanitizeMCPConfig.js +3 -0
  39. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  40. package/dist/types.d.ts +81 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/dist/utils/toStandardSchema.js.map +1 -1
  44. package/package.json +20 -23
  45. package/src/endpoint/access.ts +12 -3
  46. package/src/endpoint/index.ts +8 -0
  47. package/src/endpoint/oauth.ts +91 -0
  48. package/src/endpoint/oauthLoginToken.ts +210 -0
  49. package/src/endpoint/oauthServer.ts +428 -0
  50. package/src/index.ts +40 -5
  51. package/src/mcp/buildMcpServer.ts +2 -2
  52. package/src/mcp/builtin/collections/authTools.ts +2 -2
  53. package/src/mcp/builtin/collections/createTool.ts +1 -3
  54. package/src/mcp/builtin/collections/deleteTool.ts +1 -1
  55. package/src/mcp/builtin/collections/duplicateTool.ts +1 -1
  56. package/src/mcp/builtin/collections/findTool.ts +1 -1
  57. package/src/mcp/builtin/collections/restoreVersionTool.ts +1 -1
  58. package/src/mcp/builtin/collections/updateTool.ts +1 -1
  59. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +1 -1
  60. package/src/mcp/builtin/globals/findTool.ts +1 -1
  61. package/src/mcp/builtin/globals/updateTool.ts +1 -1
  62. package/src/mcp/sanitizeMCPConfig.ts +1 -0
  63. package/src/types.ts +82 -0
  64. package/src/utils/toStandardSchema.ts +1 -1
@@ -0,0 +1,377 @@
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 { getMCPURL } from './oauth.js';
6
+ import { issueLoginToken, refreshLoginToken } from './oauthLoginToken.js';
7
+ /**
8
+ * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is
9
+ * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and
10
+ * the client receives the tokens `issueToken` creates.
11
+ *
12
+ * Clients, consent forms, authorization codes, and refresh tokens are encrypted and handed to the
13
+ * client instead of being stored. Only the default tokens write to the database, to the user's
14
+ * login sessions.
15
+ */ export const oauthServerEndpoints = [
16
+ {
17
+ disableAuth: true,
18
+ handler: (req)=>withIssuer(req, (issuer)=>metadataResponse(issuer)),
19
+ method: 'get',
20
+ // OpenID discovery is the only metadata location MCP clients check below the issuer's path,
21
+ // which lets this work under /api without a route at the site root
22
+ path: '/mcp/oauth/.well-known/openid-configuration'
23
+ },
24
+ {
25
+ disableAuth: true,
26
+ handler: (req)=>withIssuer(req, ()=>Response.json({
27
+ keys: []
28
+ })),
29
+ method: 'get',
30
+ path: '/mcp/oauth/jwks'
31
+ },
32
+ {
33
+ disableAuth: true,
34
+ handler: (req)=>withIssuer(req, ()=>register(req)),
35
+ method: 'post',
36
+ path: '/mcp/oauth/register'
37
+ },
38
+ {
39
+ handler: (req)=>withIssuer(req, ()=>authorize(req)),
40
+ method: 'get',
41
+ path: '/mcp/oauth/authorize'
42
+ },
43
+ {
44
+ handler: (req)=>withIssuer(req, ()=>approve(req)),
45
+ method: 'post',
46
+ path: '/mcp/oauth/authorize'
47
+ },
48
+ {
49
+ disableAuth: true,
50
+ handler: (req)=>withIssuer(req, ()=>token(req)),
51
+ method: 'post',
52
+ path: '/mcp/oauth/token'
53
+ }
54
+ ];
55
+ /** The authorization server URL, or `undefined` when Payload isn't the authorization server. */ const getIssuer = (req)=>getPluginConfig({
56
+ config: req.payload.config
57
+ }).oauth?.type === 'builtin' ? `${getMCPURL({
58
+ config: req.payload.config,
59
+ url: req.url
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
+ const metadataResponse = (issuer)=>Response.json({
68
+ authorization_endpoint: `${issuer}/authorize`,
69
+ code_challenge_methods_supported: [
70
+ 'S256'
71
+ ],
72
+ grant_types_supported: [
73
+ 'authorization_code',
74
+ 'refresh_token'
75
+ ],
76
+ // Required by OpenID discovery. No ID tokens are issued.
77
+ id_token_signing_alg_values_supported: [
78
+ 'RS256'
79
+ ],
80
+ issuer,
81
+ jwks_uri: `${issuer}/jwks`,
82
+ registration_endpoint: `${issuer}/register`,
83
+ response_types_supported: [
84
+ 'code'
85
+ ],
86
+ subject_types_supported: [
87
+ 'public'
88
+ ],
89
+ token_endpoint: `${issuer}/token`,
90
+ token_endpoint_auth_methods_supported: [
91
+ 'none'
92
+ ]
93
+ }, {
94
+ headers: {
95
+ 'Access-Control-Allow-Origin': '*'
96
+ }
97
+ });
98
+ const register = async (req)=>{
99
+ const { client_name, redirect_uris } = await req.json().catch(()=>({}));
100
+ if (!Array.isArray(redirect_uris) || !redirect_uris.length || !redirect_uris.every((uri)=>typeof uri === 'string' && URL.canParse(uri) && !uri.includes('#'))) {
101
+ return Response.json({
102
+ error: OAuthErrorCode.InvalidRedirectUri
103
+ }, {
104
+ status: 400
105
+ });
106
+ }
107
+ const client = {
108
+ name: typeof client_name === 'string' ? client_name : 'An MCP client',
109
+ redirectURIs: redirect_uris
110
+ };
111
+ return Response.json({
112
+ client_id: seal({
113
+ type: 'client',
114
+ data: client,
115
+ req
116
+ }),
117
+ client_name: client.name,
118
+ grant_types: [
119
+ 'authorization_code',
120
+ 'refresh_token'
121
+ ],
122
+ redirect_uris: client.redirectURIs,
123
+ response_types: [
124
+ 'code'
125
+ ],
126
+ token_endpoint_auth_method: 'none'
127
+ }, {
128
+ status: 201
129
+ });
130
+ };
131
+ const authorize = (req)=>{
132
+ const url = new URL(req.url);
133
+ const params = url.searchParams;
134
+ const clientID = params.get('client_id');
135
+ const client = unseal({
136
+ type: 'client',
137
+ req,
138
+ value: clientID
139
+ });
140
+ const redirectURI = params.get('redirect_uri');
141
+ // Never redirect to an address the client didn't register
142
+ if (!client || !redirectURI || !client.redirectURIs.includes(redirectURI)) {
143
+ return page({
144
+ content: '<p>Start the sign-in again from your MCP client.</p>',
145
+ status: 400,
146
+ title: 'This sign-in link is invalid'
147
+ });
148
+ }
149
+ const state = params.get('state');
150
+ const codeChallenge = params.get('code_challenge');
151
+ if (params.get('response_type') !== 'code' || params.get('code_challenge_method') !== 'S256' || !codeChallenge) {
152
+ return redirect({
153
+ params: {
154
+ error: OAuthErrorCode.InvalidRequest,
155
+ state
156
+ },
157
+ redirectURI
158
+ });
159
+ }
160
+ if (!req.user) {
161
+ const loginURL = formatAdminURL({
162
+ adminRoute: req.payload.config.routes.admin,
163
+ path: '/login'
164
+ });
165
+ return new Response(null, {
166
+ headers: {
167
+ Location: `${loginURL}?redirect=${encodeURIComponent(url.pathname + url.search)}`
168
+ },
169
+ status: 302
170
+ });
171
+ }
172
+ const consent = {
173
+ clientID: clientID,
174
+ codeChallenge,
175
+ collection: req.user.collection,
176
+ redirectURI,
177
+ state,
178
+ userID: req.user.id
179
+ };
180
+ return page({
181
+ content: `<p>It will be able to use this app's MCP tools as ${escapeHTML(String(req.user.email ?? req.user.id))}, and you will return to ${escapeHTML(new URL(redirectURI).host || redirectURI)}.</p>
182
+ <form method="post">
183
+ <input name="consent" type="hidden" value="${seal({
184
+ type: 'consent',
185
+ data: consent,
186
+ expiresIn: 20 * 60 * 60,
187
+ req
188
+ })}">
189
+ <button name="decision" type="submit" value="allow">Allow</button>
190
+ <button name="decision" type="submit" value="deny">Deny</button>
191
+ </form>`,
192
+ status: 200,
193
+ title: `Allow ${client.name} to use your account?`
194
+ });
195
+ };
196
+ const approve = async (req)=>{
197
+ const form = new URLSearchParams(await req.text());
198
+ const consent = unseal({
199
+ type: 'consent',
200
+ req,
201
+ value: form.get('consent')
202
+ });
203
+ // The form is only valid for the user it was shown to
204
+ if (!consent || req.user?.id !== consent.userID || req.user.collection !== consent.collection) {
205
+ return page({
206
+ content: '<p>Start the sign-in again from your MCP client.</p>',
207
+ status: 400,
208
+ title: 'This sign-in request expired'
209
+ });
210
+ }
211
+ const { clientID, codeChallenge, redirectURI, state } = consent;
212
+ if (form.get('decision') !== 'allow') {
213
+ return redirect({
214
+ params: {
215
+ error: OAuthErrorCode.AccessDenied,
216
+ state
217
+ },
218
+ redirectURI
219
+ });
220
+ }
221
+ const code = {
222
+ clientID,
223
+ codeChallenge,
224
+ redirectURI,
225
+ tokens: await getTokenHooks(req).issueToken({
226
+ req
227
+ })
228
+ };
229
+ return redirect({
230
+ params: {
231
+ code: seal({
232
+ type: 'code',
233
+ data: code,
234
+ expiresIn: 60,
235
+ req
236
+ }),
237
+ state
238
+ },
239
+ redirectURI
240
+ });
241
+ };
242
+ const token = async (req)=>{
243
+ const form = new URLSearchParams(await req.text());
244
+ const clientID = form.get('client_id');
245
+ const tokens = clientID && (form.get('grant_type') === 'refresh_token' ? await refresh({
246
+ clientID,
247
+ form,
248
+ req
249
+ }) : redeemCode({
250
+ clientID,
251
+ form,
252
+ req
253
+ }));
254
+ if (!clientID || !tokens) {
255
+ return Response.json({
256
+ error: OAuthErrorCode.InvalidGrant
257
+ }, {
258
+ status: 400
259
+ });
260
+ }
261
+ return Response.json({
262
+ access_token: tokens.accessToken,
263
+ expires_in: tokens.expiresIn,
264
+ refresh_token: tokens.refreshToken && seal({
265
+ type: 'refresh',
266
+ data: {
267
+ clientID,
268
+ refreshToken: tokens.refreshToken
269
+ },
270
+ req
271
+ }),
272
+ token_type: 'Bearer'
273
+ }, {
274
+ headers: {
275
+ 'Cache-Control': 'no-store'
276
+ }
277
+ });
278
+ };
279
+ const redeemCode = ({ clientID, form, req })=>{
280
+ const code = unseal({
281
+ type: 'code',
282
+ req,
283
+ value: form.get('code')
284
+ });
285
+ 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
286
+ createHash('sha256').update(form.get('code_verifier') ?? '').digest('base64url') === code.codeChallenge) {
287
+ return code.tokens;
288
+ }
289
+ };
290
+ const refresh = async ({ clientID, form, req })=>{
291
+ const refreshToken = unseal({
292
+ type: 'refresh',
293
+ req,
294
+ value: form.get('refresh_token')
295
+ });
296
+ // Only the client a refresh token was issued to can use it
297
+ if (refreshToken?.clientID === clientID) {
298
+ return await getTokenHooks(req).refreshToken?.({
299
+ refreshToken: refreshToken.refreshToken,
300
+ req
301
+ });
302
+ }
303
+ };
304
+ /** `issueToken` and `refreshToken`, or the defaults for Payload login tokens. */ const getTokenHooks = (req)=>{
305
+ const oauth = getPluginConfig({
306
+ config: req.payload.config
307
+ }).oauth;
308
+ return oauth.issueToken ? {
309
+ issueToken: oauth.issueToken,
310
+ refreshToken: oauth.refreshToken
311
+ } : {
312
+ issueToken: issueLoginToken,
313
+ refreshToken: refreshLoginToken
314
+ };
315
+ };
316
+ /**
317
+ * Encrypts data for the client to hold, so it doesn't need to be stored. `type` keeps one kind
318
+ * from being used as another, e.g. a client ID as an authorization code.
319
+ */ const seal = ({ type, data, expiresIn, req })=>req.payload.encrypt(JSON.stringify({
320
+ ...data,
321
+ type,
322
+ exp: expiresIn && Date.now() + expiresIn * 1000
323
+ }));
324
+ const unseal = ({ type, req, value })=>{
325
+ // Only the v1 format is authenticated. Older formats decrypt anything without an error.
326
+ if (!value?.startsWith('v1:')) {
327
+ return undefined;
328
+ }
329
+ try {
330
+ // Previous secrets can be public, like a default secret from source code, so only client IDs
331
+ // accept them. Client IDs grant nothing, and this keeps clients registered when the secret changes.
332
+ const secret = type === 'client' ? undefined : req.payload.config.secret;
333
+ const data = JSON.parse(req.payload.decrypt(value, {
334
+ secret
335
+ }));
336
+ return data.type === type && (!data.exp || data.exp > Date.now()) ? data : undefined;
337
+ } catch {
338
+ return undefined;
339
+ }
340
+ };
341
+ const redirect = ({ params, redirectURI })=>{
342
+ const location = new URL(redirectURI);
343
+ for (const [key, value] of Object.entries(params)){
344
+ if (value) {
345
+ location.searchParams.set(key, value);
346
+ }
347
+ }
348
+ return new Response(null, {
349
+ headers: {
350
+ Location: location.href
351
+ },
352
+ status: 302
353
+ });
354
+ };
355
+ const page = ({ content, status, title })=>new Response(`<!doctype html>
356
+ <html lang="en">
357
+ <head>
358
+ <meta charset="utf-8">
359
+ <meta name="viewport" content="width=device-width, initial-scale=1">
360
+ <title>${escapeHTML(title)}</title>
361
+ <style>
362
+ body { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 32rem; overflow-wrap: anywhere; padding: 0 1rem }
363
+ button { font: inherit; margin-inline-end: 0.5rem; padding: 0.5rem 1rem }
364
+ </style>
365
+ </head>
366
+ <body><h1>${escapeHTML(title)}</h1>${content}</body>
367
+ </html>`, {
368
+ headers: {
369
+ // Stop other sites from framing the page to trick users into clicking Allow
370
+ 'Content-Security-Policy': "frame-ancestors 'none'",
371
+ 'Content-Type': 'text/html; charset=utf-8'
372
+ },
373
+ status
374
+ });
375
+ const escapeHTML = (value)=>value.replace(/[&<>"']/g, (character)=>`&#${character.charCodeAt(0)};`);
376
+
377
+ //# 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 { getMCPURL } 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 ? `${getMCPURL({ config: req.payload.config, url: req.url! })}/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\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 { headers: { 'Access-Control-Allow-Origin': '*' } },\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 if (!req.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: req.user.collection,\n redirectURI,\n state,\n userID: req.user.id,\n }\n\n return page({\n content: `<p>It will be able to use this app's MCP tools as ${escapeHTML(String(req.user.email ?? req.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\n // The form is only valid for the user it was shown to\n if (!consent || req.user?.id !== consent.userID || req.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\n/**\n * Encrypts data for the client to hold, so it doesn't need to be stored. `type` keeps one kind\n * from being used as another, e.g. a client ID as an authorization code.\n */\nconst seal = ({\n type,\n data,\n expiresIn,\n req,\n}: {\n data: object\n /** Seconds */\n expiresIn?: number\n req: PayloadRequest\n type: string\n}): string =>\n req.payload.encrypt(\n JSON.stringify({ ...data, type, exp: expiresIn && Date.now() + expiresIn * 1000 }),\n )\n\nconst unseal = <T>({\n type,\n req,\n value,\n}: {\n req: PayloadRequest\n type: string\n value: null | string\n}): T | undefined => {\n // Only the v1 format is authenticated. Older formats decrypt anything without an error.\n if (!value?.startsWith('v1:')) {\n return undefined\n }\n\n try {\n // Previous secrets can be public, like a default secret from source code, so only client IDs\n // accept them. Client IDs grant nothing, and this keeps clients registered when the secret changes.\n const secret = type === 'client' ? undefined : req.payload.config.secret\n const data = JSON.parse(req.payload.decrypt(value, { secret }))\n return data.type === type && (!data.exp || data.exp > Date.now()) ? data : undefined\n } catch {\n return undefined\n }\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","getMCPURL","issueLoginToken","refreshLoginToken","oauthServerEndpoints","disableAuth","handler","req","withIssuer","issuer","metadataResponse","method","path","Response","json","keys","register","authorize","approve","token","getIssuer","config","payload","oauth","type","url","undefined","status","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","headers","client_name","redirect_uris","catch","Array","isArray","length","every","uri","URL","canParse","includes","error","InvalidRedirectUri","client","name","redirectURIs","client_id","seal","data","grant_types","response_types","token_endpoint_auth_method","params","searchParams","clientID","get","unseal","value","redirectURI","page","content","title","state","codeChallenge","redirect","InvalidRequest","user","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","encrypt","JSON","stringify","exp","Date","now","startsWith","secret","parse","decrypt","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"}
@@ -205,7 +205,7 @@ const withSlugInput = ({ input })=>{
205
205
  }
206
206
  return addSlugToJSONSchema({
207
207
  description,
208
- schema: schema
208
+ schema
209
209
  });
210
210
  }
211
211
  return addSlugToJSONSchema({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/mcp/buildMcpServer.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { McpServer, type ServerContext } from '@modelcontextprotocol/server'\nimport { APIError, strictObject, z } from 'payload'\n\nimport type {\n AuthorizedMCP,\n CollectionMCPItem,\n GlobalMCPItem,\n JsonSchemaType,\n MCPResponseOverride,\n MCPToolResponse,\n SanitizedMCPPluginConfig,\n ToolInputSchema,\n} from '../types.js'\n\nimport { getLogger } from '../utils/getLogger.js'\nimport { toStandardSchema } from '../utils/toStandardSchema.js'\n\n/**\n * Serving-entry-agnostic core: registers every authorized MCP item onto a fresh\n * `McpServer` and returns it. The HTTP and stdio entry point callers provide fresh\n * instances from this builder while they own the transport and protocol-era\n * decision.\n *\n * `req` is the request context handlers see. For HTTP it's the live\n * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a\n * synthesized one built via `createPayloadRequest`.\n */\nexport const buildMcpServer = ({\n authorizedMCP,\n pluginConfig,\n req,\n}: {\n authorizedMCP: AuthorizedMCP\n pluginConfig: SanitizedMCPPluginConfig\n req: PayloadRequest\n}): McpServer => {\n z.config(z.locales.en())\n\n const serverOptions = pluginConfig.mcp?.serverOptions || {}\n const server = new McpServer(\n { name: 'Payload MCP Server', version: '1.0.0', ...serverOptions.serverInfo },\n serverOptions.options,\n )\n\n const logger = getLogger({ payload: req.payload })\n\n /**\n * Wrap a tool handler's response with the tool's `overrideResponse`, then\n * strip the internal `doc` field so it doesn't leak onto the wire.\n */\n const finalizeToolResponse = async ({\n input,\n overrideResponse,\n response,\n toolName,\n }: {\n input: unknown\n overrideResponse?: MCPResponseOverride\n response: MCPToolResponse\n toolName: string\n }): Promise<MCPToolResponse> => {\n let overridden = overrideResponse?.(response, response.doc ?? {}, req) ?? response\n for (const hook of pluginConfig.hooks?.afterToolCall ?? []) {\n overridden = await hook({ input, req, response: overridden, toolName })\n }\n const { doc: _doc, ...rest } = overridden\n return rest\n }\n\n /**\n * Runs a collection/global tool call:\n * - reads `slug` from the input\n * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug\n * - runs the tool handler and finalizes its response\n */\n const callEntityTool = async ({\n input,\n item,\n serverContext,\n }: {\n input: unknown\n item: CollectionMCPItem | GlobalMCPItem\n serverContext: ServerContext\n }): Promise<MCPToolResponse> => {\n const entity = item.type === 'collectionTool' ? 'collection' : 'global'\n const toolInput = (input ?? {}) as Record<string, unknown>\n const slug = toolInput.slug as string | undefined\n\n if (!slug) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: \"${item.mcpName}\" requires slug. Use getConfigInfo to inspect ${entity} slugs.`,\n },\n ],\n isError: true,\n }\n }\n\n const match = authorizedMCP.items.find(\n (candidate): candidate is CollectionMCPItem | GlobalMCPItem =>\n candidate.type === item.type &&\n candidate.mcpName === item.mcpName &&\n (candidate.type === 'collectionTool'\n ? candidate.collectionSlug === slug\n : candidate.type === 'globalTool' && candidate.globalSlug === slug),\n )\n\n if (!match) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: MCP access to \"${item.mcpName}\" is not enabled for ${entity} \"${slug}\"`,\n },\n ],\n isError: true,\n }\n }\n\n const handlerArgs = {\n authorizedMCP,\n input: toolInput,\n req,\n serverContext,\n }\n const response = await (match.type === 'collectionTool'\n ? match.tool.handler({ ...handlerArgs, slug })\n : match.tool.handler({ ...handlerArgs, slug }))\n\n return finalizeToolResponse({\n input: toolInput,\n overrideResponse: match.tool.overrideResponse,\n response,\n toolName: match.mcpName,\n })\n }\n\n try {\n const registeredEntityTools = new Set<string>()\n\n for (const item of authorizedMCP.items) {\n switch (item.type) {\n case 'collectionTool':\n case 'globalTool': {\n if (registeredEntityTools.has(item.mcpName)) {\n break\n }\n registeredEntityTools.add(item.mcpName)\n\n const inputSchema = withSlugInput({ input: item.tool.input })\n\n server.registerTool(\n item.mcpName,\n {\n annotations: item.tool.annotations,\n description: item.tool.description,\n inputSchema: toStandardSchema(inputSchema),\n },\n async (input: unknown, ctx: ServerContext) =>\n callEntityTool({ input, item, serverContext: ctx }),\n )\n logger.info(`✅ Tool: ${item.mcpName} Registered.`)\n break\n }\n case 'prompt': {\n const prompt = item.prompt\n server.registerPrompt(\n item.mcpName,\n {\n argsSchema: prompt.argsSchema ? toStandardSchema(prompt.argsSchema) : undefined,\n description: prompt.description,\n title: prompt.title,\n },\n async (input: unknown, ctx: ServerContext) =>\n prompt.handler({\n input: (input ?? {}) as Record<string, unknown>,\n req,\n serverContext: ctx,\n }),\n )\n logger.info(`✅ Prompt: ${prompt.title} Registered.`)\n break\n }\n case 'resource': {\n const resource = item.resource\n server.registerResource(\n item.mcpName,\n // @ts-expect-error - Overload type ambiguity (string OR ResourceTemplate is valid)\n resource.uri,\n {\n description: resource.description,\n mimeType: resource.mimeType,\n title: resource.title,\n },\n // Static URIs call (uri, ctx); ResourceTemplates call (uri, params, ctx).\n // The rest-params shape lets us collect either signature uniformly.\n async (...sdkArgs: unknown[]) => {\n const ctx = sdkArgs[sdkArgs.length - 1] as ServerContext\n const uri = sdkArgs[0] as URL\n const params = (sdkArgs.length > 2 ? sdkArgs[1] : {}) as Record<string, string>\n return resource.handler({ params, req, serverContext: ctx, uri })\n },\n )\n logger.info(`✅ Resource: ${resource.title} Registered.`)\n break\n }\n case 'tool': {\n const tool = item.tool\n server.registerTool(\n item.mcpName,\n {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.input ? toStandardSchema(tool.input) : undefined,\n },\n async (input: unknown, ctx: ServerContext) => {\n const toolInput = (input ?? {}) as Record<string, unknown>\n const response = await tool.handler({\n authorizedMCP,\n input: toolInput,\n req,\n serverContext: ctx,\n })\n return finalizeToolResponse({\n input: toolInput,\n overrideResponse: tool.overrideResponse,\n response,\n toolName: item.mcpName,\n })\n },\n )\n logger.info(`✅ Tool: ${item.mcpName} Registered.`)\n break\n }\n }\n }\n } catch (error) {\n throw new APIError(`Error initializing MCP handler: ${String(error)}`, 500)\n }\n\n return server\n}\n\nconst withSlugInput = ({ input }: { input?: ToolInputSchema }): ToolInputSchema => {\n const description = 'The target slug.'\n const slugSchema = z.string().check(z.describe(description))\n\n if (!input) {\n return strictObject({ slug: slugSchema }) as unknown as ToolInputSchema\n }\n\n if (typeof input === 'object' && input !== null && '~standard' in input) {\n const schema = input['~standard'].jsonSchema.input({ target: 'draft-2020-12' })\n const properties = schema.properties as Record<string, unknown> | undefined\n\n if (properties?.slug) {\n return input\n }\n\n return addSlugToJSONSchema({ description, schema: schema as JsonSchemaType })\n }\n\n return addSlugToJSONSchema({ description, schema: input })\n}\n\nconst addSlugToJSONSchema = ({\n description,\n schema,\n}: {\n description: string\n schema: JsonSchemaType\n}): JsonSchemaType => {\n const objectSchema = schema as {\n properties?: Record<string, JsonSchemaType>\n required?: string[]\n } & JsonSchemaType\n\n return {\n ...objectSchema,\n type: 'object',\n properties: {\n ...objectSchema.properties,\n slug: {\n type: 'string',\n description,\n },\n },\n required: Array.from(new Set(['slug', ...(objectSchema.required ?? [])])),\n }\n}\n"],"names":["McpServer","APIError","strictObject","z","getLogger","toStandardSchema","buildMcpServer","authorizedMCP","pluginConfig","req","config","locales","en","serverOptions","mcp","server","name","version","serverInfo","options","logger","payload","finalizeToolResponse","input","overrideResponse","response","toolName","overridden","doc","hook","hooks","afterToolCall","_doc","rest","callEntityTool","item","serverContext","entity","type","toolInput","slug","content","text","mcpName","isError","match","items","find","candidate","collectionSlug","globalSlug","handlerArgs","tool","handler","registeredEntityTools","Set","has","add","inputSchema","withSlugInput","registerTool","annotations","description","ctx","info","prompt","registerPrompt","argsSchema","undefined","title","resource","registerResource","uri","mimeType","sdkArgs","length","params","error","String","slugSchema","string","check","describe","schema","jsonSchema","target","properties","addSlugToJSONSchema","objectSchema","required","Array","from"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/mcp/buildMcpServer.ts"],"sourcesContent":["import type { PayloadRequest } from 'payload'\n\nimport { McpServer, type ServerContext } from '@modelcontextprotocol/server'\nimport { APIError, strictObject, z } from 'payload'\n\nimport type {\n AuthorizedMCP,\n CollectionMCPItem,\n GlobalMCPItem,\n JsonSchemaType,\n MCPResponseOverride,\n MCPToolResponse,\n SanitizedMCPPluginConfig,\n ToolInputSchema,\n} from '../types.js'\n\nimport { getLogger } from '../utils/getLogger.js'\nimport { toStandardSchema } from '../utils/toStandardSchema.js'\n\n/**\n * Serving-entry-agnostic core: registers every authorized MCP item onto a fresh\n * `McpServer` and returns it. The HTTP and stdio entry point callers provide fresh\n * instances from this builder while they own the transport and protocol-era\n * decision.\n *\n * `req` is the request context handlers see. For HTTP it's the live\n * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a\n * synthesized one built via `createPayloadRequest`.\n */\nexport const buildMcpServer = ({\n authorizedMCP,\n pluginConfig,\n req,\n}: {\n authorizedMCP: AuthorizedMCP\n pluginConfig: SanitizedMCPPluginConfig\n req: PayloadRequest\n}): McpServer => {\n z.config(z.locales.en())\n\n const serverOptions = pluginConfig.mcp?.serverOptions || {}\n const server = new McpServer(\n { name: 'Payload MCP Server', version: '1.0.0', ...serverOptions.serverInfo },\n serverOptions.options,\n )\n\n const logger = getLogger({ payload: req.payload })\n\n /**\n * Wrap a tool handler's response with the tool's `overrideResponse`, then\n * strip the internal `doc` field so it doesn't leak onto the wire.\n */\n const finalizeToolResponse = async ({\n input,\n overrideResponse,\n response,\n toolName,\n }: {\n input: unknown\n overrideResponse?: MCPResponseOverride\n response: MCPToolResponse\n toolName: string\n }): Promise<MCPToolResponse> => {\n let overridden = overrideResponse?.(response, response.doc ?? {}, req) ?? response\n for (const hook of pluginConfig.hooks?.afterToolCall ?? []) {\n overridden = await hook({ input, req, response: overridden, toolName })\n }\n const { doc: _doc, ...rest } = overridden\n return rest\n }\n\n /**\n * Runs a collection/global tool call:\n * - reads `slug` from the input\n * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug\n * - runs the tool handler and finalizes its response\n */\n const callEntityTool = async ({\n input,\n item,\n serverContext,\n }: {\n input: unknown\n item: CollectionMCPItem | GlobalMCPItem\n serverContext: ServerContext\n }): Promise<MCPToolResponse> => {\n const entity = item.type === 'collectionTool' ? 'collection' : 'global'\n const toolInput = (input ?? {}) as Record<string, unknown>\n const slug = toolInput.slug as string | undefined\n\n if (!slug) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: \"${item.mcpName}\" requires slug. Use getConfigInfo to inspect ${entity} slugs.`,\n },\n ],\n isError: true,\n }\n }\n\n const match = authorizedMCP.items.find(\n (candidate): candidate is CollectionMCPItem | GlobalMCPItem =>\n candidate.type === item.type &&\n candidate.mcpName === item.mcpName &&\n (candidate.type === 'collectionTool'\n ? candidate.collectionSlug === slug\n : candidate.type === 'globalTool' && candidate.globalSlug === slug),\n )\n\n if (!match) {\n return {\n content: [\n {\n type: 'text',\n text: `Error: MCP access to \"${item.mcpName}\" is not enabled for ${entity} \"${slug}\"`,\n },\n ],\n isError: true,\n }\n }\n\n const handlerArgs = {\n authorizedMCP,\n input: toolInput,\n req,\n serverContext,\n }\n const response = await (match.type === 'collectionTool'\n ? match.tool.handler({ ...handlerArgs, slug })\n : match.tool.handler({ ...handlerArgs, slug }))\n\n return finalizeToolResponse({\n input: toolInput,\n overrideResponse: match.tool.overrideResponse,\n response,\n toolName: match.mcpName,\n })\n }\n\n try {\n const registeredEntityTools = new Set<string>()\n\n for (const item of authorizedMCP.items) {\n switch (item.type) {\n case 'collectionTool':\n case 'globalTool': {\n if (registeredEntityTools.has(item.mcpName)) {\n break\n }\n registeredEntityTools.add(item.mcpName)\n\n const inputSchema = withSlugInput({ input: item.tool.input })\n\n server.registerTool(\n item.mcpName,\n {\n annotations: item.tool.annotations,\n description: item.tool.description,\n inputSchema: toStandardSchema(inputSchema),\n },\n async (input: unknown, ctx: ServerContext) =>\n callEntityTool({ input, item, serverContext: ctx }),\n )\n logger.info(`✅ Tool: ${item.mcpName} Registered.`)\n break\n }\n case 'prompt': {\n const prompt = item.prompt\n server.registerPrompt(\n item.mcpName,\n {\n argsSchema: prompt.argsSchema ? toStandardSchema(prompt.argsSchema) : undefined,\n description: prompt.description,\n title: prompt.title,\n },\n async (input: unknown, ctx: ServerContext) =>\n prompt.handler({\n input: (input ?? {}) as Record<string, unknown>,\n req,\n serverContext: ctx,\n }),\n )\n logger.info(`✅ Prompt: ${prompt.title} Registered.`)\n break\n }\n case 'resource': {\n const resource = item.resource\n server.registerResource(\n item.mcpName,\n // @ts-expect-error - Overload type ambiguity (string OR ResourceTemplate is valid)\n resource.uri,\n {\n description: resource.description,\n mimeType: resource.mimeType,\n title: resource.title,\n },\n // Static URIs call (uri, ctx); ResourceTemplates call (uri, params, ctx).\n // The rest-params shape lets us collect either signature uniformly.\n async (...sdkArgs: unknown[]) => {\n const ctx = sdkArgs[sdkArgs.length - 1] as ServerContext\n const uri = sdkArgs[0] as URL\n const params = (sdkArgs.length > 2 ? sdkArgs[1] : {}) as Record<string, string>\n return resource.handler({ params, req, serverContext: ctx, uri })\n },\n )\n logger.info(`✅ Resource: ${resource.title} Registered.`)\n break\n }\n case 'tool': {\n const tool = item.tool\n server.registerTool(\n item.mcpName,\n {\n annotations: tool.annotations,\n description: tool.description,\n inputSchema: tool.input ? toStandardSchema(tool.input) : undefined,\n },\n async (input: unknown, ctx: ServerContext) => {\n const toolInput = (input ?? {}) as Record<string, unknown>\n const response = await tool.handler({\n authorizedMCP,\n input: toolInput,\n req,\n serverContext: ctx,\n })\n return finalizeToolResponse({\n input: toolInput,\n overrideResponse: tool.overrideResponse,\n response,\n toolName: item.mcpName,\n })\n },\n )\n logger.info(`✅ Tool: ${item.mcpName} Registered.`)\n break\n }\n }\n }\n } catch (error) {\n throw new APIError(`Error initializing MCP handler: ${String(error)}`, 500)\n }\n\n return server\n}\n\nconst withSlugInput = ({ input }: { input?: ToolInputSchema }): ToolInputSchema => {\n const description = 'The target slug.'\n const slugSchema = z.string().check(z.describe(description))\n\n if (!input) {\n return strictObject({ slug: slugSchema })\n }\n\n if (typeof input === 'object' && input !== null && '~standard' in input) {\n const schema = input['~standard'].jsonSchema.input({ target: 'draft-2020-12' })\n const properties = schema.properties as Record<string, unknown> | undefined\n\n if (properties?.slug) {\n return input\n }\n\n return addSlugToJSONSchema({ description, schema })\n }\n\n return addSlugToJSONSchema({ description, schema: input })\n}\n\nconst addSlugToJSONSchema = ({\n description,\n schema,\n}: {\n description: string\n schema: JsonSchemaType\n}): JsonSchemaType => {\n const objectSchema = schema as {\n properties?: Record<string, JsonSchemaType>\n required?: string[]\n } & JsonSchemaType\n\n return {\n ...objectSchema,\n type: 'object',\n properties: {\n ...objectSchema.properties,\n slug: {\n type: 'string',\n description,\n },\n },\n required: Array.from(new Set(['slug', ...(objectSchema.required ?? [])])),\n }\n}\n"],"names":["McpServer","APIError","strictObject","z","getLogger","toStandardSchema","buildMcpServer","authorizedMCP","pluginConfig","req","config","locales","en","serverOptions","mcp","server","name","version","serverInfo","options","logger","payload","finalizeToolResponse","input","overrideResponse","response","toolName","overridden","doc","hook","hooks","afterToolCall","_doc","rest","callEntityTool","item","serverContext","entity","type","toolInput","slug","content","text","mcpName","isError","match","items","find","candidate","collectionSlug","globalSlug","handlerArgs","tool","handler","registeredEntityTools","Set","has","add","inputSchema","withSlugInput","registerTool","annotations","description","ctx","info","prompt","registerPrompt","argsSchema","undefined","title","resource","registerResource","uri","mimeType","sdkArgs","length","params","error","String","slugSchema","string","check","describe","schema","jsonSchema","target","properties","addSlugToJSONSchema","objectSchema","required","Array","from"],"mappings":"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"}