@payloadcms/plugin-mcp 4.0.0-internal.7b031c8 → 4.0.0-internal.7d5c665

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 (188) hide show
  1. package/bin.js +0 -7
  2. package/dist/endpoint/access.d.ts.map +1 -1
  3. package/dist/endpoint/access.js +17 -5
  4. package/dist/endpoint/access.js.map +1 -1
  5. package/dist/endpoint/index.d.ts.map +1 -1
  6. package/dist/endpoint/index.js +46 -4
  7. package/dist/endpoint/index.js.map +1 -1
  8. package/dist/endpoint/oauth.d.ts +35 -0
  9. package/dist/endpoint/oauth.d.ts.map +1 -0
  10. package/dist/endpoint/oauth.js +75 -0
  11. package/dist/endpoint/oauth.js.map +1 -0
  12. package/dist/index.d.ts +8 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +12 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp/buildMcpServer.d.ts +2 -2
  17. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  18. package/dist/mcp/buildMcpServer.js +36 -24
  19. package/dist/mcp/buildMcpServer.js.map +1 -1
  20. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  21. package/dist/mcp/builtin/collections/authTools.js +80 -56
  22. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  23. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -1
  24. package/dist/mcp/builtin/collections/countTool.js +9 -14
  25. package/dist/mcp/builtin/collections/countTool.js.map +1 -1
  26. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -1
  27. package/dist/mcp/builtin/collections/countVersionsTool.js +9 -13
  28. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -1
  29. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  30. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  31. package/dist/mcp/builtin/collections/createTool.js +104 -63
  32. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  33. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  34. package/dist/mcp/builtin/collections/deleteTool.js +44 -43
  35. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  36. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  37. package/dist/mcp/builtin/collections/duplicateTool.js +31 -37
  38. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  39. package/dist/mcp/builtin/collections/fileInput.d.ts +21 -13
  40. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
  41. package/dist/mcp/builtin/collections/fileInput.js +52 -64
  42. package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
  43. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -1
  44. package/dist/mcp/builtin/collections/findDistinctTool.js +11 -26
  45. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -1
  46. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  47. package/dist/mcp/builtin/collections/findTool.js +27 -45
  48. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  49. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -1
  50. package/dist/mcp/builtin/collections/findVersionByIDTool.js +17 -26
  51. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -1
  52. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -1
  53. package/dist/mcp/builtin/collections/findVersionsTool.js +16 -30
  54. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -1
  55. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  56. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +33 -16
  57. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  58. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -1
  59. package/dist/mcp/builtin/collections/restoreVersionTool.js +14 -26
  60. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  61. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  62. package/dist/mcp/builtin/collections/updateTool.js +58 -90
  63. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  64. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
  65. package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
  66. package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
  67. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
  68. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  69. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  70. package/dist/mcp/builtin/formatEntityError.js +55 -0
  71. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  72. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
  73. package/dist/mcp/builtin/getConfigInfoTool.js +3 -2
  74. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
  75. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -1
  76. package/dist/mcp/builtin/globals/countVersionsTool.js +9 -13
  77. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -1
  78. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  79. package/dist/mcp/builtin/globals/findTool.js +10 -16
  80. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  81. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -1
  82. package/dist/mcp/builtin/globals/findVersionByIDTool.js +13 -24
  83. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -1
  84. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -1
  85. package/dist/mcp/builtin/globals/findVersionsTool.js +13 -29
  86. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -1
  87. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  88. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +29 -13
  89. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  90. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -1
  91. package/dist/mcp/builtin/globals/restoreVersionTool.js +15 -22
  92. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -1
  93. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  94. package/dist/mcp/builtin/globals/updateTool.js +27 -36
  95. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  96. package/dist/mcp/builtinTools.d.ts +6 -0
  97. package/dist/mcp/builtinTools.d.ts.map +1 -1
  98. package/dist/mcp/builtinTools.js +9 -3
  99. package/dist/mcp/builtinTools.js.map +1 -1
  100. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  101. package/dist/mcp/sanitizeMCPConfig.js +5 -1
  102. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  103. package/dist/stdio.d.ts.map +1 -1
  104. package/dist/stdio.js +6 -4
  105. package/dist/stdio.js.map +1 -1
  106. package/dist/types.d.ts +49 -8
  107. package/dist/types.d.ts.map +1 -1
  108. package/dist/types.js.map +1 -1
  109. package/package.json +6 -8
  110. package/src/endpoint/access.ts +14 -6
  111. package/src/endpoint/index.ts +41 -3
  112. package/src/endpoint/oauth.ts +87 -0
  113. package/src/index.ts +19 -2
  114. package/src/mcp/buildMcpServer.ts +40 -30
  115. package/src/mcp/builtin/collections/authTools.ts +80 -69
  116. package/src/mcp/builtin/collections/countTool.ts +9 -21
  117. package/src/mcp/builtin/collections/countVersionsTool.ts +9 -18
  118. package/src/mcp/builtin/collections/createTool.ts +110 -86
  119. package/src/mcp/builtin/collections/deleteTool.ts +36 -64
  120. package/src/mcp/builtin/collections/duplicateTool.ts +35 -85
  121. package/src/mcp/builtin/collections/fileInput.spec.ts +20 -6
  122. package/src/mcp/builtin/collections/fileInput.ts +64 -76
  123. package/src/mcp/builtin/collections/findDistinctTool.ts +11 -63
  124. package/src/mcp/builtin/collections/findTool.ts +23 -105
  125. package/src/mcp/builtin/collections/findVersionByIDTool.ts +15 -58
  126. package/src/mcp/builtin/collections/findVersionsTool.ts +14 -85
  127. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +28 -13
  128. package/src/mcp/builtin/collections/restoreVersionTool.ts +14 -56
  129. package/src/mcp/builtin/collections/updateTool.ts +57 -111
  130. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
  131. package/src/mcp/builtin/formatEntityError.ts +77 -0
  132. package/src/mcp/builtin/getConfigInfoTool.ts +2 -1
  133. package/src/mcp/builtin/globals/countVersionsTool.ts +9 -17
  134. package/src/mcp/builtin/globals/findTool.ts +12 -51
  135. package/src/mcp/builtin/globals/findVersionByIDTool.ts +13 -52
  136. package/src/mcp/builtin/globals/findVersionsTool.ts +14 -92
  137. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +21 -11
  138. package/src/mcp/builtin/globals/restoreVersionTool.ts +14 -45
  139. package/src/mcp/builtin/globals/updateTool.ts +37 -60
  140. package/src/mcp/builtinTools.ts +9 -2
  141. package/src/mcp/sanitizeMCPConfig.ts +5 -1
  142. package/src/stdio.ts +4 -3
  143. package/src/types.ts +50 -8
  144. package/dist/mcp/builtin/collections/fileInput.spec.js +0 -72
  145. package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
  146. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
  147. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
  148. package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
  149. package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
  150. package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
  151. package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
  152. package/dist/mcp/builtin/validateEntityData.js +0 -82
  153. package/dist/mcp/builtin/validateEntityData.js.map +0 -1
  154. package/dist/utils/getVirtualFieldNames.d.ts +0 -14
  155. package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
  156. package/dist/utils/getVirtualFieldNames.js +0 -35
  157. package/dist/utils/getVirtualFieldNames.js.map +0 -1
  158. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +0 -28
  159. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +0 -1
  160. package/dist/utils/schemaConversion/filterFieldsByAccess.js +0 -71
  161. package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +0 -1
  162. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +0 -13
  163. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +0 -1
  164. package/dist/utils/schemaConversion/getEntityInputSchema.js +0 -66
  165. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +0 -1
  166. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -10
  167. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
  168. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -384
  169. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
  170. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -133
  171. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
  172. package/dist/utils/transformPointDataToPayload.d.ts +0 -7
  173. package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
  174. package/dist/utils/transformPointDataToPayload.js +0 -28
  175. package/dist/utils/transformPointDataToPayload.js.map +0 -1
  176. package/dist/utils/whereSchema.d.ts +0 -9
  177. package/dist/utils/whereSchema.d.ts.map +0 -1
  178. package/dist/utils/whereSchema.js +0 -13
  179. package/dist/utils/whereSchema.js.map +0 -1
  180. package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
  181. package/src/mcp/builtin/validateEntityData.ts +0 -132
  182. package/src/utils/getVirtualFieldNames.ts +0 -53
  183. package/src/utils/schemaConversion/filterFieldsByAccess.ts +0 -123
  184. package/src/utils/schemaConversion/getEntityInputSchema.ts +0 -96
  185. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -96
  186. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -432
  187. package/src/utils/transformPointDataToPayload.ts +0 -40
  188. package/src/utils/whereSchema.ts +0 -24
package/bin.js CHANGED
@@ -27,13 +27,6 @@ const start = async () => {
27
27
  return
28
28
  }
29
29
 
30
- // tsx 4.22.4's synchronous loader leaks its namespace query onto Node built-ins on Node 23.5+.
31
- // Force the stable asynchronous loader path, matching payload/bin.js.
32
- const nodeModule = await import('node:module')
33
- if (typeof nodeModule.default.registerHooks === 'function') {
34
- nodeModule.default.registerHooks = undefined
35
- }
36
-
37
30
  const { tsImport } = await import('tsx/esm/api')
38
31
  const { runMcpStdio } = await tsImport('./dist/stdio.js', baseURL)
39
32
  await runMcpStdio()
@@ -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
  }
@@ -62,13 +74,13 @@ export const filterMCPItems = async ({ items, overrideAccess, req })=>{
62
74
  switch(item.type){
63
75
  case 'collectionTool':
64
76
  return !item.tool.access || await item.tool.access({
65
- collectionSlug: item.collectionSlug,
77
+ slug: item.collectionSlug,
66
78
  permissions,
67
79
  req
68
80
  });
69
81
  case 'globalTool':
70
82
  return !item.tool.access || await item.tool.access({
71
- globalSlug: item.globalSlug,
83
+ slug: item.globalSlug,
72
84
  permissions,
73
85
  req
74
86
  });
@@ -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({ collectionSlug: item.collectionSlug, permissions, req }))\n )\n case 'globalTool':\n return (\n !item.tool.access ||\n (await item.tool.access({ globalSlug: 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","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,gBAAgBN,KAAKM,cAAc;gBAAEP;gBAAahB;YAAI;QAEpF,KAAK;YACH,OACE,CAACiB,KAAKI,IAAI,CAACC,MAAM,IAChB,MAAML,KAAKI,IAAI,CAACC,MAAM,CAAC;gBAAEE,YAAYP,KAAKO,UAAU;gBAAER;gBAAahB;YAAI;QAE5E,KAAK;YACH,OAAO,CAACiB,KAAKQ,MAAM,CAACH,MAAM,IAAK,MAAML,KAAKQ,MAAM,CAACH,MAAM,CAAC;gBAAEN;gBAAahB;YAAI;QAC7E,KAAK;YACH,OAAO,CAACiB,KAAKS,QAAQ,CAACJ,MAAM,IAAK,MAAML,KAAKS,QAAQ,CAACJ,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;AAa7D,eAAO,MAAM,WAAW,EAAE,cA+DzB,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"}
@@ -1,13 +1,21 @@
1
- import { createMcpHandler, isLegacyRequest, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
1
+ import { createMcpHandler, isLegacyRequest, readRequestBody, WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/server';
2
2
  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,10 +33,22 @@ 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
+ }
41
+ const maxRequestBodySize = pluginConfig.mcp?.maxRequestBodySize;
28
42
  // Payload augments the original web-standard Request in place.
29
43
  const mcpRequest = req;
44
+ const parsedBody = await parseRequestBody({
45
+ maxRequestBodySize,
46
+ request: mcpRequest
47
+ });
30
48
  // Keep the old JSON-only, stateless behavior because the SDK's 2025 fallback uses SSE.
31
- if (await isLegacyRequest(mcpRequest)) {
49
+ if (await isLegacyRequest(mcpRequest, parsedBody, {
50
+ maxRequestBodySize
51
+ })) {
32
52
  const server = buildMcpServer({
33
53
  authorizedMCP,
34
54
  pluginConfig,
@@ -36,6 +56,7 @@ export const mcpEndpoint = async (req)=>{
36
56
  });
37
57
  const transport = new WebStandardStreamableHTTPServerTransport({
38
58
  enableJsonResponse: true,
59
+ maxRequestBodySize,
39
60
  sessionIdGenerator: undefined
40
61
  });
41
62
  transport.onerror = (err)=>{
@@ -46,7 +67,9 @@ export const mcpEndpoint = async (req)=>{
46
67
  };
47
68
  try {
48
69
  await server.connect(transport);
49
- return await transport.handleRequest(mcpRequest);
70
+ return await transport.handleRequest(mcpRequest, {
71
+ parsedBody
72
+ });
50
73
  } finally{
51
74
  await server.close().catch((err)=>{
52
75
  req.payload.logger.error({
@@ -62,6 +85,7 @@ export const mcpEndpoint = async (req)=>{
62
85
  req
63
86
  }), {
64
87
  legacy: 'reject',
88
+ maxRequestBodySize,
65
89
  // SDK subscriptions always use SSE, so disable them to keep every response JSON-only.
66
90
  maxSubscriptions: 0,
67
91
  onerror: (err)=>{
@@ -73,7 +97,9 @@ export const mcpEndpoint = async (req)=>{
73
97
  responseMode: 'json'
74
98
  });
75
99
  try {
76
- return await handler.fetch(mcpRequest);
100
+ return await handler.fetch(mcpRequest, {
101
+ parsedBody
102
+ });
77
103
  } finally{
78
104
  await handler.close().catch((err)=>{
79
105
  req.payload.logger.error({
@@ -83,5 +109,21 @@ export const mcpEndpoint = async (req)=>{
83
109
  });
84
110
  }
85
111
  };
112
+ /**
113
+ * Parses a POST body once so the era check and the SDK handler can share it. The SDK
114
+ * skips its size limit for a pre-parsed body, so this reads with the same limit. It reads
115
+ * a clone and returns `undefined` for any body the SDK should reject (too large, unreadable,
116
+ * or not JSON), leaving the original request for the SDK to read and answer.
117
+ */ const parseRequestBody = async ({ maxRequestBodySize, request })=>{
118
+ if (request.method.toUpperCase() !== 'POST') {
119
+ return undefined;
120
+ }
121
+ try {
122
+ const body = await readRequestBody(request.clone(), maxRequestBodySize);
123
+ return body.tooLarge ? undefined : JSON.parse(body.text);
124
+ } catch {
125
+ return undefined;
126
+ }
127
+ };
86
128
 
87
129
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/endpoint/index.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport {\n createMcpHandler,\n isLegacyRequest,\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 // Payload augments the original web-standard Request in place.\n const mcpRequest = req as PayloadRequest & Request\n\n // Keep the old JSON-only, stateless behavior because the SDK's 2025 fallback uses SSE.\n if (await isLegacyRequest(mcpRequest)) {\n const server = buildMcpServer({ authorizedMCP, pluginConfig, req })\n const transport = new WebStandardStreamableHTTPServerTransport({\n enableJsonResponse: true,\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)\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 // 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)\n } finally {\n await handler.close().catch((err) => {\n req.payload.logger.error({ err, msg: 'Error closing modern MCP handler' })\n })\n }\n}\n"],"names":["createMcpHandler","isLegacyRequest","WebStandardStreamableHTTPServerTransport","APIError","buildMcpServer","getPluginConfig","getAuthorizedMCP","mcpEndpoint","req","url","payloadAPI","pluginConfig","config","payload","overrideAccessParam","URL","searchParams","get","process","env","NODE_ENV","overrideAccess","authorizedMCP","mcpRequest","server","transport","enableJsonResponse","sessionIdGenerator","undefined","onerror","err","logger","error","msg","connect","handleRequest","close","catch","handler","legacy","maxSubscriptions","responseMode","fetch"],"mappings":"AAEA,SACEA,gBAAgB,EAChBC,eAAe,EACfC,wCAAwC,QACnC,+BAA8B;AACrC,SAASC,QAAQ,QAAQ,UAAS;AAElC,SAASC,cAAc,QAAQ,2BAA0B;AACzD,SAASC,eAAe,QAAQ,8BAA6B;AAC7D,SAASC,gBAAgB,QAAQ,cAAa;AAE9C,OAAO,MAAMC,cAA8B,OAAOC;IAChD,IAAI,CAACA,IAAIC,GAAG,EAAE;QACZ,MAAM,IAAIN,SAAS,uBAAuB;IAC5C;IAEAK,IAAIE,UAAU,GAAG;IAEjB,MAAMC,eAAeN,gBAAgB;QAAEO,QAAQJ,IAAIK,OAAO,CAACD,MAAM;IAAC;IAClE,MAAME,sBAAsB,IAAIC,IAAIP,IAAIC,GAAG,EAAEO,YAAY,CAACC,GAAG,CAAC;IAE9D,IAAIH,wBAAwB,QAAQI,QAAQC,GAAG,CAACC,QAAQ,KAAK,eAAe;QAC1E,MAAM,IAAIjB,SAAS,wDAAwD;IAC7E;IAEA,IAAIkB,iBAAiB;IACrB,IAAIP,wBAAwB,QAAQ;QAClCO,iBAAiB;IACnB,OAAO,IAAIP,wBAAwB,QAAQA,wBAAwB,SAAS;QAC1E,MAAM,IAAIX,SAAS,iDAAiD;IACtE;IAEA,MAAMmB,gBAAgB,MAAMhB,iBAAiB;QAAEe;QAAgBb;IAAI;IACnE,+DAA+D;IAC/D,MAAMe,aAAaf;IAEnB,uFAAuF;IACvF,IAAI,MAAMP,gBAAgBsB,aAAa;QACrC,MAAMC,SAASpB,eAAe;YAAEkB;YAAeX;YAAcH;QAAI;QACjE,MAAMiB,YAAY,IAAIvB,yCAAyC;YAC7DwB,oBAAoB;YACpBC,oBAAoBC;QACtB;QACAH,UAAUI,OAAO,GAAG,CAACC;YACnBtB,IAAIK,OAAO,CAACkB,MAAM,CAACC,KAAK,CAAC;gBAAEF;gBAAKG,KAAK;YAAmC;QAC1E;QAEA,IAAI;YACF,MAAMT,OAAOU,OAAO,CAACT;YACrB,OAAO,MAAMA,UAAUU,aAAa,CAACZ;QACvC,SAAU;YACR,MAAMC,OAAOY,KAAK,GAAGC,KAAK,CAAC,CAACP;gBAC1BtB,IAAIK,OAAO,CAACkB,MAAM,CAACC,KAAK,CAAC;oBAAEF;oBAAKG,KAAK;gBAA2B;YAClE;QACF;IACF;IAEA,MAAMK,UAAUtC,iBAAiB,IAAMI,eAAe;YAAEkB;YAAeX;YAAcH;QAAI,IAAI;QAC3F+B,QAAQ;QACR,sFAAsF;QACtFC,kBAAkB;QAClBX,SAAS,CAACC;YACRtB,IAAIK,OAAO,CAACkB,MAAM,CAACC,KAAK,CAAC;gBAAEF;gBAAKG,KAAK;YAAmC;QAC1E;QACAQ,cAAc;IAChB;IAEA,IAAI;QACF,OAAO,MAAMH,QAAQI,KAAK,CAACnB;IAC7B,SAAU;QACR,MAAMe,QAAQF,KAAK,GAAGC,KAAK,CAAC,CAACP;YAC3BtB,IAAIK,OAAO,CAACkB,MAAM,CAACC,KAAK,CAAC;gBAAEF;gBAAKG,KAAK;YAAmC;QAC1E;IACF;AACF,EAAC"}
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,35 @@
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
+ //# 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;AAU9E;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,cACuB,CAAA;AAEpE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,WAC/B,OAAO,CAAC,eAAe,CAAC,GAAG,eAAe,eACnC,OAAO,KAAG,OAAO,CAAC,QAAQ,CACuB,CAAA;AAEnE;;;GAGG;AACH,eAAO,MAAM,cAAc,YAAa;IAAE,GAAG,EAAE,cAAc,CAAA;CAAE,KAAG,QAO/D,CAAA;AAEH;;;GAGG;AACH,eAAO,MAAM,cAAc,oBAAqB;IAAE,MAAM,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,KAAG,MAChB,CAAA"}
@@ -0,0 +1,75 @@
1
+ import { bearerAuthChallengeResponse, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
2
+ import { getPluginConfig } from '../utils/getPluginConfig.js';
3
+ /**
4
+ * Serves the OAuth protected resource metadata (RFC 9728) at `/api/mcp/oauth-protected-resource`.
5
+ * The OAuth challenge links here, so MCP clients find the authorization server without setup.
6
+ */ export const oauthProtectedResourceEndpoint = (req)=>getMetadataResponse({
7
+ config: req.payload.config,
8
+ url: req.url
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
+ config: await config,
23
+ url: request.url
24
+ });
25
+ /**
26
+ * Answers an unauthenticated MCP request with a challenge linking to the protected resource
27
+ * metadata, which starts the client's OAuth flow.
28
+ */ export const oauthChallenge = ({ req })=>bearerAuthChallengeResponse(new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')), {
29
+ requiredScopes: getPluginConfig({
30
+ config: req.payload.config
31
+ }).oauth?.scopes,
32
+ resourceMetadataUrl: `${getMCPURL({
33
+ config: req.payload.config,
34
+ url: req.url
35
+ })}/oauth-protected-resource`
36
+ });
37
+ /**
38
+ * The MCP endpoint's address, as advertised in the metadata's `resource`. Clients send it when
39
+ * they sign in, and authorization servers that support it put it in the access token's audience.
40
+ */ export const getMCPResource = ({ config, url })=>getPluginConfig({
41
+ config
42
+ }).oauth?.resource ?? getMCPURL({
43
+ config,
44
+ url
45
+ });
46
+ const getMCPURL = ({ config, url })=>new URL(`${config.routes.api}/mcp`, config.serverURL || url).href;
47
+ const getMetadataResponse = async ({ config, url })=>{
48
+ const pluginConfig = getPluginConfig({
49
+ config
50
+ });
51
+ if (!pluginConfig.oauth) {
52
+ return new Response(null, {
53
+ status: 404
54
+ });
55
+ }
56
+ const { authorizationServers, scopes } = pluginConfig.oauth;
57
+ return Response.json({
58
+ authorization_servers: typeof authorizationServers === 'function' ? await authorizationServers() : authorizationServers,
59
+ bearer_methods_supported: [
60
+ 'header'
61
+ ],
62
+ resource: getMCPResource({
63
+ config,
64
+ url
65
+ }),
66
+ scopes_supported: scopes
67
+ }, // Public document, readable by browser-based MCP clients on any origin
68
+ {
69
+ headers: {
70
+ 'Access-Control-Allow-Origin': '*'
71
+ }
72
+ });
73
+ };
74
+
75
+ //# 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'\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) =>\n getMetadataResponse({ config: req.payload.config, url: req.url! })\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({ config: await config, url: request.url })\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 bearerAuthChallengeResponse(\n new OAuthError(OAuthErrorCode.InvalidToken, req.t('error:unauthorized')),\n {\n requiredScopes: getPluginConfig({ config: req.payload.config }).oauth?.scopes,\n resourceMetadataUrl: `${getMCPURL({ config: req.payload.config, url: req.url! })}/oauth-protected-resource`,\n },\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\nconst getMCPURL = ({ config, url }: { config: SanitizedConfig; url: string }): string =>\n new URL(`${config.routes.api}/mcp`, config.serverURL || url).href\n\nconst getMetadataResponse = async ({\n config,\n url,\n}: {\n config: SanitizedConfig\n url: string\n}): Promise<Response> => {\n const pluginConfig = getPluginConfig({ config })\n\n if (!pluginConfig.oauth) {\n return new Response(null, { status: 404 })\n }\n\n const { authorizationServers, scopes } = pluginConfig.oauth\n\n return Response.json(\n {\n authorization_servers:\n typeof authorizationServers === 'function'\n ? await authorizationServers()\n : authorizationServers,\n bearer_methods_supported: ['header'],\n resource: getMCPResource({ config, url }),\n scopes_supported: scopes,\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","getPluginConfig","oauthProtectedResourceEndpoint","req","getMetadataResponse","config","payload","url","oauthProtectedResourceHandler","request","oauthChallenge","InvalidToken","t","requiredScopes","oauth","scopes","resourceMetadataUrl","getMCPURL","getMCPResource","resource","URL","routes","api","serverURL","href","pluginConfig","Response","status","authorizationServers","json","authorization_servers","bearer_methods_supported","scopes_supported","headers"],"mappings":"AAGA,SACEA,2BAA2B,EAC3BC,UAAU,EACVC,cAAc,QACT,+BAA8B;AAErC,SAASC,eAAe,QAAQ,8BAA6B;AAE7D;;;CAGC,GACD,OAAO,MAAMC,iCAAiD,CAACC,MAC7DC,oBAAoB;QAAEC,QAAQF,IAAIG,OAAO,CAACD,MAAM;QAAEE,KAAKJ,IAAII,GAAG;IAAE,GAAE;AAEpE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,gCACX,CAACH,SACD,OAAOI,UACLL,oBAAoB;YAAEC,QAAQ,MAAMA;YAAQE,KAAKE,QAAQF,GAAG;QAAC,GAAE;AAEnE;;;CAGC,GACD,OAAO,MAAMG,iBAAiB,CAAC,EAAEP,GAAG,EAA2B,GAC7DL,4BACE,IAAIC,WAAWC,eAAeW,YAAY,EAAER,IAAIS,CAAC,CAAC,wBAClD;QACEC,gBAAgBZ,gBAAgB;YAAEI,QAAQF,IAAIG,OAAO,CAACD,MAAM;QAAC,GAAGS,KAAK,EAAEC;QACvEC,qBAAqB,GAAGC,UAAU;YAAEZ,QAAQF,IAAIG,OAAO,CAACD,MAAM;YAAEE,KAAKJ,IAAII,GAAG;QAAE,GAAG,yBAAyB,CAAC;IAC7G,GACD;AAEH;;;CAGC,GACD,OAAO,MAAMW,iBAAiB,CAAC,EAAEb,MAAM,EAAEE,GAAG,EAA4C,GACtFN,gBAAgB;QAAEI;IAAO,GAAGS,KAAK,EAAEK,YAAYF,UAAU;QAAEZ;QAAQE;IAAI,GAAE;AAE3E,MAAMU,YAAY,CAAC,EAAEZ,MAAM,EAAEE,GAAG,EAA4C,GAC1E,IAAIa,IAAI,GAAGf,OAAOgB,MAAM,CAACC,GAAG,CAAC,IAAI,CAAC,EAAEjB,OAAOkB,SAAS,IAAIhB,KAAKiB,IAAI;AAEnE,MAAMpB,sBAAsB,OAAO,EACjCC,MAAM,EACNE,GAAG,EAIJ;IACC,MAAMkB,eAAexB,gBAAgB;QAAEI;IAAO;IAE9C,IAAI,CAACoB,aAAaX,KAAK,EAAE;QACvB,OAAO,IAAIY,SAAS,MAAM;YAAEC,QAAQ;QAAI;IAC1C;IAEA,MAAM,EAAEC,oBAAoB,EAAEb,MAAM,EAAE,GAAGU,aAAaX,KAAK;IAE3D,OAAOY,SAASG,IAAI,CAClB;QACEC,uBACE,OAAOF,yBAAyB,aAC5B,MAAMA,yBACNA;QACNG,0BAA0B;YAAC;SAAS;QACpCZ,UAAUD,eAAe;YAAEb;YAAQE;QAAI;QACvCyB,kBAAkBjB;IACpB,GACA,uEAAuE;IACvE;QAAEkB,SAAS;YAAE,+BAA+B;QAAI;IAAE;AAEtD"}
package/dist/index.d.ts CHANGED
@@ -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
18
  export type { AuthorizedMCP, 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,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAM1F,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,EAAE,aAAa,EAAE,eAAe,EAAE,wBAAwB,EAAE,CAAA;AAExE,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,wDAyCpB,CAAA"}
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { definePlugin } from 'payload';
2
2
  import { mcpEndpoint } from './endpoint/index.js';
3
+ import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js';
3
4
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js';
4
5
  export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js';
6
+ export { oauthProtectedResourceHandler } from './endpoint/oauth.js';
5
7
  export const mcpPlugin = definePlugin({
6
8
  slug: '@payloadcms/plugin-mcp',
7
9
  order: 10,
@@ -23,8 +25,10 @@ export const mcpPlugin = definePlugin({
23
25
  ...config,
24
26
  endpoints: [
25
27
  ...config.endpoints ?? [],
26
- // Payload prefixes /api, so the full path is /api/mcp.
28
+ // Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
29
+ // request with dev auto-login turned off, so Payload doesn't authenticate it first.
27
30
  {
31
+ disableAuth: true,
28
32
  handler: mcpEndpoint,
29
33
  method: 'post',
30
34
  path: '/mcp'
@@ -32,6 +36,7 @@ export const mcpPlugin = definePlugin({
32
36
  // Streamable HTTP's optional server=>client GET stream. We don't offer
33
37
  // one, so answer 405 per spec-— clients then skip it cleanly
34
38
  {
39
+ disableAuth: true,
35
40
  handler: ()=>new Response(null, {
36
41
  headers: {
37
42
  Allow: 'POST'
@@ -40,6 +45,12 @@ export const mcpPlugin = definePlugin({
40
45
  }),
41
46
  method: 'get',
42
47
  path: '/mcp'
48
+ },
49
+ {
50
+ disableAuth: true,
51
+ handler: oauthProtectedResourceEndpoint,
52
+ method: 'get',
53
+ path: '/mcp/oauth-protected-resource'
43
54
  }
44
55
  ]
45
56
  };
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 { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'\n\nimport { mcpEndpoint } from './endpoint/index.js'\nimport { oauthProtectedResourceEndpoint } from './endpoint/oauth.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 { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig }\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 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. 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 ],\n }\n },\n})\n"],"names":["definePlugin","mcpEndpoint","oauthProtectedResourceEndpoint","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;AAItC,SAASC,WAAW,QAAQ,sBAAqB;AACjD,SAASC,8BAA8B,QAAQ,sBAAqB;AACpE,SAASC,iBAAiB,QAAQ,6BAA4B;AAqB9D,SAASC,oBAAoB,EAAEC,gBAAgB,EAAEC,YAAY,EAAEC,UAAU,QAAQ,kBAAiB;AAClG,SAASC,6BAA6B,QAAQ,sBAAqB;AAEnE,OAAO,MAAMC,YAAYT,aAA8B;IACrDU,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,SAASrB;oBAAasB,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,SAASpB;oBACTqB,QAAQ;oBACRC,MAAM;gBACR;aACD;QACH;IACF;AACF,GAAE"}
@@ -1,5 +1,5 @@
1
+ import type { PayloadRequest } from 'payload';
1
2
  import { McpServer } from '@modelcontextprotocol/server';
2
- import { type PayloadRequest } from 'payload';
3
3
  import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
4
4
  /**
5
5
  * Serving-entry-agnostic core: registers every authorized MCP item onto a fresh
@@ -9,7 +9,7 @@ import type { AuthorizedMCP, SanitizedMCPPluginConfig } from '../types.js';
9
9
  *
10
10
  * `req` is the request context handlers see. For HTTP it's the live
11
11
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
12
- * synthesized one built via `createLocalReq`.
12
+ * synthesized one built via `createPayloadRequest`.
13
13
  */
14
14
  export declare const buildMcpServer: ({ authorizedMCP, pluginConfig, req, }: {
15
15
  authorizedMCP: AuthorizedMCP;
@@ -1 +1 @@
1
- {"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAA;AAC5E,OAAO,EAAY,KAAK,cAAc,EAAE,MAAM,SAAS,CAAA;AAGvD,OAAO,KAAK,EACV,aAAa,EAMb,wBAAwB,EAEzB,MAAM,aAAa,CAAA;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0CAIxB;IACD,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,wBAAwB,CAAA;IACtC,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,SAkNH,CAAA"}
1
+ {"version":3,"file":"buildMcpServer.d.ts","sourceRoot":"","sources":["../../src/mcp/buildMcpServer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,OAAO,EAAE,SAAS,EAAsB,MAAM,8BAA8B,CAAA;AAG5E,OAAO,KAAK,EACV,aAAa,EAMb,wBAAwB,EAEzB,MAAM,aAAa,CAAA;AAKpB;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,0CAIxB;IACD,aAAa,EAAE,aAAa,CAAA;IAC5B,YAAY,EAAE,wBAAwB,CAAA;IACtC,GAAG,EAAE,cAAc,CAAA;CACpB,KAAG,SAgNH,CAAA"}
@@ -1,6 +1,5 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
- import { APIError } from 'payload';
3
- import { z } from 'zod';
2
+ import { APIError, strictObject, z } from 'payload';
4
3
  import { getLogger } from '../utils/getLogger.js';
5
4
  import { toStandardSchema } from '../utils/toStandardSchema.js';
6
5
  /**
@@ -11,8 +10,9 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
11
10
  *
12
11
  * `req` is the request context handlers see. For HTTP it's the live
13
12
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
14
- * synthesized one built via `createLocalReq`.
13
+ * synthesized one built via `createPayloadRequest`.
15
14
  */ export const buildMcpServer = ({ authorizedMCP, pluginConfig, req })=>{
15
+ z.config(z.locales.en());
16
16
  const serverOptions = pluginConfig.mcp?.serverOptions || {};
17
17
  const server = new McpServer({
18
18
  name: 'Payload MCP Server',
@@ -40,20 +40,19 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
40
40
  };
41
41
  /**
42
42
  * Runs a collection/global tool call:
43
- * - reads `collectionSlug` / `globalSlug` from the input
43
+ * - reads `slug` from the input
44
44
  * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug
45
45
  * - runs the tool handler and finalizes its response
46
46
  */ const callEntityTool = async ({ input, item, serverContext })=>{
47
47
  const entity = item.type === 'collectionTool' ? 'collection' : 'global';
48
- const slugKey = item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug';
49
48
  const toolInput = input ?? {};
50
- const slug = toolInput[slugKey];
49
+ const slug = toolInput.slug;
51
50
  if (!slug) {
52
51
  return {
53
52
  content: [
54
53
  {
55
54
  type: 'text',
56
- text: `Error: "${item.mcpName}" requires ${slugKey}. Use getConfigInfo to inspect ${entity} slugs.`
55
+ text: `Error: "${item.mcpName}" requires slug. Use getConfigInfo to inspect ${entity} slugs.`
57
56
  }
58
57
  ],
59
58
  isError: true
@@ -79,10 +78,10 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
79
78
  };
80
79
  const response = await (match.type === 'collectionTool' ? match.tool.handler({
81
80
  ...handlerArgs,
82
- collectionSlug: slug
81
+ slug
83
82
  }) : match.tool.handler({
84
83
  ...handlerArgs,
85
- globalSlug: slug
84
+ slug
86
85
  }));
87
86
  return finalizeToolResponse({
88
87
  input: toolInput,
@@ -103,7 +102,6 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
103
102
  }
104
103
  registeredEntityTools.add(item.mcpName);
105
104
  const inputSchema = withSlugInput({
106
- name: item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug',
107
105
  input: item.tool.input
108
106
  });
109
107
  server.registerTool(item.mcpName, {
@@ -189,33 +187,47 @@ import { toStandardSchema } from '../utils/toStandardSchema.js';
189
187
  }
190
188
  return server;
191
189
  };
192
- const withSlugInput = ({ name, input })=>{
193
- const description = name === 'collectionSlug' ? 'The collection slug' : 'The global slug';
194
- const slugSchema = z.string().describe(description);
190
+ const withSlugInput = ({ input })=>{
191
+ const description = 'The target slug.';
192
+ const slugSchema = z.string().check(z.describe(description));
195
193
  if (!input) {
196
- return z.object({
197
- [name]: slugSchema
194
+ return strictObject({
195
+ slug: slugSchema
198
196
  });
199
197
  }
200
- if (input instanceof z.ZodObject) {
201
- return input.extend({
202
- [name]: slugSchema
198
+ if (typeof input === 'object' && input !== null && '~standard' in input) {
199
+ const schema = input['~standard'].jsonSchema.input({
200
+ target: 'draft-2020-12'
201
+ });
202
+ const properties = schema.properties;
203
+ if (properties?.slug) {
204
+ return input;
205
+ }
206
+ return addSlugToJSONSchema({
207
+ description,
208
+ schema: schema
203
209
  });
204
210
  }
205
- const schema = input;
211
+ return addSlugToJSONSchema({
212
+ description,
213
+ schema: input
214
+ });
215
+ };
216
+ const addSlugToJSONSchema = ({ description, schema })=>{
217
+ const objectSchema = schema;
206
218
  return {
207
- ...schema,
219
+ ...objectSchema,
208
220
  type: 'object',
209
221
  properties: {
210
- ...schema.properties,
211
- [name]: {
222
+ ...objectSchema.properties,
223
+ slug: {
212
224
  type: 'string',
213
225
  description
214
226
  }
215
227
  },
216
228
  required: Array.from(new Set([
217
- name,
218
- ...schema.required ?? []
229
+ 'slug',
230
+ ...objectSchema.required ?? []
219
231
  ]))
220
232
  };
221
233
  };