@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
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/endpoint/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,SAAS,CAAA;AAInE,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAIzD,MAAM,MAAM,oBAAoB,GAAG;IACjC,cAAc,EAAE,OAAO,CAAA;IACvB,GAAG,EAAE,cAAc,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,OAAO,CAAC,aAAa,CAkCnF,CAAA;AAED,eAAO,MAAM,cAAc,oCAIxB;IACD,KAAK,EAAE,OAAO,EAAE,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,OAAO,CAAC,OAAO,EAAE,CAkBpB,CAAA"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/endpoint/access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAwB,MAAM,SAAS,CAAA;AAInE,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAIzD,MAAM,MAAM,oBAAoB,GAAG;IACjC,cAAc,EAAE,OAAO,CAAA;IACvB,GAAG,EAAE,cAAc,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,OAAO,CAAC,aAAa,CA2CnF,CAAA;AAED,eAAO,MAAM,cAAc,oCAIxB;IACD,KAAK,EAAE,OAAO,EAAE,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,OAAO,CAAC,OAAO,EAAE,CAkBpB,CAAA"}
@@ -1,4 +1,4 @@
1
- import { getAccessResults, UnauthorizedError } from 'payload';
1
+ import { executeAuthStrategies, getAccessResults, UnauthorizedError } from 'payload';
2
2
  import { getPluginConfig } from '../utils/getPluginConfig.js';
3
3
  /**
4
4
  * Authenticates an MCP request and removes items the user cannot access.
@@ -18,11 +18,23 @@ import { getPluginConfig } from '../utils/getPluginConfig.js';
18
18
  const headers = new Headers(req.headers);
19
19
  const hasAuthorization = headers.has('Authorization');
20
20
  headers.set('DisableAutologin', 'true');
21
- req.user = (await req.payload.auth({
21
+ // Not `payload.auth()`, which also computes permissions that filterMCPItems computes anyway
22
+ req.user = (await executeAuthStrategies({
22
23
  headers,
24
+ payload: req.payload,
23
25
  req
24
26
  })).user;
25
- if (hasAuthorization && !req.user) {
27
+ // Only bearer tokens that no auth strategy accepted reach the hook, e.g. an OAuth access token.
28
+ // API keys use the `<collection> API-Key <key>` scheme instead of `Bearer`, so they never do.
29
+ const bearerToken = headers.get('Authorization')?.match(/^Bearer (.+)$/)?.[1];
30
+ if (!req.user && bearerToken && pluginConfig.oauth?.authenticate) {
31
+ req.user = await pluginConfig.oauth.authenticate({
32
+ req,
33
+ token: bearerToken
34
+ });
35
+ }
36
+ // With OAuth, the endpoint answers with a challenge that starts the client's sign-in
37
+ if (hasAuthorization && !req.user && !pluginConfig.oauth) {
26
38
  throw new UnauthorizedError(req.t);
27
39
  }
28
40
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoint/access.ts"],"sourcesContent":["import type { PayloadRequest, SanitizedPermissions } from 'payload'\n\nimport { getAccessResults, UnauthorizedError } from 'payload'\n\nimport type { AuthorizedMCP, MCPItem } from '../types.js'\n\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\n\nexport type GetAuthorizedMCPArgs = {\n overrideAccess: boolean\n req: PayloadRequest\n}\n\n/**\n * Authenticates an MCP request and removes items the user cannot access.\n * `overrideAccess` skips Payload and MCP item access checks.\n */\nexport const getAuthorizedMCP: (args: GetAuthorizedMCPArgs) => Promise<AuthorizedMCP> = async ({\n overrideAccess,\n req,\n}) => {\n const pluginConfig = getPluginConfig({ config: req.payload.config })\n\n if (pluginConfig.overrideGetAuthorizedMCP) {\n return await pluginConfig.overrideGetAuthorizedMCP({\n overrideAccess,\n pluginConfig,\n req,\n })\n }\n\n if (req.headers) {\n const headers = new Headers(req.headers)\n const hasAuthorization = headers.has('Authorization')\n\n headers.set('DisableAutologin', 'true')\n req.user = (await req.payload.auth({ headers, req })).user\n\n if (hasAuthorization && !req.user) {\n throw new UnauthorizedError(req.t)\n }\n }\n\n return {\n items: await filterMCPItems({\n items: pluginConfig.items,\n overrideAccess,\n req,\n }),\n overrideAccess,\n }\n}\n\nexport const filterMCPItems = async ({\n items,\n overrideAccess,\n req,\n}: {\n items: MCPItem[]\n overrideAccess: boolean\n req: PayloadRequest\n}): Promise<MCPItem[]> => {\n // Match Payload core: overrideAccess bypasses access evaluation\n if (overrideAccess) {\n return items\n }\n\n const authorizedItems: MCPItem[] = []\n\n const permissions = await getAccessResults({ req })\n\n for (const item of items) {\n if (!(await checkItemAccess({ item, permissions, req }))) {\n continue\n }\n authorizedItems.push(item)\n }\n\n return authorizedItems\n}\n\n/**\n * Runs MCP item access callbacks\n */\nconst checkItemAccess = async ({\n item,\n permissions,\n req,\n}: {\n item: MCPItem\n permissions?: SanitizedPermissions\n req: PayloadRequest\n}): Promise<boolean> => {\n switch (item.type) {\n case 'collectionTool':\n return (\n !item.tool.access ||\n (await item.tool.access({ slug: item.collectionSlug, permissions, req }))\n )\n case 'globalTool':\n return (\n !item.tool.access || (await item.tool.access({ slug: item.globalSlug, permissions, req }))\n )\n case 'prompt':\n return !item.prompt.access || (await item.prompt.access({ permissions, req }))\n case 'resource':\n return !item.resource.access || (await item.resource.access({ permissions, req }))\n case 'tool':\n return !item.tool.access || (await item.tool.access({ permissions, req }))\n }\n}\n"],"names":["getAccessResults","UnauthorizedError","getPluginConfig","getAuthorizedMCP","overrideAccess","req","pluginConfig","config","payload","overrideGetAuthorizedMCP","headers","Headers","hasAuthorization","has","set","user","auth","t","items","filterMCPItems","authorizedItems","permissions","item","checkItemAccess","push","type","tool","access","slug","collectionSlug","globalSlug","prompt","resource"],"mappings":"AAEA,SAASA,gBAAgB,EAAEC,iBAAiB,QAAQ,UAAS;AAI7D,SAASC,eAAe,QAAQ,8BAA6B;AAO7D;;;CAGC,GACD,OAAO,MAAMC,mBAA2E,OAAO,EAC7FC,cAAc,EACdC,GAAG,EACJ;IACC,MAAMC,eAAeJ,gBAAgB;QAAEK,QAAQF,IAAIG,OAAO,CAACD,MAAM;IAAC;IAElE,IAAID,aAAaG,wBAAwB,EAAE;QACzC,OAAO,MAAMH,aAAaG,wBAAwB,CAAC;YACjDL;YACAE;YACAD;QACF;IACF;IAEA,IAAIA,IAAIK,OAAO,EAAE;QACf,MAAMA,UAAU,IAAIC,QAAQN,IAAIK,OAAO;QACvC,MAAME,mBAAmBF,QAAQG,GAAG,CAAC;QAErCH,QAAQI,GAAG,CAAC,oBAAoB;QAChCT,IAAIU,IAAI,GAAG,AAAC,CAAA,MAAMV,IAAIG,OAAO,CAACQ,IAAI,CAAC;YAAEN;YAASL;QAAI,EAAC,EAAGU,IAAI;QAE1D,IAAIH,oBAAoB,CAACP,IAAIU,IAAI,EAAE;YACjC,MAAM,IAAId,kBAAkBI,IAAIY,CAAC;QACnC;IACF;IAEA,OAAO;QACLC,OAAO,MAAMC,eAAe;YAC1BD,OAAOZ,aAAaY,KAAK;YACzBd;YACAC;QACF;QACAD;IACF;AACF,EAAC;AAED,OAAO,MAAMe,iBAAiB,OAAO,EACnCD,KAAK,EACLd,cAAc,EACdC,GAAG,EAKJ;IACC,gEAAgE;IAChE,IAAID,gBAAgB;QAClB,OAAOc;IACT;IAEA,MAAME,kBAA6B,EAAE;IAErC,MAAMC,cAAc,MAAMrB,iBAAiB;QAAEK;IAAI;IAEjD,KAAK,MAAMiB,QAAQJ,MAAO;QACxB,IAAI,CAAE,MAAMK,gBAAgB;YAAED;YAAMD;YAAahB;QAAI,IAAK;YACxD;QACF;QACAe,gBAAgBI,IAAI,CAACF;IACvB;IAEA,OAAOF;AACT,EAAC;AAED;;CAEC,GACD,MAAMG,kBAAkB,OAAO,EAC7BD,IAAI,EACJD,WAAW,EACXhB,GAAG,EAKJ;IACC,OAAQiB,KAAKG,IAAI;QACf,KAAK;YACH,OACE,CAACH,KAAKI,IAAI,CAACC,MAAM,IAChB,MAAML,KAAKI,IAAI,CAACC,MAAM,CAAC;gBAAEC,MAAMN,KAAKO,cAAc;gBAAER;gBAAahB;YAAI;QAE1E,KAAK;YACH,OACE,CAACiB,KAAKI,IAAI,CAACC,MAAM,IAAK,MAAML,KAAKI,IAAI,CAACC,MAAM,CAAC;gBAAEC,MAAMN,KAAKQ,UAAU;gBAAET;gBAAahB;YAAI;QAE3F,KAAK;YACH,OAAO,CAACiB,KAAKS,MAAM,CAACJ,MAAM,IAAK,MAAML,KAAKS,MAAM,CAACJ,MAAM,CAAC;gBAAEN;gBAAahB;YAAI;QAC7E,KAAK;YACH,OAAO,CAACiB,KAAKU,QAAQ,CAACL,MAAM,IAAK,MAAML,KAAKU,QAAQ,CAACL,MAAM,CAAC;gBAAEN;gBAAahB;YAAI;QACjF,KAAK;YACH,OAAO,CAACiB,KAAKI,IAAI,CAACC,MAAM,IAAK,MAAML,KAAKI,IAAI,CAACC,MAAM,CAAC;gBAAEN;gBAAahB;YAAI;IAC3E;AACF"}
1
+ {"version":3,"sources":["../../src/endpoint/access.ts"],"sourcesContent":["import type { PayloadRequest, SanitizedPermissions } from 'payload'\n\nimport { executeAuthStrategies, getAccessResults, UnauthorizedError } from 'payload'\n\nimport type { AuthorizedMCP, MCPItem } from '../types.js'\n\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\n\nexport type GetAuthorizedMCPArgs = {\n overrideAccess: boolean\n req: PayloadRequest\n}\n\n/**\n * Authenticates an MCP request and removes items the user cannot access.\n * `overrideAccess` skips Payload and MCP item access checks.\n */\nexport const getAuthorizedMCP: (args: GetAuthorizedMCPArgs) => Promise<AuthorizedMCP> = async ({\n overrideAccess,\n req,\n}) => {\n const pluginConfig = getPluginConfig({ config: req.payload.config })\n\n if (pluginConfig.overrideGetAuthorizedMCP) {\n return await pluginConfig.overrideGetAuthorizedMCP({\n overrideAccess,\n pluginConfig,\n req,\n })\n }\n\n if (req.headers) {\n const headers = new Headers(req.headers)\n const hasAuthorization = headers.has('Authorization')\n\n headers.set('DisableAutologin', 'true')\n // Not `payload.auth()`, which also computes permissions that filterMCPItems computes anyway\n req.user = (await executeAuthStrategies({ headers, payload: req.payload, req })).user\n\n // Only bearer tokens that no auth strategy accepted reach the hook, e.g. an OAuth access token.\n // API keys use the `<collection> API-Key <key>` scheme instead of `Bearer`, so they never do.\n const bearerToken = headers.get('Authorization')?.match(/^Bearer (.+)$/)?.[1]\n if (!req.user && bearerToken && pluginConfig.oauth?.authenticate) {\n req.user = await pluginConfig.oauth.authenticate({ req, token: bearerToken })\n }\n\n // With OAuth, the endpoint answers with a challenge that starts the client's sign-in\n if (hasAuthorization && !req.user && !pluginConfig.oauth) {\n throw new UnauthorizedError(req.t)\n }\n }\n\n return {\n items: await filterMCPItems({\n items: pluginConfig.items,\n overrideAccess,\n req,\n }),\n overrideAccess,\n }\n}\n\nexport const filterMCPItems = async ({\n items,\n overrideAccess,\n req,\n}: {\n items: MCPItem[]\n overrideAccess: boolean\n req: PayloadRequest\n}): Promise<MCPItem[]> => {\n // Match Payload core: overrideAccess bypasses access evaluation\n if (overrideAccess) {\n return items\n }\n\n const authorizedItems: MCPItem[] = []\n\n const permissions = await getAccessResults({ req })\n\n for (const item of items) {\n if (!(await checkItemAccess({ item, permissions, req }))) {\n continue\n }\n authorizedItems.push(item)\n }\n\n return authorizedItems\n}\n\n/**\n * Runs MCP item access callbacks\n */\nconst checkItemAccess = async ({\n item,\n permissions,\n req,\n}: {\n item: MCPItem\n permissions?: SanitizedPermissions\n req: PayloadRequest\n}): Promise<boolean> => {\n switch (item.type) {\n case 'collectionTool':\n return (\n !item.tool.access ||\n (await item.tool.access({ slug: item.collectionSlug, permissions, req }))\n )\n case 'globalTool':\n return (\n !item.tool.access || (await item.tool.access({ slug: item.globalSlug, permissions, req }))\n )\n case 'prompt':\n return !item.prompt.access || (await item.prompt.access({ permissions, req }))\n case 'resource':\n return !item.resource.access || (await item.resource.access({ permissions, req }))\n case 'tool':\n return !item.tool.access || (await item.tool.access({ permissions, req }))\n }\n}\n"],"names":["executeAuthStrategies","getAccessResults","UnauthorizedError","getPluginConfig","getAuthorizedMCP","overrideAccess","req","pluginConfig","config","payload","overrideGetAuthorizedMCP","headers","Headers","hasAuthorization","has","set","user","bearerToken","get","match","oauth","authenticate","token","t","items","filterMCPItems","authorizedItems","permissions","item","checkItemAccess","push","type","tool","access","slug","collectionSlug","globalSlug","prompt","resource"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/endpoint/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAA;AAc7D,eAAO,MAAM,WAAW,EAAE,cAmEzB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/endpoint/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,SAAS,CAAA;AAe7D,eAAO,MAAM,WAAW,EAAE,cA0EzB,CAAA"}
@@ -3,11 +3,19 @@ import { APIError } from 'payload';
3
3
  import { buildMcpServer } from '../mcp/buildMcpServer.js';
4
4
  import { getPluginConfig } from '../utils/getPluginConfig.js';
5
5
  import { getAuthorizedMCP } from './access.js';
6
+ import { getMCPResource, oauthChallenge } from './oauth.js';
6
7
  export const mcpEndpoint = async (req)=>{
7
8
  if (!req.url) {
8
9
  throw new APIError('Missing request URL', 400);
9
10
  }
10
11
  req.payloadAPI = 'MCP';
12
+ // Lets auth strategies check that an OAuth access token was issued for this MCP server
13
+ req.context.mcp = {
14
+ resource: getMCPResource({
15
+ config: req.payload.config,
16
+ url: req.url
17
+ })
18
+ };
11
19
  const pluginConfig = getPluginConfig({
12
20
  config: req.payload.config
13
21
  });
@@ -25,6 +33,11 @@ export const mcpEndpoint = async (req)=>{
25
33
  overrideAccess,
26
34
  req
27
35
  });
36
+ if (pluginConfig.oauth && !req.user) {
37
+ return oauthChallenge({
38
+ req
39
+ });
40
+ }
28
41
  const maxRequestBodySize = pluginConfig.mcp?.maxRequestBodySize;
29
42
  // Payload augments the original web-standard Request in place.
30
43
  const mcpRequest = req;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoint/index.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport {\n createMcpHandler,\n isLegacyRequest,\n readRequestBody,\n WebStandardStreamableHTTPServerTransport,\n} from '@modelcontextprotocol/server'\nimport { APIError } from 'payload'\n\nimport { buildMcpServer } from '../mcp/buildMcpServer.js'\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\nimport { getAuthorizedMCP } from './access.js'\n\nexport const mcpEndpoint: PayloadHandler = async (req) => {\n if (!req.url) {\n throw new APIError('Missing request URL', 400)\n }\n\n req.payloadAPI = 'MCP' as const\n\n const pluginConfig = getPluginConfig({ config: req.payload.config })\n const overrideAccessParam = new URL(req.url).searchParams.get('overrideAccess')\n\n if (overrideAccessParam !== null && process.env.NODE_ENV !== 'development') {\n throw new APIError('MCP overrideAccess is only available in development.', 400)\n }\n\n let overrideAccess = false\n if (overrideAccessParam === 'true') {\n overrideAccess = true\n } else if (overrideAccessParam !== null && overrideAccessParam !== 'false') {\n throw new APIError('MCP overrideAccess must be \"true\" or \"false\".', 400)\n }\n\n const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })\n const maxRequestBodySize = pluginConfig.mcp?.maxRequestBodySize\n // Payload augments the original web-standard Request in place.\n const mcpRequest = req as PayloadRequest & Request\n const parsedBody = await parseRequestBody({ maxRequestBodySize, request: mcpRequest })\n\n // Keep the old JSON-only, stateless behavior because the SDK's 2025 fallback uses SSE.\n if (await isLegacyRequest(mcpRequest, parsedBody, { maxRequestBodySize })) {\n const server = buildMcpServer({ authorizedMCP, pluginConfig, req })\n const transport = new WebStandardStreamableHTTPServerTransport({\n enableJsonResponse: true,\n maxRequestBodySize,\n sessionIdGenerator: undefined, // stateless mode\n })\n transport.onerror = (err) => {\n req.payload.logger.error({ err, msg: 'Error serving legacy MCP request' })\n }\n\n try {\n await server.connect(transport)\n return await transport.handleRequest(mcpRequest, { parsedBody })\n } finally {\n await server.close().catch((err) => {\n req.payload.logger.error({ err, msg: 'Error closing MCP server' })\n })\n }\n }\n\n const handler = createMcpHandler(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {\n legacy: 'reject',\n maxRequestBodySize,\n // SDK subscriptions always use SSE, so disable them to keep every response JSON-only.\n maxSubscriptions: 0,\n onerror: (err) => {\n req.payload.logger.error({ err, msg: 'Error serving modern MCP request' })\n },\n responseMode: 'json',\n })\n\n try {\n return await handler.fetch(mcpRequest, { parsedBody })\n } finally {\n await handler.close().catch((err) => {\n req.payload.logger.error({ err, msg: 'Error closing modern MCP handler' })\n })\n }\n}\n\n/**\n * Parses a POST body once so the era check and the SDK handler can share it. The SDK\n * skips its size limit for a pre-parsed body, so this reads with the same limit. It reads\n * a clone and returns `undefined` for any body the SDK should reject (too large, unreadable,\n * or not JSON), leaving the original request for the SDK to read and answer.\n */\nconst parseRequestBody = async ({\n maxRequestBodySize,\n request,\n}: {\n maxRequestBodySize?: number\n request: Request\n}): Promise<unknown> => {\n if (request.method.toUpperCase() !== 'POST') {\n return undefined\n }\n\n try {\n const body = await readRequestBody(request.clone(), maxRequestBodySize)\n return body.tooLarge ? undefined : JSON.parse(body.text)\n } catch {\n return undefined\n }\n}\n"],"names":["createMcpHandler","isLegacyRequest","readRequestBody","WebStandardStreamableHTTPServerTransport","APIError","buildMcpServer","getPluginConfig","getAuthorizedMCP","mcpEndpoint","req","url","payloadAPI","pluginConfig","config","payload","overrideAccessParam","URL","searchParams","get","process","env","NODE_ENV","overrideAccess","authorizedMCP","maxRequestBodySize","mcp","mcpRequest","parsedBody","parseRequestBody","request","server","transport","enableJsonResponse","sessionIdGenerator","undefined","onerror","err","logger","error","msg","connect","handleRequest","close","catch","handler","legacy","maxSubscriptions","responseMode","fetch","method","toUpperCase","body","clone","tooLarge","JSON","parse","text"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/endpoint/index.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport {\n createMcpHandler,\n isLegacyRequest,\n readRequestBody,\n WebStandardStreamableHTTPServerTransport,\n} from '@modelcontextprotocol/server'\nimport { APIError } from 'payload'\n\nimport { buildMcpServer } from '../mcp/buildMcpServer.js'\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\nimport { getAuthorizedMCP } from './access.js'\nimport { getMCPResource, oauthChallenge } from './oauth.js'\n\nexport const mcpEndpoint: PayloadHandler = async (req) => {\n if (!req.url) {\n throw new APIError('Missing request URL', 400)\n }\n\n req.payloadAPI = 'MCP' as const\n // Lets auth strategies check that an OAuth access token was issued for this MCP server\n req.context.mcp = { resource: getMCPResource({ config: req.payload.config, url: req.url }) }\n\n const pluginConfig = getPluginConfig({ config: req.payload.config })\n const overrideAccessParam = new URL(req.url).searchParams.get('overrideAccess')\n\n if (overrideAccessParam !== null && process.env.NODE_ENV !== 'development') {\n throw new APIError('MCP overrideAccess is only available in development.', 400)\n }\n\n let overrideAccess = false\n if (overrideAccessParam === 'true') {\n overrideAccess = true\n } else if (overrideAccessParam !== null && overrideAccessParam !== 'false') {\n throw new APIError('MCP overrideAccess must be \"true\" or \"false\".', 400)\n }\n\n const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })\n\n if (pluginConfig.oauth && !req.user) {\n return oauthChallenge({ req })\n }\n\n const maxRequestBodySize = pluginConfig.mcp?.maxRequestBodySize\n // Payload augments the original web-standard Request in place.\n const mcpRequest = req as PayloadRequest & Request\n const parsedBody = await parseRequestBody({ maxRequestBodySize, request: mcpRequest })\n\n // Keep the old JSON-only, stateless behavior because the SDK's 2025 fallback uses SSE.\n if (await isLegacyRequest(mcpRequest, parsedBody, { maxRequestBodySize })) {\n const server = buildMcpServer({ authorizedMCP, pluginConfig, req })\n const transport = new WebStandardStreamableHTTPServerTransport({\n enableJsonResponse: true,\n maxRequestBodySize,\n sessionIdGenerator: undefined, // stateless mode\n })\n transport.onerror = (err) => {\n req.payload.logger.error({ err, msg: 'Error serving legacy MCP request' })\n }\n\n try {\n await server.connect(transport)\n return await transport.handleRequest(mcpRequest, { parsedBody })\n } finally {\n await server.close().catch((err) => {\n req.payload.logger.error({ err, msg: 'Error closing MCP server' })\n })\n }\n }\n\n const handler = createMcpHandler(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {\n legacy: 'reject',\n maxRequestBodySize,\n // SDK subscriptions always use SSE, so disable them to keep every response JSON-only.\n maxSubscriptions: 0,\n onerror: (err) => {\n req.payload.logger.error({ err, msg: 'Error serving modern MCP request' })\n },\n responseMode: 'json',\n })\n\n try {\n return await handler.fetch(mcpRequest, { parsedBody })\n } finally {\n await handler.close().catch((err) => {\n req.payload.logger.error({ err, msg: 'Error closing modern MCP handler' })\n })\n }\n}\n\n/**\n * Parses a POST body once so the era check and the SDK handler can share it. The SDK\n * skips its size limit for a pre-parsed body, so this reads with the same limit. It reads\n * a clone and returns `undefined` for any body the SDK should reject (too large, unreadable,\n * or not JSON), leaving the original request for the SDK to read and answer.\n */\nconst parseRequestBody = async ({\n maxRequestBodySize,\n request,\n}: {\n maxRequestBodySize?: number\n request: Request\n}): Promise<unknown> => {\n if (request.method.toUpperCase() !== 'POST') {\n return undefined\n }\n\n try {\n const body = await readRequestBody(request.clone(), maxRequestBodySize)\n return body.tooLarge ? undefined : JSON.parse(body.text)\n } catch {\n return undefined\n }\n}\n"],"names":["createMcpHandler","isLegacyRequest","readRequestBody","WebStandardStreamableHTTPServerTransport","APIError","buildMcpServer","getPluginConfig","getAuthorizedMCP","getMCPResource","oauthChallenge","mcpEndpoint","req","url","payloadAPI","context","mcp","resource","config","payload","pluginConfig","overrideAccessParam","URL","searchParams","get","process","env","NODE_ENV","overrideAccess","authorizedMCP","oauth","user","maxRequestBodySize","mcpRequest","parsedBody","parseRequestBody","request","server","transport","enableJsonResponse","sessionIdGenerator","undefined","onerror","err","logger","error","msg","connect","handleRequest","close","catch","handler","legacy","maxSubscriptions","responseMode","fetch","method","toUpperCase","body","clone","tooLarge","JSON","parse","text"],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ import type { PayloadHandler, PayloadRequest, SanitizedConfig } from 'payload';
2
+ /**
3
+ * Serves the OAuth protected resource metadata (RFC 9728) at `/api/mcp/oauth-protected-resource`.
4
+ * The OAuth challenge links here, so MCP clients find the authorization server without setup.
5
+ */
6
+ export declare const oauthProtectedResourceEndpoint: PayloadHandler;
7
+ /**
8
+ * Serves the same metadata for clients that ignore the challenge's link and only look at
9
+ * `/.well-known/oauth-protected-resource`. Mount it at the site root, e.g. in Next.js at
10
+ * `app/.well-known/oauth-protected-resource/[[...path]]/route.ts`:
11
+ *
12
+ * ```ts
13
+ * import config from '@payload-config'
14
+ * import { oauthProtectedResourceHandler } from '@payloadcms/plugin-mcp'
15
+ *
16
+ * export const GET = oauthProtectedResourceHandler(config)
17
+ * ```
18
+ */
19
+ export declare const oauthProtectedResourceHandler: (config: Promise<SanitizedConfig> | SanitizedConfig) => (request: Request) => Promise<Response>;
20
+ /**
21
+ * Answers an unauthenticated MCP request with a challenge linking to the protected resource
22
+ * metadata, which starts the client's OAuth flow.
23
+ */
24
+ export declare const oauthChallenge: ({ req }: {
25
+ req: PayloadRequest;
26
+ }) => Response;
27
+ /**
28
+ * The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when
29
+ * they sign in, and authorization servers that support it put it in the access token's audience.
30
+ */
31
+ export declare const getMCPResource: ({ config, url }: {
32
+ config: SanitizedConfig;
33
+ url: string;
34
+ }) => string;
35
+ export declare const getMCPURL: ({ config, url }: {
36
+ config: SanitizedConfig;
37
+ url: string;
38
+ }) => string;
39
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/endpoint/oauth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAW9E;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,cAAsD,CAAA;AAEnG;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,WAC/B,OAAO,CAAC,eAAe,CAAC,GAAG,eAAe,eACnC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAGtC,CAAA;AAEN;;;GAGG;AACH,eAAO,MAAM,cAAc,YAAa;IAAE,GAAG,EAAE,cAAc,CAAA;CAAE,KAAG,QAcjE,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,cAAc,oBAAqB;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAG,MAChB,CAAA;AAE3E,eAAO,MAAM,SAAS,oBAAqB;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAG,MACnB,CAAA"}
@@ -0,0 +1,92 @@
1
+ import { bearerAuthChallengeResponse, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
2
+ import { createPayloadRequest, getPayload } from 'payload';
3
+ import { getPluginConfig } from '../utils/getPluginConfig.js';
4
+ /**
5
+ * Serves the OAuth protected resource metadata (RFC 9728) at `/api/mcp/oauth-protected-resource`.
6
+ * The OAuth challenge links here, so MCP clients find the authorization server without setup.
7
+ */ export const oauthProtectedResourceEndpoint = (req)=>getMetadataResponse({
8
+ req
9
+ });
10
+ /**
11
+ * Serves the same metadata for clients that ignore the challenge's link and only look at
12
+ * `/.well-known/oauth-protected-resource`. Mount it at the site root, e.g. in Next.js at
13
+ * `app/.well-known/oauth-protected-resource/[[...path]]/route.ts`:
14
+ *
15
+ * ```ts
16
+ * import config from '@payload-config'
17
+ * import { oauthProtectedResourceHandler } from '@payloadcms/plugin-mcp'
18
+ *
19
+ * export const GET = oauthProtectedResourceHandler(config)
20
+ * ```
21
+ */ export const oauthProtectedResourceHandler = (config)=>async (request)=>getMetadataResponse({
22
+ req: await createPayloadRequest({
23
+ payload: await getPayload({
24
+ config
25
+ }),
26
+ req: request
27
+ })
28
+ });
29
+ /**
30
+ * Answers an unauthenticated MCP request with a challenge linking to the protected resource
31
+ * metadata, which starts the client's OAuth flow.
32
+ */ export const oauthChallenge = ({ req })=>{
33
+ const { oauth } = getPluginConfig({
34
+ config: req.payload.config
35
+ });
36
+ const response = bearerAuthChallengeResponse(new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')), {
37
+ requiredScopes: oauth?.type === 'external' ? oauth.scopes : undefined,
38
+ resourceMetadataUrl: `${getMCPURL({
39
+ config: req.payload.config,
40
+ url: req.url
41
+ })}/oauth-protected-resource`
42
+ });
43
+ // Lets browser-based MCP clients read the challenge, when Payload's `cors` setting allows them
44
+ response.headers.set('Access-Control-Expose-Headers', 'WWW-Authenticate');
45
+ return response;
46
+ };
47
+ /**
48
+ * The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when
49
+ * they sign in, and authorization servers that support it put it in the access token's audience.
50
+ */ export const getMCPResource = ({ config, url })=>getPluginConfig({
51
+ config
52
+ }).oauth?.resource ?? getMCPURL({
53
+ config,
54
+ url
55
+ });
56
+ export const getMCPURL = ({ config, url })=>new URL(`${config.routes.api}/mcp`, config.serverURL || url).href;
57
+ const getMetadataResponse = async ({ req })=>{
58
+ const { config } = req.payload;
59
+ const { oauth } = getPluginConfig({
60
+ config
61
+ });
62
+ if (!oauth) {
63
+ return new Response(null, {
64
+ status: 404
65
+ });
66
+ }
67
+ return Response.json({
68
+ authorization_servers: oauth.type === 'builtin' ? [
69
+ `${getMCPURL({
70
+ config,
71
+ url: req.url
72
+ })}/oauth`
73
+ ] : typeof oauth.authorizationServers === 'function' ? await oauth.authorizationServers({
74
+ req
75
+ }) : oauth.authorizationServers,
76
+ bearer_methods_supported: [
77
+ 'header'
78
+ ],
79
+ resource: getMCPResource({
80
+ config,
81
+ url: req.url
82
+ }),
83
+ scopes_supported: oauth.type === 'external' ? oauth.scopes : undefined
84
+ }, // Public document, readable by browser-based MCP clients on any origin
85
+ {
86
+ headers: {
87
+ 'Access-Control-Allow-Origin': '*'
88
+ }
89
+ });
90
+ };
91
+
92
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoint/oauth.ts"],"sourcesContent":["import type { OAuthProtectedResourceMetadata } from '@modelcontextprotocol/server'\nimport type { PayloadHandler, PayloadRequest, SanitizedConfig } from 'payload'\n\nimport {\n bearerAuthChallengeResponse,\n OAuthError,\n OAuthErrorCode,\n} from '@modelcontextprotocol/server'\nimport { createPayloadRequest, getPayload } from 'payload'\n\nimport { getPluginConfig } from '../utils/getPluginConfig.js'\n\n/**\n * Serves the OAuth protected resource metadata (RFC 9728) at `/api/mcp/oauth-protected-resource`.\n * The OAuth challenge links here, so MCP clients find the authorization server without setup.\n */\nexport const oauthProtectedResourceEndpoint: PayloadHandler = (req) => getMetadataResponse({ req })\n\n/**\n * Serves the same metadata for clients that ignore the challenge's link and only look at\n * `/.well-known/oauth-protected-resource`. Mount it at the site root, e.g. in Next.js at\n * `app/.well-known/oauth-protected-resource/[[...path]]/route.ts`:\n *\n * ```ts\n * import config from '@payload-config'\n * import { oauthProtectedResourceHandler } from '@payloadcms/plugin-mcp'\n *\n * export const GET = oauthProtectedResourceHandler(config)\n * ```\n */\nexport const oauthProtectedResourceHandler =\n (config: Promise<SanitizedConfig> | SanitizedConfig) =>\n async (request: Request): Promise<Response> =>\n getMetadataResponse({\n req: await createPayloadRequest({ payload: await getPayload({ config }), req: request }),\n })\n\n/**\n * Answers an unauthenticated MCP request with a challenge linking to the protected resource\n * metadata, which starts the client's OAuth flow.\n */\nexport const oauthChallenge = ({ req }: { req: PayloadRequest }): Response => {\n const { oauth } = getPluginConfig({ config: req.payload.config })\n const response = bearerAuthChallengeResponse(\n new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')),\n {\n requiredScopes: oauth?.type === 'external' ? oauth.scopes : undefined,\n resourceMetadataUrl: `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth-protected-resource`,\n },\n )\n\n // Lets browser-based MCP clients read the challenge, when Payload's `cors` setting allows them\n response.headers.set('Access-Control-Expose-Headers', 'WWW-Authenticate')\n\n return response\n}\n\n/**\n * The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when\n * they sign in, and authorization servers that support it put it in the access token's audience.\n */\nexport const getMCPResource = ({ config, url }: { config: SanitizedConfig; url: string }): string =>\n getPluginConfig({ config }).oauth?.resource ?? getMCPURL({ config, url })\n\nexport const getMCPURL = ({ config, url }: { config: SanitizedConfig; url: string }): string =>\n new URL(`${config.routes.api}/mcp`, config.serverURL || url).href\n\nconst getMetadataResponse = async ({ req }: { req: PayloadRequest }): Promise<Response> => {\n const { config } = req.payload\n const { oauth } = getPluginConfig({ config })\n\n if (!oauth) {\n return new Response(null, { status: 404 })\n }\n\n return Response.json(\n {\n authorization_servers:\n oauth.type === 'builtin'\n ? [`${getMCPURL({ config, url: req.url! })}/oauth`]\n : typeof oauth.authorizationServers === 'function'\n ? await oauth.authorizationServers({ req })\n : oauth.authorizationServers,\n bearer_methods_supported: ['header'],\n resource: getMCPResource({ config, url: req.url! }),\n scopes_supported: oauth.type === 'external' ? oauth.scopes : undefined,\n } satisfies OAuthProtectedResourceMetadata,\n // Public document, readable by browser-based MCP clients on any origin\n { headers: { 'Access-Control-Allow-Origin': '*' } },\n )\n}\n"],"names":["bearerAuthChallengeResponse","OAuthError","OAuthErrorCode","createPayloadRequest","getPayload","getPluginConfig","oauthProtectedResourceEndpoint","req","getMetadataResponse","oauthProtectedResourceHandler","config","request","payload","oauthChallenge","oauth","response","InvalidToken","t","requiredScopes","type","scopes","undefined","resourceMetadataUrl","getMCPURL","url","headers","set","getMCPResource","resource","URL","routes","api","serverURL","href","Response","status","json","authorization_servers","authorizationServers","bearer_methods_supported","scopes_supported"],"mappings":"AAGA,SACEA,2BAA2B,EAC3BC,UAAU,EACVC,cAAc,QACT,+BAA8B;AACrC,SAASC,oBAAoB,EAAEC,UAAU,QAAQ,UAAS;AAE1D,SAASC,eAAe,QAAQ,8BAA6B;AAE7D;;;CAGC,GACD,OAAO,MAAMC,iCAAiD,CAACC,MAAQC,oBAAoB;QAAED;IAAI,GAAE;AAEnG;;;;;;;;;;;CAWC,GACD,OAAO,MAAME,gCACX,CAACC,SACD,OAAOC,UACLH,oBAAoB;YAClBD,KAAK,MAAMJ,qBAAqB;gBAAES,SAAS,MAAMR,WAAW;oBAAEM;gBAAO;gBAAIH,KAAKI;YAAQ;QACxF,GAAE;AAEN;;;CAGC,GACD,OAAO,MAAME,iBAAiB,CAAC,EAAEN,GAAG,EAA2B;IAC7D,MAAM,EAAEO,KAAK,EAAE,GAAGT,gBAAgB;QAAEK,QAAQH,IAAIK,OAAO,CAACF,MAAM;IAAC;IAC/D,MAAMK,WAAWf,4BACf,IAAIC,WAAWC,eAAec,YAAY,EAAET,IAAIU,CAAC,CAAC,wBAClD;QACEC,gBAAgBJ,OAAOK,SAAS,aAAaL,MAAMM,MAAM,GAAGC;QAC5DC,qBAAqB,GAAGC,UAAU;YAAEb,QAAQH,IAAIK,OAAO,CAACF,MAAM;YAAEc,KAAKjB,IAAIiB,GAAG;QAAE,GAAG,yBAAyB,CAAC;IAC7G;IAGF,+FAA+F;IAC/FT,SAASU,OAAO,CAACC,GAAG,CAAC,iCAAiC;IAEtD,OAAOX;AACT,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMY,iBAAiB,CAAC,EAAEjB,MAAM,EAAEc,GAAG,EAA4C,GACtFnB,gBAAgB;QAAEK;IAAO,GAAGI,KAAK,EAAEc,YAAYL,UAAU;QAAEb;QAAQc;IAAI,GAAE;AAE3E,OAAO,MAAMD,YAAY,CAAC,EAAEb,MAAM,EAAEc,GAAG,EAA4C,GACjF,IAAIK,IAAI,GAAGnB,OAAOoB,MAAM,CAACC,GAAG,CAAC,IAAI,CAAC,EAAErB,OAAOsB,SAAS,IAAIR,KAAKS,IAAI,CAAA;AAEnE,MAAMzB,sBAAsB,OAAO,EAAED,GAAG,EAA2B;IACjE,MAAM,EAAEG,MAAM,EAAE,GAAGH,IAAIK,OAAO;IAC9B,MAAM,EAAEE,KAAK,EAAE,GAAGT,gBAAgB;QAAEK;IAAO;IAE3C,IAAI,CAACI,OAAO;QACV,OAAO,IAAIoB,SAAS,MAAM;YAAEC,QAAQ;QAAI;IAC1C;IAEA,OAAOD,SAASE,IAAI,CAClB;QACEC,uBACEvB,MAAMK,IAAI,KAAK,YACX;YAAC,GAAGI,UAAU;gBAAEb;gBAAQc,KAAKjB,IAAIiB,GAAG;YAAE,GAAG,MAAM,CAAC;SAAC,GACjD,OAAOV,MAAMwB,oBAAoB,KAAK,aACpC,MAAMxB,MAAMwB,oBAAoB,CAAC;YAAE/B;QAAI,KACvCO,MAAMwB,oBAAoB;QAClCC,0BAA0B;YAAC;SAAS;QACpCX,UAAUD,eAAe;YAAEjB;YAAQc,KAAKjB,IAAIiB,GAAG;QAAE;QACjDgB,kBAAkB1B,MAAMK,IAAI,KAAK,aAAaL,MAAMM,MAAM,GAAGC;IAC/D,GACA,uEAAuE;IACvE;QAAEI,SAAS;YAAE,+BAA+B;QAAI;IAAE;AAEtD"}
@@ -0,0 +1,20 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ import type { MCPOAuthTokens } from '../types.js';
3
+ /**
4
+ * The default `issueToken`: a Payload login token, which Payload's JWT strategy accepts. When the
5
+ * collection uses sessions, the token gets its own login session, and the client gets a refresh
6
+ * token for it.
7
+ */
8
+ export declare const issueLoginToken: ({ req, }: {
9
+ req: PayloadRequest;
10
+ }) => Promise<MCPOAuthTokens>;
11
+ /**
12
+ * The default `refreshToken`. Every refresh moves the session's expiry, so a refresh token with an
13
+ * older expiry was already used. Outside of parallel refreshes, that means someone copied it, and
14
+ * the session is signed out, which also revokes the client's access token.
15
+ */
16
+ export declare const refreshLoginToken: ({ refreshToken, req, }: {
17
+ refreshToken: string;
18
+ req: PayloadRequest;
19
+ }) => Promise<MCPOAuthTokens | null>;
20
+ //# sourceMappingURL=oauthLoginToken.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthLoginToken.d.ts","sourceRoot":"","sources":["../../src/endpoint/oauthLoginToken.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA6B,MAAM,SAAS,CAAA;AAMxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAwBjD;;;;GAIG;AACH,eAAO,MAAM,eAAe,aAEzB;IACD,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,OAAO,CAAC,cAAc,CAkBzB,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,2BAG3B;IACD,YAAY,EAAE,MAAM,CAAA;IACpB,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAsChC,CAAA"}
@@ -0,0 +1,158 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { APIError, getFieldsToSign, jwtSign } from 'payload';
3
+ import { removeExpiredSessions } from 'payload/shared';
4
+ /** How long a refresh token stays valid without being used, in milliseconds: 30 days. */ const refreshTokenExpiration = 30 * 24 * 60 * 60 * 1000;
5
+ /**
6
+ * How long a used refresh token is still accepted, in milliseconds. MCP clients that send
7
+ * requests in parallel can refresh twice at once with the same refresh token.
8
+ */ const refreshTokenReuseInterval = 30 * 1000;
9
+ /**
10
+ * The default `issueToken`: a Payload login token, which Payload's JWT strategy accepts. When the
11
+ * collection uses sessions, the token gets its own login session, and the client gets a refresh
12
+ * token for it.
13
+ */ export const issueLoginToken = async ({ req })=>{
14
+ const collection = req.payload.collections[req.user.collection].config;
15
+ if (collection.auth.disableLocalStrategy) {
16
+ throw new APIError(`MCP clients can't get Payload login tokens for the "${collection.slug}" collection because its local strategy is disabled. Pass oauth.issueToken to the MCP plugin.`);
17
+ }
18
+ const user = await findUser({
19
+ id: req.user.id,
20
+ collection,
21
+ req
22
+ });
23
+ if (!collection.auth.useSessions) {
24
+ return await signLoginToken({
25
+ collection,
26
+ req,
27
+ user
28
+ });
29
+ }
30
+ const session = await saveSession({
31
+ collection,
32
+ req,
33
+ sid: randomUUID(),
34
+ user
35
+ });
36
+ return await signLoginToken({
37
+ collection,
38
+ req,
39
+ session,
40
+ user
41
+ });
42
+ };
43
+ /**
44
+ * The default `refreshToken`. Every refresh moves the session's expiry, so a refresh token with an
45
+ * older expiry was already used. Outside of parallel refreshes, that means someone copied it, and
46
+ * the session is signed out, which also revokes the client's access token.
47
+ */ export const refreshLoginToken = async ({ refreshToken, req })=>{
48
+ const { collection: slug, expiresAt, sid, userID } = JSON.parse(refreshToken);
49
+ const collection = req.payload.collections[slug]?.config;
50
+ const user = collection && await findUser({
51
+ id: userID,
52
+ collection,
53
+ req
54
+ });
55
+ const session = removeExpiredSessions(user?.sessions ?? []).find(({ id })=>id === sid);
56
+ // Signed out, expired, or the user was deleted
57
+ if (!collection || !user || !session) {
58
+ return null;
59
+ }
60
+ const sessionExpiresAt = new Date(session.expiresAt).getTime();
61
+ if (sessionExpiresAt === expiresAt) {
62
+ return await signLoginToken({
63
+ collection,
64
+ req,
65
+ session: await saveSession({
66
+ collection,
67
+ req,
68
+ sid,
69
+ user
70
+ }),
71
+ user
72
+ });
73
+ }
74
+ // The client refreshed twice at once, so give the second request the current tokens
75
+ const refreshedAt = sessionExpiresAt - refreshTokenExpiration;
76
+ if (Date.now() - refreshedAt < refreshTokenReuseInterval) {
77
+ return await signLoginToken({
78
+ collection,
79
+ req,
80
+ session,
81
+ user
82
+ });
83
+ }
84
+ // Someone copied the refresh token
85
+ await saveSessions({
86
+ collection,
87
+ req,
88
+ sessions: user.sessions.filter(({ id })=>id !== sid),
89
+ user
90
+ });
91
+ return null;
92
+ };
93
+ // The full document, like the one a password login starts its session on
94
+ const findUser = ({ id, collection, req })=>req.payload.db.findOne({
95
+ collection: collection.slug,
96
+ req,
97
+ where: {
98
+ id: {
99
+ equals: id
100
+ }
101
+ }
102
+ });
103
+ /** Adds or extends the session `sid`, so it expires `refreshTokenExpiration` from now. */ const saveSession = async ({ collection, req, sid, user })=>{
104
+ const now = new Date();
105
+ const session = {
106
+ id: sid,
107
+ createdAt: user.sessions?.find(({ id })=>id === sid)?.createdAt ?? now,
108
+ expiresAt: new Date(now.getTime() + refreshTokenExpiration)
109
+ };
110
+ await saveSessions({
111
+ collection,
112
+ req,
113
+ sessions: [
114
+ ...removeExpiredSessions(user.sessions ?? []).filter(({ id })=>id !== sid),
115
+ session
116
+ ],
117
+ user
118
+ });
119
+ return session;
120
+ };
121
+ const saveSessions = async ({ collection, req, sessions, user })=>{
122
+ await req.payload.db.updateOne({
123
+ id: user.id,
124
+ collection: collection.slug,
125
+ // Prevent updatedAt from being updated when only changing sessions
126
+ data: {
127
+ ...user,
128
+ sessions,
129
+ updatedAt: null
130
+ },
131
+ req,
132
+ returning: false
133
+ });
134
+ };
135
+ const signLoginToken = async ({ collection, req, session, user })=>{
136
+ const { token } = await jwtSign({
137
+ fieldsToSign: getFieldsToSign({
138
+ collectionConfig: collection,
139
+ email: user.email,
140
+ sid: session?.id,
141
+ user
142
+ }),
143
+ secret: req.payload.secret,
144
+ tokenExpiration: collection.auth.tokenExpiration
145
+ });
146
+ return {
147
+ accessToken: token,
148
+ expiresIn: collection.auth.tokenExpiration,
149
+ refreshToken: session && JSON.stringify({
150
+ collection: collection.slug,
151
+ expiresAt: new Date(session.expiresAt).getTime(),
152
+ sid: session.id,
153
+ userID: user.id
154
+ })
155
+ };
156
+ };
157
+
158
+ //# sourceMappingURL=oauthLoginToken.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoint/oauthLoginToken.ts"],"sourcesContent":["import type { PayloadRequest, SanitizedCollectionConfig } from 'payload'\n\nimport { randomUUID } from 'crypto'\nimport { APIError, getFieldsToSign, jwtSign } from 'payload'\nimport { removeExpiredSessions } from 'payload/shared'\n\nimport type { MCPOAuthTokens } from '../types.js'\n\n/** How long a refresh token stays valid without being used, in milliseconds: 30 days. */\nconst refreshTokenExpiration = 30 * 24 * 60 * 60 * 1000\n\n/**\n * How long a used refresh token is still accepted, in milliseconds. MCP clients that send\n * requests in parallel can refresh twice at once with the same refresh token.\n */\nconst refreshTokenReuseInterval = 30 * 1000\n\ntype User = NonNullable<PayloadRequest['user']>\n\ntype Session = NonNullable<User['sessions']>[number]\n\n/** The default refresh token. The OAuth server seals it, so clients can't read or change it. */\ntype LoginRefreshToken = {\n collection: string\n /** The session's `expiresAt` when this refresh token was issued. Every refresh moves it. */\n expiresAt: number\n sid: string\n userID: number | string\n}\n\n/**\n * The default `issueToken`: a Payload login token, which Payload's JWT strategy accepts. When the\n * collection uses sessions, the token gets its own login session, and the client gets a refresh\n * token for it.\n */\nexport const issueLoginToken = async ({\n req,\n}: {\n req: PayloadRequest\n}): Promise<MCPOAuthTokens> => {\n const collection = req.payload.collections[req.user!.collection]!.config\n\n if (collection.auth.disableLocalStrategy) {\n throw new APIError(\n `MCP clients can't get Payload login tokens for the \"${collection.slug}\" collection because its local strategy is disabled. Pass oauth.issueToken to the MCP plugin.`,\n )\n }\n\n const user = (await findUser({ id: req.user!.id, collection, req }))!\n\n if (!collection.auth.useSessions) {\n return await signLoginToken({ collection, req, user })\n }\n\n const session = await saveSession({ collection, req, sid: randomUUID(), user })\n\n return await signLoginToken({ collection, req, session, user })\n}\n\n/**\n * The default `refreshToken`. Every refresh moves the session's expiry, so a refresh token with an\n * older expiry was already used. Outside of parallel refreshes, that means someone copied it, and\n * the session is signed out, which also revokes the client's access token.\n */\nexport const refreshLoginToken = async ({\n refreshToken,\n req,\n}: {\n refreshToken: string\n req: PayloadRequest\n}): Promise<MCPOAuthTokens | null> => {\n const { collection: slug, expiresAt, sid, userID } = JSON.parse(refreshToken) as LoginRefreshToken\n const collection = req.payload.collections[slug]?.config\n const user = collection && (await findUser({ id: userID, collection, req }))\n const session = removeExpiredSessions(user?.sessions ?? []).find(({ id }) => id === sid)\n\n // Signed out, expired, or the user was deleted\n if (!collection || !user || !session) {\n return null\n }\n\n const sessionExpiresAt = new Date(session.expiresAt).getTime()\n\n if (sessionExpiresAt === expiresAt) {\n return await signLoginToken({\n collection,\n req,\n session: await saveSession({ collection, req, sid, user }),\n user,\n })\n }\n\n // The client refreshed twice at once, so give the second request the current tokens\n const refreshedAt = sessionExpiresAt - refreshTokenExpiration\n\n if (Date.now() - refreshedAt < refreshTokenReuseInterval) {\n return await signLoginToken({ collection, req, session, user })\n }\n\n // Someone copied the refresh token\n await saveSessions({\n collection,\n req,\n sessions: user.sessions!.filter(({ id }) => id !== sid),\n user,\n })\n\n return null\n}\n\n// The full document, like the one a password login starts its session on\nconst findUser = ({\n id,\n collection,\n req,\n}: {\n collection: SanitizedCollectionConfig\n id: number | string\n req: PayloadRequest\n}): Promise<null | User> =>\n req.payload.db.findOne<User>({ collection: collection.slug, req, where: { id: { equals: id } } })\n\n/** Adds or extends the session `sid`, so it expires `refreshTokenExpiration` from now. */\nconst saveSession = async ({\n collection,\n req,\n sid,\n user,\n}: {\n collection: SanitizedCollectionConfig\n req: PayloadRequest\n sid: string\n user: User\n}): Promise<Session> => {\n const now = new Date()\n const session: Session = {\n id: sid,\n createdAt: user.sessions?.find(({ id }) => id === sid)?.createdAt ?? now,\n expiresAt: new Date(now.getTime() + refreshTokenExpiration),\n }\n\n await saveSessions({\n collection,\n req,\n sessions: [\n ...removeExpiredSessions(user.sessions ?? []).filter(({ id }) => id !== sid),\n session,\n ],\n user,\n })\n\n return session\n}\n\nconst saveSessions = async ({\n collection,\n req,\n sessions,\n user,\n}: {\n collection: SanitizedCollectionConfig\n req: PayloadRequest\n sessions: Session[]\n user: User\n}): Promise<void> => {\n await req.payload.db.updateOne({\n id: user.id,\n collection: collection.slug,\n // Prevent updatedAt from being updated when only changing sessions\n data: { ...user, sessions, updatedAt: null },\n req,\n returning: false,\n })\n}\n\nconst signLoginToken = async ({\n collection,\n req,\n session,\n user,\n}: {\n collection: SanitizedCollectionConfig\n req: PayloadRequest\n session?: Session\n user: User\n}): Promise<MCPOAuthTokens> => {\n const { token } = await jwtSign({\n fieldsToSign: getFieldsToSign({\n collectionConfig: collection,\n email: user.email!,\n sid: session?.id,\n user,\n }),\n secret: req.payload.secret,\n tokenExpiration: collection.auth.tokenExpiration,\n })\n\n return {\n accessToken: token,\n expiresIn: collection.auth.tokenExpiration,\n refreshToken:\n session &&\n JSON.stringify({\n collection: collection.slug,\n expiresAt: new Date(session.expiresAt).getTime(),\n sid: session.id,\n userID: user.id,\n } satisfies LoginRefreshToken),\n }\n}\n"],"names":["randomUUID","APIError","getFieldsToSign","jwtSign","removeExpiredSessions","refreshTokenExpiration","refreshTokenReuseInterval","issueLoginToken","req","collection","payload","collections","user","config","auth","disableLocalStrategy","slug","findUser","id","useSessions","signLoginToken","session","saveSession","sid","refreshLoginToken","refreshToken","expiresAt","userID","JSON","parse","sessions","find","sessionExpiresAt","Date","getTime","refreshedAt","now","saveSessions","filter","db","findOne","where","equals","createdAt","updateOne","data","updatedAt","returning","token","fieldsToSign","collectionConfig","email","secret","tokenExpiration","accessToken","expiresIn","stringify"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { Endpoint } from 'payload';
2
+ /**
3
+ * Turns Payload into the OAuth authorization server for MCP clients when `oauth.type` is
4
+ * `'builtin'`. Clients register, the user allows them after logging in to the admin panel, and
5
+ * the client receives the tokens `issueToken` creates.
6
+ *
7
+ * Clients, consent forms, authorization codes, and refresh tokens are encrypted and handed to the
8
+ * client instead of being stored. Only the default tokens write to the database, to the user's
9
+ * login sessions.
10
+ */
11
+ export declare const oauthServerEndpoints: Endpoint[];
12
+ //# sourceMappingURL=oauthServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauthServer.d.ts","sourceRoot":"","sources":["../../src/endpoint/oauthServer.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAkC,MAAM,SAAS,CAAA;AAmCvE;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,EAAE,QAAQ,EAqC1C,CAAA"}