@payloadcms/plugin-mcp 4.0.0-canary.1 → 4.0.0-canary.10

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 (222) hide show
  1. package/LICENSE.md +1 -1
  2. package/bin.js +7 -0
  3. package/dist/defaultAccess.d.ts +3 -0
  4. package/dist/defaultAccess.d.ts.map +1 -0
  5. package/dist/defaultAccess.js +3 -0
  6. package/dist/defaultAccess.js.map +1 -0
  7. package/dist/defineTool.d.ts +8 -8
  8. package/dist/defineTool.d.ts.map +1 -1
  9. package/dist/endpoint/access.d.ts +12 -5
  10. package/dist/endpoint/access.d.ts.map +1 -1
  11. package/dist/endpoint/access.js +75 -88
  12. package/dist/endpoint/access.js.map +1 -1
  13. package/dist/endpoint/index.d.ts +1 -1
  14. package/dist/endpoint/index.d.ts.map +1 -1
  15. package/dist/endpoint/index.js +68 -17
  16. package/dist/endpoint/index.js.map +1 -1
  17. package/dist/exports/client.d.ts +1 -1
  18. package/dist/exports/client.d.ts.map +1 -1
  19. package/dist/exports/client.js +1 -1
  20. package/dist/exports/client.js.map +1 -1
  21. package/dist/exports/internal.d.ts +2 -0
  22. package/dist/exports/internal.d.ts.map +1 -0
  23. package/dist/exports/internal.js +3 -0
  24. package/dist/exports/internal.js.map +1 -0
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +15 -20
  28. package/dist/index.js.map +1 -1
  29. package/dist/mcp/buildMcpServer.d.ts +4 -4
  30. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  31. package/dist/mcp/buildMcpServer.js +34 -10
  32. package/dist/mcp/buildMcpServer.js.map +1 -1
  33. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  34. package/dist/mcp/builtin/collections/authTools.js +44 -6
  35. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  36. package/dist/mcp/builtin/collections/countTool.d.ts +2 -0
  37. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -0
  38. package/dist/mcp/builtin/collections/countTool.js +68 -0
  39. package/dist/mcp/builtin/collections/countTool.js.map +1 -0
  40. package/dist/mcp/builtin/collections/countVersionsTool.d.ts +2 -0
  41. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -0
  42. package/dist/mcp/builtin/collections/countVersionsTool.js +64 -0
  43. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -0
  44. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  45. package/dist/mcp/builtin/collections/createTool.js +26 -24
  46. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  47. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  48. package/dist/mcp/builtin/collections/deleteTool.js +10 -2
  49. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  50. package/dist/mcp/builtin/collections/duplicateTool.d.ts +2 -0
  51. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -0
  52. package/dist/mcp/builtin/collections/duplicateTool.js +96 -0
  53. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -0
  54. package/dist/mcp/builtin/collections/fileInput.d.ts +20 -0
  55. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -0
  56. package/dist/mcp/builtin/collections/fileInput.js +100 -0
  57. package/dist/mcp/builtin/collections/fileInput.js.map +1 -0
  58. package/dist/mcp/builtin/collections/fileInput.spec.js +72 -0
  59. package/dist/mcp/builtin/collections/fileInput.spec.js.map +1 -0
  60. package/dist/mcp/builtin/collections/findDistinctTool.d.ts +2 -0
  61. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -0
  62. package/dist/mcp/builtin/collections/findDistinctTool.js +92 -0
  63. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -0
  64. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  65. package/dist/mcp/builtin/collections/findTool.js +46 -26
  66. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  67. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts +2 -0
  68. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -0
  69. package/dist/mcp/builtin/collections/findVersionByIDTool.js +83 -0
  70. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -0
  71. package/dist/mcp/builtin/collections/findVersionsTool.d.ts +2 -0
  72. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -0
  73. package/dist/mcp/builtin/collections/findVersionsTool.js +98 -0
  74. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -0
  75. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +1 -1
  76. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +1 -1
  77. package/dist/mcp/builtin/collections/formatCollectionError.js.map +1 -1
  78. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  79. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +55 -4
  80. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  81. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts +2 -0
  82. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -0
  83. package/dist/mcp/builtin/collections/restoreVersionTool.js +81 -0
  84. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -0
  85. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  86. package/dist/mcp/builtin/collections/updateTool.js +37 -32
  87. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  88. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -1
  89. package/dist/mcp/builtin/getConfigInfoTool.js +27 -8
  90. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -1
  91. package/dist/mcp/builtin/globals/countVersionsTool.d.ts +2 -0
  92. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -0
  93. package/dist/mcp/builtin/globals/countVersionsTool.js +64 -0
  94. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -0
  95. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  96. package/dist/mcp/builtin/globals/findTool.js +19 -22
  97. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  98. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts +2 -0
  99. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -0
  100. package/dist/mcp/builtin/globals/findVersionByIDTool.js +79 -0
  101. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -0
  102. package/dist/mcp/builtin/globals/findVersionsTool.d.ts +2 -0
  103. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -0
  104. package/dist/mcp/builtin/globals/findVersionsTool.js +94 -0
  105. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -0
  106. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  107. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +32 -2
  108. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  109. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts +2 -0
  110. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -0
  111. package/dist/mcp/builtin/globals/restoreVersionTool.js +77 -0
  112. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -0
  113. package/dist/mcp/builtin/globals/updateTool.js +14 -21
  114. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  115. package/dist/mcp/builtin/validateEntityData.d.ts.map +1 -1
  116. package/dist/mcp/builtin/validateEntityData.js +1 -1
  117. package/dist/mcp/builtin/validateEntityData.js.map +1 -1
  118. package/dist/mcp/builtinTools.d.ts +69 -12
  119. package/dist/mcp/builtinTools.d.ts.map +1 -1
  120. package/dist/mcp/builtinTools.js +66 -10
  121. package/dist/mcp/builtinTools.js.map +1 -1
  122. package/dist/mcp/sanitizeMCPConfig.d.ts +0 -1
  123. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  124. package/dist/mcp/sanitizeMCPConfig.js +69 -27
  125. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  126. package/dist/stdio.d.ts +3 -3
  127. package/dist/stdio.d.ts.map +1 -1
  128. package/dist/stdio.js +82 -45
  129. package/dist/stdio.js.map +1 -1
  130. package/dist/types.d.ts +100 -87
  131. package/dist/types.d.ts.map +1 -1
  132. package/dist/types.js +2 -4
  133. package/dist/types.js.map +1 -1
  134. package/dist/utils/camelCase.d.ts.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/resolveProjectRoot.d.ts.map +1 -1
  139. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts +28 -0
  140. package/dist/utils/schemaConversion/filterFieldsByAccess.d.ts.map +1 -0
  141. package/dist/utils/schemaConversion/filterFieldsByAccess.js +71 -0
  142. package/dist/utils/schemaConversion/filterFieldsByAccess.js.map +1 -0
  143. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +5 -3
  144. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +1 -1
  145. package/dist/utils/schemaConversion/getEntityInputSchema.js +44 -12
  146. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +1 -1
  147. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +5 -10
  148. package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +1 -1
  149. package/dist/utils/schemaConversion/sanitizeEntitySchema.js +15 -95
  150. package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +1 -1
  151. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +15 -40
  152. package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +1 -1
  153. package/dist/utils/toStandardSchema.d.ts.map +1 -1
  154. package/package.json +10 -15
  155. package/src/defaultAccess.ts +3 -0
  156. package/src/endpoint/access.ts +81 -101
  157. package/src/endpoint/index.ts +58 -16
  158. package/src/exports/client.ts +2 -1
  159. package/src/exports/internal.ts +1 -0
  160. package/src/index.ts +12 -20
  161. package/src/mcp/buildMcpServer.ts +48 -21
  162. package/src/mcp/builtin/collections/authTools.ts +45 -9
  163. package/src/mcp/builtin/collections/countTool.ts +73 -0
  164. package/src/mcp/builtin/collections/countVersionsTool.ts +69 -0
  165. package/src/mcp/builtin/collections/createTool.ts +27 -18
  166. package/src/mcp/builtin/collections/deleteTool.ts +11 -2
  167. package/src/mcp/builtin/collections/duplicateTool.ts +134 -0
  168. package/src/mcp/builtin/collections/fileInput.spec.ts +79 -0
  169. package/src/mcp/builtin/collections/fileInput.ts +139 -0
  170. package/src/mcp/builtin/collections/findDistinctTool.ts +119 -0
  171. package/src/mcp/builtin/collections/findTool.ts +59 -20
  172. package/src/mcp/builtin/collections/findVersionByIDTool.ts +109 -0
  173. package/src/mcp/builtin/collections/findVersionsTool.ts +154 -0
  174. package/src/mcp/builtin/collections/formatCollectionError.ts +1 -1
  175. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +51 -3
  176. package/src/mcp/builtin/collections/restoreVersionTool.ts +107 -0
  177. package/src/mcp/builtin/collections/updateTool.ts +39 -23
  178. package/src/mcp/builtin/getConfigInfoTool.ts +27 -4
  179. package/src/mcp/builtin/globals/countVersionsTool.ts +68 -0
  180. package/src/mcp/builtin/globals/findTool.ts +27 -18
  181. package/src/mcp/builtin/globals/findVersionByIDTool.ts +103 -0
  182. package/src/mcp/builtin/globals/findVersionsTool.ts +147 -0
  183. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +37 -2
  184. package/src/mcp/builtin/globals/restoreVersionTool.ts +99 -0
  185. package/src/mcp/builtin/globals/updateTool.ts +16 -16
  186. package/src/mcp/builtin/validateEntityData.ts +1 -1
  187. package/src/mcp/builtinTools.ts +72 -16
  188. package/src/mcp/sanitizeMCPConfig.ts +48 -30
  189. package/src/stdio.ts +77 -45
  190. package/src/types.ts +108 -90
  191. package/src/utils/getPluginConfig.ts +1 -1
  192. package/src/utils/schemaConversion/filterFieldsByAccess.ts +123 -0
  193. package/src/utils/schemaConversion/getEntityInputSchema.ts +45 -27
  194. package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +12 -19
  195. package/src/utils/schemaConversion/sanitizeEntitySchema.ts +17 -114
  196. package/dist/collection/getAccessField.d.ts +0 -12
  197. package/dist/collection/getAccessField.d.ts.map +0 -1
  198. package/dist/collection/getAccessField.js +0 -57
  199. package/dist/collection/getAccessField.js.map +0 -1
  200. package/dist/collection/index.d.ts +0 -6
  201. package/dist/collection/index.d.ts.map +0 -1
  202. package/dist/collection/index.js +0 -60
  203. package/dist/collection/index.js.map +0 -1
  204. package/dist/components/AccessField/index.client.d.ts +0 -10
  205. package/dist/components/AccessField/index.client.d.ts.map +0 -1
  206. package/dist/components/AccessField/index.client.js +0 -305
  207. package/dist/components/AccessField/index.client.js.map +0 -1
  208. package/dist/components/AccessField/index.css +0 -93
  209. package/dist/utils/localAPIDefaults.d.ts +0 -20
  210. package/dist/utils/localAPIDefaults.d.ts.map +0 -1
  211. package/dist/utils/localAPIDefaults.js +0 -19
  212. package/dist/utils/localAPIDefaults.js.map +0 -1
  213. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts +0 -7
  214. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts.map +0 -1
  215. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js +0 -20
  216. package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js.map +0 -1
  217. package/src/collection/getAccessField.ts +0 -64
  218. package/src/collection/index.ts +0 -64
  219. package/src/components/AccessField/index.client.tsx +0 -347
  220. package/src/components/AccessField/index.css +0 -93
  221. package/src/utils/localAPIDefaults.ts +0 -22
  222. package/src/utils/schemaConversion/removeVirtualFieldsFromSchema.ts +0 -27
package/src/types.ts CHANGED
@@ -6,18 +6,18 @@ 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,
19
18
  } from 'payload'
20
19
 
20
+ import type { GetAuthorizedMCPArgs } from './endpoint/access.js'
21
21
  import type {
22
22
  MCPCollectionAuthToolName,
23
23
  MCPCollectionBuiltinName,
@@ -26,34 +26,16 @@ import type {
26
26
 
27
27
  export type { MCPCollectionAuthToolName, MCPCollectionBuiltinName, MCPGlobalBuiltinName }
28
28
 
29
- /** Re-exported from `@modelcontextprotocol/server` — the JSON Schema shape the MCP runtime validates against. */
30
- export type { JsonSchemaType, StandardSchemaWithJSON }
29
+ /** Re-exported from `@modelcontextprotocol/server` — common MCP types used in plugin config. */
30
+ export type { JsonSchemaType, StandardSchemaWithJSON, ToolAnnotations }
31
31
 
32
32
  /**
33
33
  * 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, …).
34
+ * JSON Schema 2020-12 literal, or a Standard Schema instance (Zod, Valibot, …).
35
+ * Raw schemas may omit `$schema`; when present it must declare the 2020-12 dialect.
35
36
  */
36
37
  export type ToolInputSchema = JsonSchemaType | StandardSchemaWithJSON
37
38
 
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
- configKey: string
50
- description: string
51
- globalSlug?: string
52
- label: string
53
- type: 'collectionTool' | 'globalTool' | 'prompt' | 'resource' | 'tool'
54
- }>
55
- }
56
-
57
39
  export type MCPToolResponse = {
58
40
  content: ContentBlock[]
59
41
  /**
@@ -70,6 +52,13 @@ export type MCPResponseOverride = (
70
52
  req: PayloadRequest,
71
53
  ) => MCPToolResponse
72
54
 
55
+ export type MCPAfterToolCallHook = (args: {
56
+ input: unknown
57
+ req: PayloadRequest
58
+ response: MCPToolResponse
59
+ toolName: string
60
+ }) => MaybePromise<MCPToolResponse>
61
+
73
62
  /**
74
63
  * The handler's `input` type. A specific Standard Schema (Zod, Valibot, …) gets
75
64
  * its inferred output; anything else falls back to `Record<string, unknown>`.
@@ -80,6 +69,19 @@ export type TypedInput<TSchema> = TSchema extends StandardSchemaWithJSON
80
69
  : StandardSchemaWithJSON.InferOutput<TSchema>
81
70
  : Record<string, unknown>
82
71
 
72
+ export type MCPAccessArgs = {
73
+ permissions?: SanitizedPermissions
74
+ req: PayloadRequest
75
+ }
76
+
77
+ export type CollectionMCPAccessArgs = {
78
+ collectionSlug: CollectionSlug
79
+ } & MCPAccessArgs
80
+
81
+ export type GlobalMCPAccessArgs = {
82
+ globalSlug: GlobalSlug
83
+ } & MCPAccessArgs
84
+
83
85
  export type ToolHandlerArgs<TSchema = undefined> = {
84
86
  authorizedMCP: AuthorizedMCP
85
87
  input: TypedInput<TSchema>
@@ -96,6 +98,13 @@ export type GlobalToolHandlerArgs<TSchema = undefined> = {
96
98
  } & ToolHandlerArgs<TSchema>
97
99
 
98
100
  export type Tool<TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined> = {
101
+ /**
102
+ * Runs while authorizing each MCP request, before the tool is advertised or called. Return
103
+ * `false` to make the tool unavailable for that request. This is skipped when `overrideAccess`
104
+ * is enabled.
105
+ */
106
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
107
+ annotations?: ToolAnnotations
99
108
  description: string
100
109
  handler: (args: ToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
101
110
  input?: TSchema
@@ -108,22 +117,54 @@ export type Tool<TSchema extends ToolInputSchema | undefined = ToolInputSchema |
108
117
  export type CollectionTool<
109
118
  TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined,
110
119
  > = {
120
+ /**
121
+ * Runs while authorizing each MCP request for this collection. Return `false` to reject calls
122
+ * to this tool for the collection. The shared tool is not advertised when no collections allow
123
+ * it, but can remain advertised when it is available for another collection. This is skipped
124
+ * when `overrideAccess` is enabled.
125
+ */
126
+ access?: (args: CollectionMCPAccessArgs) => MaybePromise<boolean>
111
127
  handler: (args: CollectionToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
112
128
  input?: TSchema
113
- } & Pick<Tool, 'description' | 'overrideResponse'>
129
+ } & Pick<Tool, 'annotations' | 'description' | 'overrideResponse'>
114
130
 
115
131
  export type GlobalTool<TSchema extends ToolInputSchema | undefined = ToolInputSchema | undefined> =
116
132
  {
133
+ /**
134
+ * Runs while authorizing each MCP request for this global. Return `false` to reject calls to
135
+ * this tool for the global. The shared tool is not advertised when no globals allow it, but
136
+ * can remain advertised when it is available for another global. This is skipped when
137
+ * `overrideAccess` is enabled.
138
+ */
139
+ access?: (args: GlobalMCPAccessArgs) => MaybePromise<boolean>
117
140
  handler: (args: GlobalToolHandlerArgs<TSchema>) => MaybePromise<MCPToolResponse>
118
141
  input?: TSchema
119
- } & Pick<Tool, 'description' | 'overrideResponse'>
142
+ } & Pick<Tool, 'annotations' | 'description' | 'overrideResponse'>
120
143
 
121
144
  /**
122
145
  * Configures (or disables) a built-in tool without replacing it.
123
146
  * `handler?: never` prevents a full `CollectionTool`/`GlobalTool` (which has a
124
147
  * required handler) from being silently accepted at a built-in key slot.
125
148
  */
126
- export type MCPBuiltInToolOverride = {
149
+ export type MCPBuiltInCollectionToolOverride = {
150
+ /**
151
+ * Replaces the built-in tool's access check. Return `false` to make the tool unavailable for
152
+ * this collection. This is skipped when `overrideAccess` is enabled.
153
+ */
154
+ access?: (args: CollectionMCPAccessArgs) => MaybePromise<boolean>
155
+ annotations?: ToolAnnotations
156
+ description?: string
157
+ handler?: never
158
+ overrideResponse?: MCPResponseOverride
159
+ }
160
+
161
+ export type MCPBuiltInGlobalToolOverride = {
162
+ /**
163
+ * Replaces the built-in tool's access check. Return `false` to make the tool unavailable for
164
+ * this global. This is skipped when `overrideAccess` is enabled.
165
+ */
166
+ access?: (args: GlobalMCPAccessArgs) => MaybePromise<boolean>
167
+ annotations?: ToolAnnotations
127
168
  description?: string
128
169
  handler?: never
129
170
  overrideResponse?: MCPResponseOverride
@@ -137,13 +178,13 @@ export type MCPBuiltInToolOverride = {
137
178
  export type MCPTopLevelToolEntry = Tool
138
179
 
139
180
  export type MCPCollectionToolsMap = {
140
- [customToolName: string]: boolean | CollectionTool | MCPBuiltInToolOverride | undefined
181
+ [customToolName: string]: boolean | CollectionTool | MCPBuiltInCollectionToolOverride | undefined
141
182
  } & {
142
- [K in MCPCollectionBuiltinName]?: false | MCPBuiltInToolOverride
183
+ [K in MCPCollectionBuiltinName]?: false | MCPBuiltInCollectionToolOverride
143
184
  }
144
185
 
145
186
  export type MCPAuthCollectionToolsMap = {
146
- [K in MCPCollectionAuthToolName]?: MCPBuiltInToolOverride | true
187
+ [K in MCPCollectionAuthToolName]?: MCPBuiltInCollectionToolOverride | true
147
188
  } & MCPCollectionToolsMap
148
189
 
149
190
  /** Auth-enabled collections get auth-tool name autocomplete; others get CRUD-only. */
@@ -152,9 +193,9 @@ export type MCPToolsMapForCollection<Slug extends CollectionSlug> = Slug extends
152
193
  : MCPCollectionToolsMap
153
194
 
154
195
  export type MCPGlobalToolsMap = {
155
- [customToolName: string]: boolean | GlobalTool | MCPBuiltInToolOverride | undefined
196
+ [customToolName: string]: boolean | GlobalTool | MCPBuiltInGlobalToolOverride | undefined
156
197
  } & {
157
- [K in MCPGlobalBuiltinName]?: false | MCPBuiltInToolOverride
198
+ [K in MCPGlobalBuiltinName]?: false | MCPBuiltInGlobalToolOverride
158
199
  }
159
200
 
160
201
  export type MCPTopLevelToolsMap = Record<string, Tool>
@@ -166,6 +207,12 @@ export type PromptHandlerArgs<TSchema = undefined> = {
166
207
  }
167
208
 
168
209
  export type Prompt<TSchema extends ToolInputSchema = ToolInputSchema> = {
210
+ /**
211
+ * Runs while authorizing each MCP request, before the prompt is advertised or used. Return
212
+ * `false` to make the prompt unavailable for that request. This is skipped when
213
+ * `overrideAccess` is enabled.
214
+ */
215
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
169
216
  argsSchema: TSchema
170
217
  description: string
171
218
  handler: (args: PromptHandlerArgs<TSchema>) => MaybePromise<{
@@ -183,6 +230,12 @@ export type ResourceHandlerArgs = {
183
230
  }
184
231
 
185
232
  export type Resource = {
233
+ /**
234
+ * Runs while authorizing each MCP request, before the resource is advertised or read. Return
235
+ * `false` to make the resource unavailable for that request. This is skipped when
236
+ * `overrideAccess` is enabled.
237
+ */
238
+ access?: (args: MCPAccessArgs) => MaybePromise<boolean>
186
239
  description: string
187
240
  handler: (args: ResourceHandlerArgs) => MaybePromise<{
188
241
  contents: Array<{ text: string; uri: string }>
@@ -216,34 +269,39 @@ export type MCPPluginConfig = {
216
269
  collections?: {
217
270
  [Slug in CollectionSlug]?: MCPPluginCollectionConfig<Slug>
218
271
  }
219
- /** Skip MCP registration. The API key collection is still added (so DB / types stay stable). */
272
+ /** Skip MCP endpoint registration. */
220
273
  disabled?: boolean
221
274
  globals?: {
222
275
  [Slug in GlobalSlug]?: MCPPluginGlobalConfig
223
276
  }
277
+ hooks?: {
278
+ /** Transform a tool response after its handler returns */
279
+ afterToolCall?: MCPAfterToolCallHook[]
280
+ }
224
281
  mcp?: {
225
282
  serverOptions?: MCPServerOptions
226
283
  verboseLogs?: boolean
227
284
  }
228
- overrideApiKeyCollection?: (collection: CollectionConfig) => CollectionConfig
229
- /** Replace the default API-key auth with a custom resolver. */
230
- overrideAuth?: (args: {
231
- getAPIKeyDoc: (overrideApiKey?: string) => Promise<MCPAPIKeysDoc>
232
- getAuthorizedMCP: (args: { apiKeyDoc: MCPAPIKeysDoc }) => AuthorizedMCP
233
- pluginConfig: SanitizedMCPPluginConfig
234
- req: PayloadRequest
235
- }) => MaybePromise<AuthorizedMCP>
285
+ /**
286
+ * Replace the default MCP authorization resolver.
287
+ *
288
+ * This hook replaces the default authentication flow. It must set `req.user` to the
289
+ * authenticated Payload user, or to `null` for an anonymous caller, before returning.
290
+ */
291
+ overrideGetAuthorizedMCP?: (
292
+ args: {
293
+ pluginConfig: SanitizedMCPPluginConfig
294
+ } & GetAuthorizedMCPArgs,
295
+ ) => MaybePromise<AuthorizedMCP>
236
296
  prompts?: Record<string, Prompt>
237
297
  resources?: Record<string, Resource>
238
298
  /** Cross-cutting tools (not scoped to any collection or global). */
239
299
  tools?: MCPTopLevelToolsMap
240
- userCollection?: CollectionSlug
241
300
  }
242
301
 
243
302
  export type SanitizedMCPPluginConfig = {
244
303
  items: MCPItem[]
245
- userCollection: CollectionSlug
246
- } & Pick<MCPPluginConfig, 'disabled' | 'mcp' | 'overrideApiKeyCollection' | 'overrideAuth'>
304
+ } & Pick<MCPPluginConfig, 'disabled' | 'hooks' | 'mcp' | 'overrideGetAuthorizedMCP'>
247
305
 
248
306
  export type MCPServerOptions = {
249
307
  options?: ConstructorParameters<typeof McpServer>[1]
@@ -251,49 +309,12 @@ export type MCPServerOptions = {
251
309
  }
252
310
 
253
311
  /**
254
- * Nested access tree as stored in the collection.
255
- * A `false` leaf disables that tool; missing keys defer to
256
- * defaults (built-in CRUD is on, opt-in tools are off).
257
- */
258
- export type MCPAPIKeysDocAccessTree = {
259
- collections?: {
260
- [CollectionSlug: CollectionSlug]: {
261
- [ToolKey: string]: boolean
262
- }
263
- }
264
- globals?: {
265
- [GlobalSlug: GlobalSlug]: {
266
- [ToolKey: string]: boolean
267
- }
268
- }
269
- prompts?: {
270
- [PromptKey: string]: boolean
271
- }
272
- resources?: {
273
- [ResourceKey: string]: boolean
274
- }
275
- tools?: {
276
- [ToolKey: string]: boolean
277
- }
278
- }
279
-
280
- /**
281
- * Stored on `payload-mcp-api-keys` docs
282
- */
283
- export type MCPAPIKeysDoc = {
284
- access: MCPAPIKeysDocAccessTree
285
- id: DefaultDocumentIDType
286
- overrideAccess?: boolean
287
- user: null | TypedUser
288
- }
289
-
290
- /**
291
- * One MCP primitive plus the metadata needed for access checks, admin UI, and
312
+ * One MCP primitive plus the metadata needed for config-driven filtering and
292
313
  * registration.
293
314
  *
294
- * - `configKey`: the config/API-key identifier, e.g. `find` or `echo`.
315
+ * - `configKey`: the config identifier, e.g. `find` or `echo`.
295
316
  * - `mcpName`: the MCP wire name, e.g. `findDocuments` or `echo`.
296
- * - `label`: human-readable admin checkbox text.
317
+ * - `label`: human-readable display text.
297
318
  */
298
319
  export type MCPItemBase = {
299
320
  configKey: string
@@ -330,13 +351,10 @@ export type MCPItem =
330
351
  | GlobalMCPItem
331
352
 
332
353
  /**
333
- * The caller's identity + the MCP items they can use for this request. Returned
334
- * by `getAuthorizedMCP`; denied items are simply absent from `items`. Handlers
335
- * receive this via `args.authorizedMCP` so they can spread
336
- * `localAPIDefaults(authorizedMCP)` into every local API call.
354
+ * The MCP items and access mode authorized for this request. The authenticated user is available
355
+ * as `req.user`.
337
356
  */
338
357
  export type AuthorizedMCP = {
339
358
  items: MCPItem[]
340
359
  overrideAccess: boolean
341
- user: null | TypedUser
342
360
  }
@@ -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,123 @@
1
+ import type {
2
+ FlattenedBlock,
3
+ FlattenedField,
4
+ Operation,
5
+ SanitizedDocumentPermissions,
6
+ SanitizedFieldsPermissions,
7
+ } from 'payload'
8
+
9
+ type FieldOperation = Exclude<Operation, 'delete'>
10
+ type AllowedFieldOperations = Record<FieldOperation, boolean>
11
+
12
+ /**
13
+ * Filters fields using the access results in `permissions`. `shouldExcludeField` receives the
14
+ * operations allowed by the entity, field, and its parents and decides whether to remove the
15
+ * field.
16
+ *
17
+ * Arrays, groups, named tabs, and blocks are filtered recursively. Rows and unnamed tabs are
18
+ * flattened by Payload, so their children are filtered at the containing level.
19
+ *
20
+ * @example Include fields writable through any collection operation the user can perform.
21
+ * ```ts
22
+ * filterFieldsByAccess({
23
+ * fields,
24
+ * permissions: collectionPermissions,
25
+ * shouldExcludeField: ({ create, update }) => !create && !update,
26
+ * })
27
+ * ```
28
+ */
29
+ export const filterFieldsByAccess = ({
30
+ blocks,
31
+ fields,
32
+ permissions,
33
+ shouldExcludeField,
34
+ }: {
35
+ blocks?: FlattenedBlock[]
36
+ fields: FlattenedField[]
37
+ permissions: SanitizedDocumentPermissions
38
+ shouldExcludeField: (allowedOperations: AllowedFieldOperations) => boolean
39
+ }): FlattenedField[] => {
40
+ const filterFields = (
41
+ nestedFields: FlattenedField[],
42
+ nestedPermissions: SanitizedFieldsPermissions | undefined,
43
+ parentAllowedOperations: AllowedFieldOperations,
44
+ ): FlattenedField[] => {
45
+ const accessibleFields: FlattenedField[] = []
46
+
47
+ for (const field of nestedFields) {
48
+ const fieldPermissions = nestedPermissions === true ? true : nestedPermissions?.[field.name]
49
+ const isOperationAllowed = (operation: FieldOperation): boolean =>
50
+ parentAllowedOperations[operation] &&
51
+ (fieldPermissions === true || fieldPermissions?.[operation] === true)
52
+ const allowedOperations: AllowedFieldOperations = {
53
+ create: isOperationAllowed('create'),
54
+ read: isOperationAllowed('read'),
55
+ update: isOperationAllowed('update'),
56
+ }
57
+
58
+ if (shouldExcludeField(allowedOperations)) {
59
+ continue
60
+ }
61
+
62
+ if (field.type === 'blocks') {
63
+ const accessibleBlocks: (FlattenedBlock | string)[] = []
64
+
65
+ for (const blockOrReference of field.blocks) {
66
+ const block =
67
+ typeof blockOrReference === 'string'
68
+ ? blocks?.find(({ slug }) => slug === blockOrReference)
69
+ : blockOrReference
70
+
71
+ if (!block) {
72
+ continue
73
+ }
74
+
75
+ const blockPermissions =
76
+ fieldPermissions === true
77
+ ? true
78
+ : fieldPermissions?.blocks === true
79
+ ? true
80
+ : fieldPermissions?.blocks?.[block.slug]
81
+
82
+ accessibleBlocks.push({
83
+ ...block,
84
+ flattenedFields: filterFields(
85
+ block.flattenedFields,
86
+ blockPermissions === true ? true : blockPermissions?.fields,
87
+ allowedOperations,
88
+ ),
89
+ })
90
+ }
91
+
92
+ if (accessibleBlocks.length === 0) {
93
+ continue
94
+ }
95
+
96
+ accessibleFields.push({ ...field, blocks: accessibleBlocks })
97
+ continue
98
+ }
99
+
100
+ if (field.type === 'array' || field.type === 'group' || field.type === 'tab') {
101
+ accessibleFields.push({
102
+ ...field,
103
+ flattenedFields: filterFields(
104
+ field.flattenedFields,
105
+ fieldPermissions === true ? true : fieldPermissions?.fields,
106
+ allowedOperations,
107
+ ),
108
+ })
109
+ continue
110
+ }
111
+
112
+ accessibleFields.push(field)
113
+ }
114
+
115
+ return accessibleFields
116
+ }
117
+
118
+ return filterFields(fields, permissions.fields, {
119
+ create: 'create' in permissions && permissions.create === true,
120
+ read: permissions.read === true,
121
+ update: permissions.update === true,
122
+ })
123
+ }
@@ -1,26 +1,27 @@
1
1
  import {
2
2
  type CollectionSlug,
3
3
  entityToStandaloneJSONSchema,
4
+ type FlattenedField,
4
5
  type GlobalSlug,
5
6
  type PayloadRequest,
6
7
  type SanitizedCollectionConfig,
8
+ type SanitizedCollectionPermission,
7
9
  type SanitizedGlobalConfig,
10
+ type SanitizedGlobalPermission,
8
11
  } from 'payload'
9
12
 
10
13
  import type { JsonSchemaType } from '../../types.js'
11
14
 
12
- import {
13
- getCollectionVirtualFieldNames,
14
- getGlobalVirtualFieldNames,
15
- } from '../getVirtualFieldNames.js'
16
- import { removeVirtualFieldsFromSchema } from './removeVirtualFieldsFromSchema.js'
15
+ import { filterFieldsByAccess } from './filterFieldsByAccess.js'
17
16
  import { sanitizeEntitySchema } from './sanitizeEntitySchema.js'
18
17
 
19
18
  export const getCollectionInputSchema = ({
20
19
  collectionSlug,
20
+ permissions,
21
21
  req,
22
22
  }: {
23
23
  collectionSlug: CollectionSlug
24
+ permissions?: SanitizedCollectionPermission
24
25
  req: PayloadRequest
25
26
  }): JsonSchemaType | null => {
26
27
  const collection = req.payload.collections[collectionSlug]?.config
@@ -29,18 +30,27 @@ export const getCollectionInputSchema = ({
29
30
  return null
30
31
  }
31
32
 
32
- return buildEntityInputSchema({
33
- entity: collection,
34
- req,
35
- virtualFieldNames: getCollectionVirtualFieldNames(req.payload.config, collectionSlug),
33
+ if (!permissions) {
34
+ return buildEntityInputSchema({ entity: collection, req })
35
+ }
36
+
37
+ const fieldsAllowedByAccess = filterFieldsByAccess({
38
+ blocks: req.payload.config.blocks,
39
+ fields: collection.flattenedFields,
40
+ permissions,
41
+ shouldExcludeField: ({ create, update }) => !create && !update,
36
42
  })
43
+
44
+ return buildEntityInputSchema({ entity: collection, fields: fieldsAllowedByAccess, req })
37
45
  }
38
46
 
39
47
  export const getGlobalInputSchema = ({
40
48
  globalSlug,
49
+ permissions,
41
50
  req,
42
51
  }: {
43
52
  globalSlug: GlobalSlug
53
+ permissions?: SanitizedGlobalPermission
44
54
  req: PayloadRequest
45
55
  }): JsonSchemaType | null => {
46
56
  const global = req.payload.config.globals.find((globalConfig) => globalConfig.slug === globalSlug)
@@ -49,30 +59,38 @@ export const getGlobalInputSchema = ({
49
59
  return null
50
60
  }
51
61
 
52
- return buildEntityInputSchema({
53
- entity: global,
54
- req,
55
- virtualFieldNames: getGlobalVirtualFieldNames(req.payload.config, globalSlug),
62
+ if (!permissions) {
63
+ return buildEntityInputSchema({ entity: global, req })
64
+ }
65
+
66
+ const fieldsAllowedByAccess = filterFieldsByAccess({
67
+ blocks: req.payload.config.blocks,
68
+ fields: global.flattenedFields,
69
+ permissions,
70
+ shouldExcludeField: ({ create, update }) => !create && !update,
56
71
  })
72
+
73
+ return buildEntityInputSchema({ entity: global, fields: fieldsAllowedByAccess, req })
57
74
  }
58
75
 
59
76
  const buildEntityInputSchema = ({
60
77
  entity,
78
+ fields = entity.flattenedFields,
61
79
  req,
62
- virtualFieldNames,
63
80
  }: {
64
81
  entity: SanitizedCollectionConfig | SanitizedGlobalConfig
82
+ fields?: FlattenedField[]
65
83
  req: PayloadRequest
66
- virtualFieldNames: string[]
67
- }): JsonSchemaType =>
68
- sanitizeEntitySchema(
69
- removeVirtualFieldsFromSchema(
70
- entityToStandaloneJSONSchema({
71
- config: req.payload.config,
72
- defaultIDType: req.payload.db.defaultIDType,
73
- entity,
74
- i18n: req.i18n,
75
- }) as unknown as JsonSchemaType,
76
- virtualFieldNames,
77
- ),
78
- )
84
+ }): JsonSchemaType => {
85
+ // The core schema generator reads flattenedFields from the entity and has no fields argument.
86
+ const entityForSchema = { ...entity, flattenedFields: fields }
87
+ const schema = entityToStandaloneJSONSchema({
88
+ config: req.payload.config,
89
+ defaultIDType: req.payload.db.defaultIDType,
90
+ entity: entityForSchema,
91
+ i18n: req.i18n,
92
+ variant: 'input',
93
+ }) as unknown as JsonSchemaType
94
+
95
+ return sanitizeEntitySchema(schema)
96
+ }
@@ -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 timestamps are excluded upstream by the `input` variant. `id` stays because it's
80
+ // 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: {