@payloadcms/plugin-mcp 4.0.0-internal.40de3ec → 4.0.0-internal.45cfa3d

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 (221) hide show
  1. package/LICENSE.md +1 -1
  2. package/dist/defaultAccess.d.ts +3 -0
  3. package/dist/defaultAccess.d.ts.map +1 -0
  4. package/dist/defaultAccess.js +3 -0
  5. package/dist/defaultAccess.js.map +1 -0
  6. package/dist/endpoint/access.d.ts +17 -5
  7. package/dist/endpoint/access.d.ts.map +1 -1
  8. package/dist/endpoint/access.js +82 -88
  9. package/dist/endpoint/access.js.map +1 -1
  10. package/dist/endpoint/index.d.ts +1 -1
  11. package/dist/endpoint/index.d.ts.map +1 -1
  12. package/dist/endpoint/index.js +68 -17
  13. package/dist/endpoint/index.js.map +1 -1
  14. package/dist/exports/client.d.ts +1 -1
  15. package/dist/exports/client.d.ts.map +1 -1
  16. package/dist/exports/client.js +1 -1
  17. package/dist/exports/client.js.map +1 -1
  18. package/dist/exports/internal.d.ts +2 -0
  19. package/dist/exports/internal.d.ts.map +1 -0
  20. package/dist/exports/internal.js +3 -0
  21. package/dist/exports/internal.js.map +1 -0
  22. package/dist/index.d.ts +2 -2
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +15 -20
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp/buildMcpServer.d.ts +4 -4
  27. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  28. package/dist/mcp/buildMcpServer.js +133 -80
  29. package/dist/mcp/buildMcpServer.js.map +1 -1
  30. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  31. package/dist/mcp/builtin/collections/authTools.js +44 -6
  32. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  33. package/dist/mcp/builtin/collections/countTool.d.ts +2 -0
  34. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -0
  35. package/dist/mcp/builtin/collections/countTool.js +69 -0
  36. package/dist/mcp/builtin/collections/countTool.js.map +1 -0
  37. package/dist/mcp/builtin/collections/countVersionsTool.d.ts +2 -0
  38. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -0
  39. package/dist/mcp/builtin/collections/countVersionsTool.js +65 -0
  40. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -0
  41. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  42. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  43. package/dist/mcp/builtin/collections/createTool.js +38 -42
  44. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  45. package/dist/mcp/builtin/collections/deleteTool.d.ts +1 -1
  46. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  47. package/dist/mcp/builtin/collections/deleteTool.js +14 -20
  48. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  49. package/dist/mcp/builtin/collections/duplicateTool.d.ts +2 -0
  50. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -0
  51. package/dist/mcp/builtin/collections/duplicateTool.js +97 -0
  52. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -0
  53. package/dist/mcp/builtin/collections/findDistinctTool.d.ts +2 -0
  54. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -0
  55. package/dist/mcp/builtin/collections/findDistinctTool.js +93 -0
  56. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -0
  57. package/dist/mcp/builtin/collections/findTool.d.ts +1 -1
  58. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  59. package/dist/mcp/builtin/collections/findTool.js +49 -43
  60. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  61. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts +2 -0
  62. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -0
  63. package/dist/mcp/builtin/collections/findVersionByIDTool.js +84 -0
  64. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -0
  65. package/dist/mcp/builtin/collections/findVersionsTool.d.ts +2 -0
  66. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -0
  67. package/dist/mcp/builtin/collections/findVersionsTool.js +99 -0
  68. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -0
  69. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +9 -0
  70. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +1 -0
  71. package/dist/mcp/builtin/collections/formatCollectionError.js +60 -0
  72. package/dist/mcp/builtin/collections/formatCollectionError.js.map +1 -0
  73. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts +2 -0
  74. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -0
  75. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +47 -0
  76. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -0
  77. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts +2 -0
  78. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -0
  79. package/dist/mcp/builtin/collections/restoreVersionTool.js +82 -0
  80. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -0
  81. package/dist/mcp/builtin/collections/updateTool.d.ts +1 -1
  82. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  83. package/dist/mcp/builtin/collections/updateTool.js +87 -86
  84. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  85. package/dist/mcp/builtin/getConfigInfoTool.d.ts +2 -0
  86. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -0
  87. package/dist/mcp/builtin/getConfigInfoTool.js +71 -0
  88. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -0
  89. package/dist/mcp/builtin/globals/countVersionsTool.d.ts +2 -0
  90. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -0
  91. package/dist/mcp/builtin/globals/countVersionsTool.js +65 -0
  92. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -0
  93. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  94. package/dist/mcp/builtin/globals/findTool.js +18 -21
  95. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  96. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts +2 -0
  97. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -0
  98. package/dist/mcp/builtin/globals/findVersionByIDTool.js +80 -0
  99. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -0
  100. package/dist/mcp/builtin/globals/findVersionsTool.d.ts +2 -0
  101. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -0
  102. package/dist/mcp/builtin/globals/findVersionsTool.js +95 -0
  103. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -0
  104. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts +2 -0
  105. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -0
  106. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +47 -0
  107. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -0
  108. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts +2 -0
  109. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -0
  110. package/dist/mcp/builtin/globals/restoreVersionTool.js +78 -0
  111. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -0
  112. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  113. package/dist/mcp/builtin/globals/updateTool.js +31 -40
  114. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  115. package/dist/mcp/builtin/validateEntityData.d.ts +14 -0
  116. package/dist/mcp/builtin/validateEntityData.d.ts.map +1 -0
  117. package/dist/mcp/builtin/validateEntityData.js +82 -0
  118. package/dist/mcp/builtin/validateEntityData.js.map +1 -0
  119. package/dist/mcp/builtinTools.d.ts +144 -19
  120. package/dist/mcp/builtinTools.d.ts.map +1 -1
  121. package/dist/mcp/builtinTools.js +119 -20
  122. package/dist/mcp/builtinTools.js.map +1 -1
  123. package/dist/mcp/sanitizeMCPConfig.d.ts +0 -1
  124. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  125. package/dist/mcp/sanitizeMCPConfig.js +121 -58
  126. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  127. package/dist/stdio.d.ts +3 -3
  128. package/dist/stdio.d.ts.map +1 -1
  129. package/dist/stdio.js +82 -45
  130. package/dist/stdio.js.map +1 -1
  131. package/dist/types.d.ts +75 -110
  132. package/dist/types.d.ts.map +1 -1
  133. package/dist/types.js +4 -4
  134. package/dist/types.js.map +1 -1
  135. package/dist/utils/getPluginConfig.d.ts +1 -1
  136. package/dist/utils/getPluginConfig.js +1 -1
  137. package/dist/utils/getPluginConfig.js.map +1 -1
  138. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +11 -0
  139. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +1 -0
  140. package/dist/utils/schemaConversion/getEntityInputSchema.js +32 -0
  141. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +1 -0
  142. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +5 -10
  143. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +1 -1
  144. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +15 -95
  145. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +1 -1
  146. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +15 -40
  147. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +1 -1
  148. package/dist/utils/whereSchema.d.ts +9 -0
  149. package/dist/utils/whereSchema.d.ts.map +1 -0
  150. package/dist/utils/whereSchema.js +13 -0
  151. package/dist/utils/whereSchema.js.map +1 -0
  152. package/package.json +10 -15
  153. package/src/defaultAccess.ts +3 -0
  154. package/src/endpoint/access.ts +88 -101
  155. package/src/endpoint/index.ts +58 -16
  156. package/src/exports/client.ts +2 -1
  157. package/src/exports/internal.ts +1 -0
  158. package/src/index.ts +12 -20
  159. package/src/mcp/buildMcpServer.ts +170 -115
  160. package/src/mcp/builtin/collections/authTools.ts +45 -9
  161. package/src/mcp/builtin/collections/countTool.ts +74 -0
  162. package/src/mcp/builtin/collections/countVersionsTool.ts +73 -0
  163. package/src/mcp/builtin/collections/createTool.ts +55 -59
  164. package/src/mcp/builtin/collections/deleteTool.ts +19 -16
  165. package/src/mcp/builtin/collections/duplicateTool.ts +135 -0
  166. package/src/mcp/builtin/collections/findDistinctTool.ts +119 -0
  167. package/src/mcp/builtin/collections/findTool.ts +63 -33
  168. package/src/mcp/builtin/collections/findVersionByIDTool.ts +110 -0
  169. package/src/mcp/builtin/collections/findVersionsTool.ts +155 -0
  170. package/src/mcp/builtin/collections/formatCollectionError.ts +84 -0
  171. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +46 -0
  172. package/src/mcp/builtin/collections/restoreVersionTool.ts +108 -0
  173. package/src/mcp/builtin/collections/updateTool.ts +111 -110
  174. package/src/mcp/builtin/getConfigInfoTool.ts +69 -0
  175. package/src/mcp/builtin/globals/countVersionsTool.ts +72 -0
  176. package/src/mcp/builtin/globals/findTool.ts +26 -17
  177. package/src/mcp/builtin/globals/findVersionByIDTool.ts +104 -0
  178. package/src/mcp/builtin/globals/findVersionsTool.ts +148 -0
  179. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +41 -0
  180. package/src/mcp/builtin/globals/restoreVersionTool.ts +99 -0
  181. package/src/mcp/builtin/globals/updateTool.ts +50 -55
  182. package/src/mcp/builtin/validateEntityData.ts +132 -0
  183. package/src/mcp/builtinTools.ts +111 -41
  184. package/src/mcp/sanitizeMCPConfig.ts +117 -78
  185. package/src/stdio.ts +77 -45
  186. package/src/types.ts +92 -115
  187. package/src/utils/getPluginConfig.ts +1 -1
  188. package/src/utils/schemaConversion/getEntityInputSchema.ts +62 -0
  189. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +12 -19
  190. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +17 -114
  191. package/src/utils/whereSchema.ts +24 -0
  192. package/dist/@types/assets.d.js +0 -2
  193. package/dist/@types/assets.d.js.map +0 -1
  194. package/dist/collection/getAccessField.d.ts +0 -12
  195. package/dist/collection/getAccessField.d.ts.map +0 -1
  196. package/dist/collection/getAccessField.js +0 -57
  197. package/dist/collection/getAccessField.js.map +0 -1
  198. package/dist/collection/index.d.ts +0 -6
  199. package/dist/collection/index.d.ts.map +0 -1
  200. package/dist/collection/index.js +0 -60
  201. package/dist/collection/index.js.map +0 -1
  202. package/dist/components/AccessField/index.client.d.ts +0 -10
  203. package/dist/components/AccessField/index.client.d.ts.map +0 -1
  204. package/dist/components/AccessField/index.client.js +0 -305
  205. package/dist/components/AccessField/index.client.js.map +0 -1
  206. package/dist/components/AccessField/index.css +0 -93
  207. package/dist/utils/schemaConversion/buildToolInput.d.ts +0 -29
  208. package/dist/utils/schemaConversion/buildToolInput.d.ts.map +0 -1
  209. package/dist/utils/schemaConversion/buildToolInput.js +0 -51
  210. package/dist/utils/schemaConversion/buildToolInput.js.map +0 -1
  211. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts +0 -7
  212. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts.map +0 -1
  213. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js +0 -20
  214. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js.map +0 -1
  215. package/src/@types/assets.d.ts +0 -3
  216. package/src/collection/getAccessField.ts +0 -64
  217. package/src/collection/index.ts +0 -64
  218. package/src/components/AccessField/index.client.tsx +0 -344
  219. package/src/components/AccessField/index.css +0 -93
  220. package/src/utils/schemaConversion/buildToolInput.ts +0 -68
  221. package/src/utils/schemaConversion/removeVirtualFieldsFromSchema.ts +0 -27
package/src/stdio.ts CHANGED
@@ -1,35 +1,28 @@
1
- import type { MaybePromise, Plugin, SanitizedConfig } from 'payload'
1
+ import type { Config, Plugin, SanitizedConfig } from 'payload'
2
2
 
3
3
  /* eslint-disable no-console */
4
- import { StdioServerTransport } from '@modelcontextprotocol/server'
4
+ import { serveStdio } from '@modelcontextprotocol/server/stdio'
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url'
6
6
  import { createLocalReq, getPayload } from 'payload'
7
7
  import { findConfig } from 'payload/node'
8
8
 
9
- import type { AuthorizedMCP, SanitizedMCPPluginConfig } from './types.js'
9
+ import type { SanitizedMCPPluginConfig } from './types.js'
10
10
 
11
+ import { getAuthorizedMCP } from './endpoint/access.js'
11
12
  import { buildMcpServer } from './mcp/buildMcpServer.js'
12
13
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'
13
14
  import { getPluginConfig } from './utils/getPluginConfig.js'
14
15
  import { resolveProjectRoot } from './utils/resolveProjectRoot.js'
15
16
 
16
17
  /**
17
- * Stdio adapter for the Payload MCP server.
18
+ * Starts Payload's MCP server over stdin and stdout.
18
19
  *
19
- * Do not use in production. There's no auth; whoever can spawn the process gets
20
- * full access to your local data.
20
+ * Set `PAYLOAD_MCP_AUTHORIZATION` to authenticate. In development,
21
+ * `PAYLOAD_MCP_OVERRIDE_ACCESS=true` skips access checks.
21
22
  */
22
23
  export const runMcpStdio = async (): Promise<void> => {
23
- /**
24
- * If MCP clients spawn stdio servers from an arbitrary working directory,
25
- * Payload's cwd-anchored `findConfig()` can't locate the project on
26
- * its own. This module always lives inside the project's `node_modules`, so
27
- * we derive the project root from its own path and `chdir` into it.
28
- *
29
- * An absolute `PAYLOAD_CONFIG_PATH` still overrides everything; unusual
30
- * layouts (e.g. a monorepo where the package is hoisted above the app) can
31
- * fall back to it. See the implementation of findConfig for details.
32
- */
24
+ // MCP clients may start this command from another folder. Move to the Payload
25
+ // project so findConfig() can find the config.
33
26
  const projectRoot = resolveProjectRoot(fileURLToPath(import.meta.url))
34
27
  if (projectRoot) {
35
28
  process.chdir(projectRoot)
@@ -37,62 +30,101 @@ export const runMcpStdio = async (): Promise<void> => {
37
30
 
38
31
  const configPath = findConfig()
39
32
  const configModule = await import(pathToFileURL(configPath).toString())
40
- const config = (await (configModule.default ?? configModule)) as MaybePromise<SanitizedConfig>
41
-
42
- const payload = await getPayload({ config })
33
+ const config = (await (configModule.default ?? configModule)) as SanitizedConfig
43
34
 
44
35
  /**
45
- * If the user added `mcpPlugin({...})` to their `plugins` array, read the
46
- * sanitized config they registered. Otherwise fall back to the defaults. That way,
47
- * the mcp works on any project that has `@payloadcms/plugin-mcp` installed,
48
- * even if the plugin is not added to the plugins array.
36
+ * stdout is only for MCP messages. The spec says the server must not write
37
+ * logs there and may write them to stderr instead.
38
+ *
39
+ * The 2.0 client library recovers from stray stdout logs, but that behavior is not
40
+ * guaranteed. Move Payload logs to stderr so Payload does not add invalid data
41
+ * to stdout. Keep options from configurable loggers. Replace pre-built loggers
42
+ * because their output cannot be redirected.
43
+ *
44
+ * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio
49
45
  */
46
+ const loggerConfig: { logger?: Config['logger'] } = config
47
+ if (
48
+ loggerConfig.logger &&
49
+ typeof loggerConfig.logger === 'object' &&
50
+ 'options' in loggerConfig.logger
51
+ ) {
52
+ loggerConfig.logger.destination = process.stderr
53
+ } else {
54
+ loggerConfig.logger = { destination: process.stderr, options: {} }
55
+ }
56
+
57
+ const payload = await getPayload({ config, cron: false, disableOnInit: true })
58
+
50
59
  let pluginConfig: SanitizedMCPPluginConfig
51
60
  try {
52
61
  pluginConfig = getPluginConfig({ config: payload.config })
53
62
  } catch {
63
+ // The command also works when mcpPlugin() is not in the Payload config.
54
64
  pluginConfig = sanitizeMCPConfig({ config: payload.config, pluginConfig: {} })
55
65
 
56
- const fakePluginFn: Plugin = (config) => config
57
- fakePluginFn.slug = '@payloadcms/plugin-mcp'
58
- // @ts-expect-error
59
- fakePluginFn.sanitizedOptions = pluginConfig
66
+ const fallbackPlugin: Plugin = (config) => config
67
+ Object.assign(fallbackPlugin, {
68
+ slug: '@payloadcms/plugin-mcp',
69
+ sanitizedOptions: pluginConfig,
70
+ })
71
+ payload.config.plugins ??= []
72
+ payload.config.plugins.push(fallbackPlugin)
73
+ }
74
+
75
+ const overrideAccessEnv = process.env.PAYLOAD_MCP_OVERRIDE_ACCESS
76
+ let overrideAccess = false
77
+
78
+ if (overrideAccessEnv === 'true') {
79
+ overrideAccess = true
80
+ } else if (overrideAccessEnv && overrideAccessEnv !== 'false') {
81
+ throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS must be "true" or "false".')
82
+ }
60
83
 
61
- // Push to payload config, to ensure consecutive calls to `getPluginConfig()` work
62
- ;(payload.config.plugins ??= []).push(fakePluginFn)
84
+ if (overrideAccess && process.env.NODE_ENV !== 'development') {
85
+ throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS is only available in development.')
63
86
  }
64
87
 
65
- const authorizedMCP: AuthorizedMCP = {
66
- items: pluginConfig.items,
67
- overrideAccess: true,
68
- user: null,
88
+ const headers = new Headers()
89
+ if (process.env.PAYLOAD_MCP_AUTHORIZATION) {
90
+ headers.set('Authorization', process.env.PAYLOAD_MCP_AUTHORIZATION)
69
91
  }
70
92
 
71
- const req = await createLocalReq({}, payload)
93
+ const req = await createLocalReq({ req: { headers } }, payload)
72
94
  req.payloadAPI = 'MCP' as const
95
+ const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })
73
96
 
74
- const server = buildMcpServer({ authorizedMCP, pluginConfig, req })
97
+ const stdioServer = serveStdio(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {
98
+ onerror: (err) => {
99
+ // MCP messages use stdout, so write SDK errors to stderr.
100
+ console.error('[payload-mcp] error serving MCP over stdio:', err)
101
+ },
102
+ })
75
103
 
76
- const transport = new StdioServerTransport()
77
- await server.connect(transport)
104
+ // Close the MCP server and Payload when the client disconnects or the process stops.
105
+ let isShuttingDown = false
106
+ const shutdown = async () => {
107
+ if (isShuttingDown) {
108
+ return
109
+ }
110
+ isShuttingDown = true
78
111
 
79
- // Process now lives until stdin closes. Wire graceful shutdown so DB
80
- // connections (and any other Payload-managed resources) get released.
81
- const shutdown = async (code = 0) => {
82
112
  try {
83
- await server.close()
113
+ await stdioServer.close()
84
114
  } catch (err) {
85
115
  console.error('[payload-mcp] error closing server:', err)
86
116
  }
117
+
87
118
  try {
88
119
  await payload.destroy()
89
120
  } catch (err) {
90
121
  console.error('[payload-mcp] error destroying payload:', err)
91
122
  }
92
- process.exit(code)
123
+
124
+ process.exit(0)
93
125
  }
94
126
 
95
- process.on('SIGINT', () => void shutdown(0))
96
- process.on('SIGTERM', () => void shutdown(0))
97
- process.stdin.on('close', () => void shutdown(0))
127
+ process.once('SIGINT', () => void shutdown())
128
+ process.once('SIGTERM', () => void shutdown())
129
+ process.stdin.once('close', () => void shutdown())
98
130
  }
package/src/types.ts CHANGED
@@ -6,18 +6,19 @@ import type {
6
6
  ResourceTemplate,
7
7
  ServerContext,
8
8
  StandardSchemaWithJSON,
9
+ ToolAnnotations,
9
10
  } from '@modelcontextprotocol/server'
10
11
  import type {
11
12
  AuthCollectionSlug,
12
- CollectionConfig,
13
13
  CollectionSlug,
14
- DefaultDocumentIDType,
15
14
  GlobalSlug,
16
15
  MaybePromise,
17
16
  PayloadRequest,
18
- TypedUser,
17
+ SanitizedPermissions,
18
+ User,
19
19
  } from 'payload'
20
20
 
21
+ import type { GetAuthorizedMCPArgs } from './endpoint/access.js'
21
22
  import type {
22
23
  MCPCollectionAuthToolName,
23
24
  MCPCollectionBuiltinName,
@@ -26,34 +27,16 @@ import type {
26
27
 
27
28
  export type { MCPCollectionAuthToolName, MCPCollectionBuiltinName, MCPGlobalBuiltinName }
28
29
 
29
- /** Re-exported from `@modelcontextprotocol/server` — the JSON Schema shape the MCP runtime validates against. */
30
- export type { JsonSchemaType, StandardSchemaWithJSON }
30
+ /** Re-exported from `@modelcontextprotocol/server` — common MCP types used in plugin config. */
31
+ export type { JsonSchemaType, StandardSchemaWithJSON, ToolAnnotations }
31
32
 
32
33
  /**
33
34
  * What a tool's `input` (or a prompt's `argsSchema`) can be — either a raw
34
- * JSON Schema literal, or a Standard Schema instance (Zod, Valibot, …).
35
+ * JSON Schema 2020-12 literal, or a Standard Schema instance (Zod, Valibot, …).
36
+ * Raw schemas may omit `$schema`; when present it must declare the 2020-12 dialect.
35
37
  */
36
38
  export type ToolInputSchema = JsonSchemaType | StandardSchemaWithJSON
37
39
 
38
- /**
39
- * Serializable mirror of `SanitizedMCPPluginConfig` for client components —
40
- * the full sanitized config carries functions (tool handlers, etc.) that can't
41
- * cross the server→client boundary. Built by `sanitizeClientPluginConfig` and
42
- * passed to the `AccessField` component via `clientProps`.
43
- *
44
- * @internal
45
- */
46
- export type ClientMCPPluginConfig = {
47
- items: Array<{
48
- collectionSlug?: string
49
- description: string
50
- globalSlug?: string
51
- key: string
52
- label: string
53
- type: 'collectionTool' | 'globalTool' | 'prompt' | 'resource' | 'tool'
54
- }>
55
- }
56
-
57
40
  export type MCPToolResponse = {
58
41
  content: ContentBlock[]
59
42
  /**
@@ -70,6 +53,13 @@ export type MCPResponseOverride = (
70
53
  req: PayloadRequest,
71
54
  ) => MCPToolResponse
72
55
 
56
+ export type MCPAfterToolCallHook = (args: {
57
+ input: unknown
58
+ req: PayloadRequest
59
+ response: MCPToolResponse
60
+ toolName: string
61
+ }) => MaybePromise<MCPToolResponse>
62
+
73
63
  /**
74
64
  * The handler's `input` type. A specific Standard Schema (Zod, Valibot, …) gets
75
65
  * its inferred output; anything else falls back to `Record<string, unknown>`.
@@ -80,6 +70,19 @@ export type TypedInput<TSchema> = TSchema extends StandardSchemaWithJSON
80
70
  : StandardSchemaWithJSON.InferOutput<TSchema>
81
71
  : Record<string, unknown>
82
72
 
73
+ export type MCPAccessArgs = {
74
+ permissions?: SanitizedPermissions
75
+ req: PayloadRequest
76
+ }
77
+
78
+ export type CollectionMCPAccessArgs = {
79
+ collectionSlug: CollectionSlug
80
+ } & MCPAccessArgs
81
+
82
+ export type GlobalMCPAccessArgs = {
83
+ globalSlug: GlobalSlug
84
+ } & MCPAccessArgs
85
+
83
86
  export type ToolHandlerArgs<TSchema = undefined> = {
84
87
  authorizedMCP: AuthorizedMCP
85
88
  input: TypedInput<TSchema>
@@ -96,6 +99,8 @@ export type GlobalToolHandlerArgs<TSchema = undefined> = {
96
99
  } & ToolHandlerArgs<TSchema>
97
100
 
98
101
  export type Tool<TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined> = {
102
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
103
+ annotations?: ToolAnnotations
99
104
  description: string
100
105
  handler: (args: ToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
101
106
  input?: TSchema
@@ -105,30 +110,37 @@ export type Tool<TSchema extends ToolInputSchema | undefined = ToolInputSchema |
105
110
  overrideResponse?: MCPResponseOverride
106
111
  }
107
112
 
108
- /**
109
- * `TSchema` is the schema itself (Standard Schema, raw JSON Schema, or undefined).
110
- * The function-form variant of `input` carries a concrete `{ collectionSchema: JsonSchemaType }`
111
- * parameter type so callers can write `({ collectionSchema }) => …` without annotating it.
112
- */
113
113
  export type CollectionTool<
114
114
  TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined,
115
115
  > = {
116
+ access?: (args: CollectionMCPAccessArgs) => MaybePromise<boolean>
116
117
  handler: (args: CollectionToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
117
- input?: ((args: { collectionSchema: JsonSchemaType }) => TSchema) | TSchema
118
- } & Pick<Tool, 'description' | 'overrideResponse'>
118
+ input?: TSchema
119
+ } & Pick<Tool, 'annotations' | 'description' | 'overrideResponse'>
119
120
 
120
121
  export type GlobalTool<TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined> =
121
122
  {
123
+ access?: (args: GlobalMCPAccessArgs) => MaybePromise<boolean>
122
124
  handler: (args: GlobalToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
123
- input?: ((args: { globalSchema: JsonSchemaType }) => TSchema) | TSchema
124
- } & Pick<Tool, 'description' | 'overrideResponse'>
125
+ input?: TSchema
126
+ } & Pick<Tool, 'annotations' | 'description' | 'overrideResponse'>
125
127
 
126
128
  /**
127
129
  * Configures (or disables) a built-in tool without replacing it.
128
130
  * `handler?: never` prevents a full `CollectionTool`/`GlobalTool` (which has a
129
131
  * required handler) from being silently accepted at a built-in key slot.
130
132
  */
131
- export type MCPBuiltInToolOverride = {
133
+ export type MCPBuiltInCollectionToolOverride = {
134
+ access?: (args: CollectionMCPAccessArgs) => MaybePromise<boolean>
135
+ annotations?: ToolAnnotations
136
+ description?: string
137
+ handler?: never
138
+ overrideResponse?: MCPResponseOverride
139
+ }
140
+
141
+ export type MCPBuiltInGlobalToolOverride = {
142
+ access?: (args: GlobalMCPAccessArgs) => MaybePromise<boolean>
143
+ annotations?: ToolAnnotations
132
144
  description?: string
133
145
  handler?: never
134
146
  overrideResponse?: MCPResponseOverride
@@ -142,13 +154,13 @@ export type MCPBuiltInToolOverride = {
142
154
  export type MCPTopLevelToolEntry = Tool
143
155
 
144
156
  export type MCPCollectionToolsMap = {
145
- [customToolName: string]: boolean | CollectionTool | MCPBuiltInToolOverride | undefined
157
+ [customToolName: string]: boolean | CollectionTool | MCPBuiltInCollectionToolOverride | undefined
146
158
  } & {
147
- [K in MCPCollectionBuiltinName]?: false | MCPBuiltInToolOverride
159
+ [K in MCPCollectionBuiltinName]?: false | MCPBuiltInCollectionToolOverride
148
160
  }
149
161
 
150
162
  export type MCPAuthCollectionToolsMap = {
151
- [K in MCPCollectionAuthToolName]?: MCPBuiltInToolOverride | true
163
+ [K in MCPCollectionAuthToolName]?: MCPBuiltInCollectionToolOverride | true
152
164
  } & MCPCollectionToolsMap
153
165
 
154
166
  /** Auth-enabled collections get auth-tool name autocomplete; others get CRUD-only. */
@@ -157,9 +169,9 @@ export type MCPToolsMapForCollection<Slug extends CollectionSlug> = Slug extends
157
169
  : MCPCollectionToolsMap
158
170
 
159
171
  export type MCPGlobalToolsMap = {
160
- [customToolName: string]: boolean | GlobalTool | MCPBuiltInToolOverride | undefined
172
+ [customToolName: string]: boolean | GlobalTool | MCPBuiltInGlobalToolOverride | undefined
161
173
  } & {
162
- [K in MCPGlobalBuiltinName]?: false | MCPBuiltInToolOverride
174
+ [K in MCPGlobalBuiltinName]?: false | MCPBuiltInGlobalToolOverride
163
175
  }
164
176
 
165
177
  export type MCPTopLevelToolsMap = Record<string, Tool>
@@ -171,6 +183,7 @@ export type PromptHandlerArgs<TSchema = undefined> = {
171
183
  }
172
184
 
173
185
  export type Prompt<TSchema extends ToolInputSchema = ToolInputSchema> = {
186
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
174
187
  argsSchema: TSchema
175
188
  description: string
176
189
  handler: (args: PromptHandlerArgs<TSchema>) => MaybePromise<{
@@ -188,6 +201,7 @@ export type ResourceHandlerArgs = {
188
201
  }
189
202
 
190
203
  export type Resource = {
204
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
191
205
  description: string
192
206
  handler: (args: ResourceHandlerArgs) => MaybePromise<{
193
207
  contents: Array<{ text: string; uri: string }>
@@ -221,34 +235,34 @@ export type MCPPluginConfig = {
221
235
  collections?: {
222
236
  [Slug in CollectionSlug]?: MCPPluginCollectionConfig<Slug>
223
237
  }
224
- /** Skip MCP registration. The API key collection is still added (so DB / types stay stable). */
238
+ /** Skip MCP endpoint registration. */
225
239
  disabled?: boolean
226
240
  globals?: {
227
241
  [Slug in GlobalSlug]?: MCPPluginGlobalConfig
228
242
  }
243
+ hooks?: {
244
+ /** Transform a tool response after its handler returns */
245
+ afterToolCall?: MCPAfterToolCallHook[]
246
+ }
229
247
  mcp?: {
230
248
  serverOptions?: MCPServerOptions
231
249
  verboseLogs?: boolean
232
250
  }
233
- overrideApiKeyCollection?: (collection: CollectionConfig) => CollectionConfig
234
- /** Replace the default API-key auth with a custom resolver. */
235
- overrideAuth?: (args: {
236
- getAPIKeyDoc: (overrideApiKey?: string) => Promise<MCPAPIKeysDoc>
237
- getAuthorizedMCP: (args: { apiKeyDoc: MCPAPIKeysDoc }) => AuthorizedMCP
238
- pluginConfig: SanitizedMCPPluginConfig
239
- req: PayloadRequest
240
- }) => MaybePromise<AuthorizedMCP>
251
+ /** Replace the default MCP authorization resolver. */
252
+ overrideGetAuthorizedMCP?: (
253
+ args: {
254
+ pluginConfig: SanitizedMCPPluginConfig
255
+ } & GetAuthorizedMCPArgs,
256
+ ) => MaybePromise<AuthorizedMCP>
241
257
  prompts?: Record<string, Prompt>
242
258
  resources?: Record<string, Resource>
243
259
  /** Cross-cutting tools (not scoped to any collection or global). */
244
260
  tools?: MCPTopLevelToolsMap
245
- userCollection?: CollectionSlug
246
261
  }
247
262
 
248
263
  export type SanitizedMCPPluginConfig = {
249
264
  items: MCPItem[]
250
- userCollection: CollectionSlug
251
- } & Pick<MCPPluginConfig, 'disabled' | 'mcp' | 'overrideApiKeyCollection' | 'overrideAuth'>
265
+ } & Pick<MCPPluginConfig, 'disabled' | 'hooks' | 'mcp' | 'overrideGetAuthorizedMCP'>
252
266
 
253
267
  export type MCPServerOptions = {
254
268
  options?: ConstructorParameters<typeof McpServer>[1]
@@ -256,71 +270,32 @@ export type MCPServerOptions = {
256
270
  }
257
271
 
258
272
  /**
259
- * Nested access tree as stored in the collection.
260
- * A `false` leaf disables that tool; missing keys defer to
261
- * defaults (built-in CRUD is on, opt-in tools are off).
262
- */
263
- export type MCPAPIKeysDocAccessTree = {
264
- collections?: {
265
- [CollectionSlug: CollectionSlug]: {
266
- [ToolKey: string]: boolean
267
- }
268
- }
269
- globals?: {
270
- [GlobalSlug: GlobalSlug]: {
271
- [ToolKey: string]: boolean
272
- }
273
- }
274
- prompts?: {
275
- [PromptKey: string]: boolean
276
- }
277
- resources?: {
278
- [ResourceKey: string]: boolean
279
- }
280
- tools?: {
281
- [ToolKey: string]: boolean
282
- }
283
- }
284
-
285
- /**
286
- * Stored on `payload-mcp-api-keys` docs
287
- */
288
- export type MCPAPIKeysDoc = {
289
- access: MCPAPIKeysDocAccessTree
290
- id: DefaultDocumentIDType
291
- overrideAccess?: boolean
292
- user: null | TypedUser
293
- }
294
-
295
- /**
296
- * One MCP primitive — tool, prompt, or resource — paired with the metadata both
297
- * the endpoint and the API key collection need. Built by `sanitizeMCPConfig`,
298
- * filtered by `getAuthorizedMCP`, registered by the MCP endpoint.
273
+ * One MCP primitive plus the metadata needed for config-driven filtering and
274
+ * registration.
299
275
  *
300
- * - `key`: the config identifier (`find`, `echo`). Used for the API-key deny
301
- * lookup and as the admin checkbox field name. For collection/global tools,
302
- * the MCP wire name (`findPosts`) is derived from `key + slug` at
303
- * registration time.
304
- * - `label`: human-readable admin-UI display text for the checkbox.
305
- * - `tool` / `prompt` / `resource`: the live primitive (its own
306
- * `description` is what both MCP clients and the admin UI surface).
276
+ * - `configKey`: the config identifier, e.g. `find` or `echo`.
277
+ * - `mcpName`: the MCP wire name, e.g. `findDocuments` or `echo`.
278
+ * - `label`: human-readable display text.
307
279
  */
308
280
  export type MCPItemBase = {
309
- key: string
281
+ configKey: string
310
282
  label: string
283
+ mcpName: string
311
284
  }
312
285
 
286
+ export type CollectionMCPItem = {
287
+ collectionSlug: CollectionSlug
288
+ tool: CollectionTool
289
+ type: 'collectionTool'
290
+ } & MCPItemBase
291
+
292
+ export type GlobalMCPItem = {
293
+ globalSlug: GlobalSlug
294
+ tool: GlobalTool
295
+ type: 'globalTool'
296
+ } & MCPItemBase
297
+
313
298
  export type MCPItem =
314
- | ({
315
- collectionSlug: CollectionSlug
316
- tool: CollectionTool
317
- type: 'collectionTool'
318
- } & MCPItemBase)
319
- | ({
320
- globalSlug: GlobalSlug
321
- tool: GlobalTool
322
- type: 'globalTool'
323
- } & MCPItemBase)
324
299
  | ({
325
300
  prompt: Prompt
326
301
  type: 'prompt'
@@ -333,15 +308,17 @@ export type MCPItem =
333
308
  tool: Tool
334
309
  type: 'tool'
335
310
  } & MCPItemBase)
311
+ | CollectionMCPItem
312
+ | GlobalMCPItem
336
313
 
337
314
  /**
338
- * The caller's identity + the MCP items they can use for this request. Returned
339
- * by `getAuthorizedMCP`; denied items are simply absent from `items`. Handlers
340
- * receive this via `args.authorizedMCP` so they can spread
341
- * `localAPIDefaults(authorizedMCP)` into every local API call.
315
+ * The caller's identity + the MCP items authorized for this request. Disabled
316
+ * items and items blocked by access callbacks or Payload operation access are
317
+ * absent from `items`. Tool handlers receive this via `args.authorizedMCP` so
318
+ * they can spread `localAPIDefaults(authorizedMCP)` into every local API call.
342
319
  */
343
320
  export type AuthorizedMCP = {
344
321
  items: MCPItem[]
345
322
  overrideAccess: boolean
346
- user: null | TypedUser
323
+ user: null | User
347
324
  }
@@ -5,7 +5,7 @@ import type { SanitizedMCPPluginConfig } from '../types.js'
5
5
  /**
6
6
  * Returns the sanitized plugin config — the form produced by
7
7
  * `sanitizeMCPConfig` and stashed back on the plugin's `options` during init.
8
- * Callers downstream of plugin init can rely on `items`, `userCollection`, etc.
8
+ * Callers downstream of plugin init can rely on `items` and other defaults
9
9
  * being fully resolved.
10
10
  */
11
11
  export const getPluginConfig: (args: { config: SanitizedConfig }) => SanitizedMCPPluginConfig = ({
@@ -0,0 +1,62 @@
1
+ import {
2
+ type CollectionSlug,
3
+ entityToStandaloneJSONSchema,
4
+ type GlobalSlug,
5
+ type PayloadRequest,
6
+ type SanitizedCollectionConfig,
7
+ type SanitizedGlobalConfig,
8
+ } from 'payload'
9
+
10
+ import type { JsonSchemaType } from '../../types.js'
11
+
12
+ import { sanitizeEntitySchema } from './sanitizeEntitySchema.js'
13
+
14
+ export const getCollectionInputSchema = ({
15
+ collectionSlug,
16
+ req,
17
+ }: {
18
+ collectionSlug: CollectionSlug
19
+ req: PayloadRequest
20
+ }): JsonSchemaType | null => {
21
+ const collection = req.payload.collections[collectionSlug]?.config
22
+
23
+ if (!collection) {
24
+ return null
25
+ }
26
+
27
+ return buildEntityInputSchema({ entity: collection, req })
28
+ }
29
+
30
+ export const getGlobalInputSchema = ({
31
+ globalSlug,
32
+ req,
33
+ }: {
34
+ globalSlug: GlobalSlug
35
+ req: PayloadRequest
36
+ }): JsonSchemaType | null => {
37
+ const global = req.payload.config.globals.find((globalConfig) => globalConfig.slug === globalSlug)
38
+
39
+ if (!global) {
40
+ return null
41
+ }
42
+
43
+ return buildEntityInputSchema({ entity: global, req })
44
+ }
45
+
46
+ const buildEntityInputSchema = ({
47
+ entity,
48
+ req,
49
+ }: {
50
+ entity: SanitizedCollectionConfig | SanitizedGlobalConfig
51
+ req: PayloadRequest
52
+ }): JsonSchemaType =>
53
+ // The `input` variant is already the write shape; sanitizeEntitySchema only adds MCP-specific passes.
54
+ sanitizeEntitySchema(
55
+ entityToStandaloneJSONSchema({
56
+ config: req.payload.config,
57
+ defaultIDType: req.payload.db.defaultIDType,
58
+ entity,
59
+ i18n: req.i18n,
60
+ variant: 'input',
61
+ }) as unknown as JsonSchemaType,
62
+ )
@@ -5,10 +5,11 @@ import type { JsonSchemaType } from '../../types.js'
5
5
  import { sanitizeEntitySchema } from './sanitizeEntitySchema.js'
6
6
 
7
7
  describe('sanitizeEntitySchema', () => {
8
- it('keeps a Lexical node union strict (oneOf) while simplifying relationship values to IDs', () => {
9
- // Shaped like the schema the MCP server prepares for a collection with a Lexical richText field:
10
- // a `$defs` node union (a `oneOf` of node shapes) where a relationship node's `value` is either
11
- // an ID or a `$ref` to the populated related collection.
8
+ it('renames the Lexical node-union definition to a short name and keeps it a strict oneOf', () => {
9
+ // Shaped like the schema the MCP server prepares for a collection with a Lexical richText field
10
+ // (Payload's `input` variant): a `$defs` node union (a `oneOf` of node shapes). The input variant
11
+ // already types the relationship node's `value` as a bare ID - there is no populated-doc `$ref` to
12
+ // reduce here (the variant did that upstream), so sanitize leaves the value untouched.
12
13
  const standalone: JsonSchemaType = {
13
14
  type: 'object',
14
15
  $defs: {
@@ -25,17 +26,12 @@ describe('sanitizeEntitySchema', () => {
25
26
  additionalProperties: false,
26
27
  properties: {
27
28
  type: { const: 'relationship' },
28
- value: { oneOf: [{ type: 'string' }, { $ref: '#/$defs/posts' }] },
29
+ value: { type: 'string', description: 'The related document ID.' },
29
30
  },
30
31
  required: ['type'],
31
32
  },
32
33
  ],
33
34
  },
34
- posts: {
35
- type: 'object',
36
- additionalProperties: false,
37
- properties: { id: { type: 'string' }, title: { type: 'string' } },
38
- },
39
35
  },
40
36
  properties: {
41
37
  id: { type: 'string' },
@@ -57,7 +53,7 @@ describe('sanitizeEntitySchema', () => {
57
53
  type: 'object',
58
54
  $defs: {
59
55
  // The node union is renamed to a short, readable `node`, and stays a strict discriminated
60
- // `oneOf` - not loosened to `anyOf`...
56
+ // `oneOf` - not loosened to `anyOf`.
61
57
  node: {
62
58
  oneOf: [
63
59
  {
@@ -71,21 +67,18 @@ describe('sanitizeEntitySchema', () => {
71
67
  additionalProperties: false,
72
68
  properties: {
73
69
  type: { const: 'relationship' },
74
- // ...while the relationship `value` is simplified to a bare ID (the populated-doc `$ref` is dropped).
75
- value: { type: 'string', description: 'The ID of the related "posts" document.' },
70
+ // The value is already ID-only from the input variant - left as-is.
71
+ value: { type: 'string', description: 'The related document ID.' },
76
72
  },
77
73
  required: ['type'],
78
74
  },
79
75
  ],
80
76
  },
81
- posts: {
82
- type: 'object',
83
- additionalProperties: false,
84
- properties: { id: { type: 'string' }, title: { type: 'string' } },
85
- },
86
77
  },
87
78
  properties: {
88
- // `id` is dropped - it's a Payload-managed field MCP clients never set.
79
+ // Managed fields (createdAt/updatedAt/_status) are excluded upstream by the `input` variant;
80
+ // `id` stays because it's a valid optional input (a client may supply a custom ID).
81
+ id: { type: 'string' },
89
82
  content: {
90
83
  type: 'object',
91
84
  properties: {