@payloadcms/plugin-mcp 4.0.0-internal.a0ef1b8 → 4.0.0-internal.bc5cf8c

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 (201) hide show
  1. package/dist/defaultAccess.d.ts +3 -0
  2. package/dist/defaultAccess.d.ts.map +1 -0
  3. package/dist/defaultAccess.js +3 -0
  4. package/dist/defaultAccess.js.map +1 -0
  5. package/dist/endpoint/access.d.ts +17 -5
  6. package/dist/endpoint/access.d.ts.map +1 -1
  7. package/dist/endpoint/access.js +82 -88
  8. package/dist/endpoint/access.js.map +1 -1
  9. package/dist/endpoint/index.d.ts.map +1 -1
  10. package/dist/endpoint/index.js +11 -0
  11. package/dist/endpoint/index.js.map +1 -1
  12. package/dist/exports/client.d.ts +1 -1
  13. package/dist/exports/client.d.ts.map +1 -1
  14. package/dist/exports/client.js +1 -1
  15. package/dist/exports/client.js.map +1 -1
  16. package/dist/exports/internal.d.ts +2 -0
  17. package/dist/exports/internal.d.ts.map +1 -0
  18. package/dist/exports/internal.js +3 -0
  19. package/dist/exports/internal.js.map +1 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +13 -18
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  24. package/dist/mcp/buildMcpServer.js +102 -71
  25. package/dist/mcp/buildMcpServer.js.map +1 -1
  26. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  27. package/dist/mcp/builtin/collections/authTools.js +44 -6
  28. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  29. package/dist/mcp/builtin/collections/countTool.d.ts +2 -0
  30. package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -0
  31. package/dist/mcp/builtin/collections/countTool.js +69 -0
  32. package/dist/mcp/builtin/collections/countTool.js.map +1 -0
  33. package/dist/mcp/builtin/collections/countVersionsTool.d.ts +2 -0
  34. package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -0
  35. package/dist/mcp/builtin/collections/countVersionsTool.js +65 -0
  36. package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -0
  37. package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
  38. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  39. package/dist/mcp/builtin/collections/createTool.js +38 -42
  40. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  41. package/dist/mcp/builtin/collections/deleteTool.d.ts +1 -1
  42. package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
  43. package/dist/mcp/builtin/collections/deleteTool.js +14 -20
  44. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  45. package/dist/mcp/builtin/collections/duplicateTool.d.ts +2 -0
  46. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -0
  47. package/dist/mcp/builtin/collections/duplicateTool.js +97 -0
  48. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -0
  49. package/dist/mcp/builtin/collections/findDistinctTool.d.ts +2 -0
  50. package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -0
  51. package/dist/mcp/builtin/collections/findDistinctTool.js +93 -0
  52. package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -0
  53. package/dist/mcp/builtin/collections/findTool.d.ts +1 -1
  54. package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
  55. package/dist/mcp/builtin/collections/findTool.js +49 -43
  56. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  57. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts +2 -0
  58. package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -0
  59. package/dist/mcp/builtin/collections/findVersionByIDTool.js +84 -0
  60. package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -0
  61. package/dist/mcp/builtin/collections/findVersionsTool.d.ts +2 -0
  62. package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -0
  63. package/dist/mcp/builtin/collections/findVersionsTool.js +99 -0
  64. package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -0
  65. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +9 -0
  66. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +1 -0
  67. package/dist/mcp/builtin/collections/formatCollectionError.js +60 -0
  68. package/dist/mcp/builtin/collections/formatCollectionError.js.map +1 -0
  69. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts +2 -0
  70. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -0
  71. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +47 -0
  72. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -0
  73. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts +2 -0
  74. package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -0
  75. package/dist/mcp/builtin/collections/restoreVersionTool.js +82 -0
  76. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -0
  77. package/dist/mcp/builtin/collections/updateTool.d.ts +1 -1
  78. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  79. package/dist/mcp/builtin/collections/updateTool.js +87 -86
  80. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  81. package/dist/mcp/builtin/getConfigInfoTool.d.ts +2 -0
  82. package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -0
  83. package/dist/mcp/builtin/getConfigInfoTool.js +71 -0
  84. package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -0
  85. package/dist/mcp/builtin/globals/countVersionsTool.d.ts +2 -0
  86. package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -0
  87. package/dist/mcp/builtin/globals/countVersionsTool.js +65 -0
  88. package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -0
  89. package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
  90. package/dist/mcp/builtin/globals/findTool.js +18 -21
  91. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  92. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts +2 -0
  93. package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -0
  94. package/dist/mcp/builtin/globals/findVersionByIDTool.js +80 -0
  95. package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -0
  96. package/dist/mcp/builtin/globals/findVersionsTool.d.ts +2 -0
  97. package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -0
  98. package/dist/mcp/builtin/globals/findVersionsTool.js +95 -0
  99. package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -0
  100. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts +2 -0
  101. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -0
  102. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +47 -0
  103. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -0
  104. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts +2 -0
  105. package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -0
  106. package/dist/mcp/builtin/globals/restoreVersionTool.js +78 -0
  107. package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -0
  108. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  109. package/dist/mcp/builtin/globals/updateTool.js +31 -40
  110. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  111. package/dist/mcp/builtin/validateEntityData.d.ts +14 -0
  112. package/dist/mcp/builtin/validateEntityData.d.ts.map +1 -0
  113. package/dist/mcp/builtin/validateEntityData.js +82 -0
  114. package/dist/mcp/builtin/validateEntityData.js.map +1 -0
  115. package/dist/mcp/builtinTools.d.ts +144 -19
  116. package/dist/mcp/builtinTools.d.ts.map +1 -1
  117. package/dist/mcp/builtinTools.js +119 -20
  118. package/dist/mcp/builtinTools.js.map +1 -1
  119. package/dist/mcp/sanitizeMCPConfig.d.ts +0 -1
  120. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  121. package/dist/mcp/sanitizeMCPConfig.js +120 -58
  122. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  123. package/dist/stdio.d.ts +3 -2
  124. package/dist/stdio.d.ts.map +1 -1
  125. package/dist/stdio.js +27 -8
  126. package/dist/stdio.js.map +1 -1
  127. package/dist/types.d.ts +62 -108
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/types.js +4 -4
  130. package/dist/types.js.map +1 -1
  131. package/dist/utils/getPluginConfig.d.ts +1 -1
  132. package/dist/utils/getPluginConfig.js +1 -1
  133. package/dist/utils/getPluginConfig.js.map +1 -1
  134. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts +11 -0
  135. package/dist/utils/schemaConversion/getEntityInputSchema.d.ts.map +1 -0
  136. package/dist/utils/schemaConversion/getEntityInputSchema.js +34 -0
  137. package/dist/utils/schemaConversion/getEntityInputSchema.js.map +1 -0
  138. package/dist/utils/whereSchema.d.ts +9 -0
  139. package/dist/utils/whereSchema.d.ts.map +1 -0
  140. package/dist/utils/whereSchema.js +13 -0
  141. package/dist/utils/whereSchema.js.map +1 -0
  142. package/package.json +8 -11
  143. package/src/defaultAccess.ts +3 -0
  144. package/src/endpoint/access.ts +88 -101
  145. package/src/endpoint/index.ts +14 -1
  146. package/src/exports/client.ts +2 -1
  147. package/src/exports/internal.ts +1 -0
  148. package/src/index.ts +8 -16
  149. package/src/mcp/buildMcpServer.ts +131 -96
  150. package/src/mcp/builtin/collections/authTools.ts +45 -9
  151. package/src/mcp/builtin/collections/countTool.ts +74 -0
  152. package/src/mcp/builtin/collections/countVersionsTool.ts +70 -0
  153. package/src/mcp/builtin/collections/createTool.ts +55 -59
  154. package/src/mcp/builtin/collections/deleteTool.ts +19 -16
  155. package/src/mcp/builtin/collections/duplicateTool.ts +135 -0
  156. package/src/mcp/builtin/collections/findDistinctTool.ts +120 -0
  157. package/src/mcp/builtin/collections/findTool.ts +63 -33
  158. package/src/mcp/builtin/collections/findVersionByIDTool.ts +110 -0
  159. package/src/mcp/builtin/collections/findVersionsTool.ts +155 -0
  160. package/src/mcp/builtin/collections/formatCollectionError.ts +84 -0
  161. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +46 -0
  162. package/src/mcp/builtin/collections/restoreVersionTool.ts +108 -0
  163. package/src/mcp/builtin/collections/updateTool.ts +111 -110
  164. package/src/mcp/builtin/getConfigInfoTool.ts +69 -0
  165. package/src/mcp/builtin/globals/countVersionsTool.ts +69 -0
  166. package/src/mcp/builtin/globals/findTool.ts +26 -17
  167. package/src/mcp/builtin/globals/findVersionByIDTool.ts +104 -0
  168. package/src/mcp/builtin/globals/findVersionsTool.ts +148 -0
  169. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +41 -0
  170. package/src/mcp/builtin/globals/restoreVersionTool.ts +100 -0
  171. package/src/mcp/builtin/globals/updateTool.ts +50 -55
  172. package/src/mcp/builtin/validateEntityData.ts +132 -0
  173. package/src/mcp/builtinTools.ts +111 -41
  174. package/src/mcp/sanitizeMCPConfig.ts +116 -78
  175. package/src/stdio.ts +23 -8
  176. package/src/types.ts +77 -112
  177. package/src/utils/getPluginConfig.ts +1 -1
  178. package/src/utils/schemaConversion/getEntityInputSchema.ts +78 -0
  179. package/src/utils/whereSchema.ts +24 -0
  180. package/dist/collection/getAccessField.d.ts +0 -12
  181. package/dist/collection/getAccessField.d.ts.map +0 -1
  182. package/dist/collection/getAccessField.js +0 -57
  183. package/dist/collection/getAccessField.js.map +0 -1
  184. package/dist/collection/index.d.ts +0 -6
  185. package/dist/collection/index.d.ts.map +0 -1
  186. package/dist/collection/index.js +0 -60
  187. package/dist/collection/index.js.map +0 -1
  188. package/dist/components/AccessField/index.client.d.ts +0 -10
  189. package/dist/components/AccessField/index.client.d.ts.map +0 -1
  190. package/dist/components/AccessField/index.client.js +0 -305
  191. package/dist/components/AccessField/index.client.js.map +0 -1
  192. package/dist/components/AccessField/index.css +0 -93
  193. package/dist/utils/schemaConversion/buildToolInput.d.ts +0 -29
  194. package/dist/utils/schemaConversion/buildToolInput.d.ts.map +0 -1
  195. package/dist/utils/schemaConversion/buildToolInput.js +0 -51
  196. package/dist/utils/schemaConversion/buildToolInput.js.map +0 -1
  197. package/src/collection/getAccessField.ts +0 -64
  198. package/src/collection/index.ts +0 -64
  199. package/src/components/AccessField/index.client.tsx +0 -344
  200. package/src/components/AccessField/index.css +0 -93
  201. package/src/utils/schemaConversion/buildToolInput.ts +0 -68
@@ -1,132 +1,119 @@
1
- import type { DefaultDocumentIDType, PayloadRequest, TypedUser } from 'payload'
1
+ import type { PayloadRequest, SanitizedPermissions } from 'payload'
2
2
 
3
- import crypto from 'crypto'
4
- import { UnauthorizedError } from 'payload'
3
+ import { getAccessResults, UnauthorizedError } from 'payload'
5
4
 
6
- import type { AuthorizedMCP, MCPAPIKeysDoc } from '../types.js'
5
+ import type { AuthorizedMCP, MCPItem } from '../types.js'
7
6
 
8
- import { getLogger } from '../utils/getLogger.js'
9
7
  import { getPluginConfig } from '../utils/getPluginConfig.js'
10
8
 
9
+ export type GetAuthorizedMCPArgs = {
10
+ overrideAccess: boolean
11
+ req: PayloadRequest
12
+ }
13
+
11
14
  /**
12
- * Resolves the API key (or dev-mode session) and returns the items the caller
13
- * may use. Denied items are dropped from the array.
15
+ * Resolves the MCP caller and returns the MCP surface authorized for that request.
16
+ *
17
+ * Authorization has two layers:
18
+ * 1. Payload collection/global permissions determine whether built-in operation tools are shown.
19
+ * 2. MCP `access` callbacks can further hide any configured tool, prompt, or resource.
20
+ *
21
+ * Like Payload core operations, `overrideAccess` skips both layers.
14
22
  */
15
- export const getAuthorizedMCP: (args: { req: PayloadRequest }) => Promise<AuthorizedMCP> = async ({
23
+ export const getAuthorizedMCP: (args: GetAuthorizedMCPArgs) => Promise<AuthorizedMCP> = async ({
24
+ overrideAccess,
16
25
  req,
17
26
  }) => {
18
- const logger = getLogger({ payload: req.payload })
19
27
  const pluginConfig = getPluginConfig({ config: req.payload.config })
20
28
 
21
- const authHeader = req.headers.get('Authorization')
22
- const hasBearerToken = authHeader?.startsWith('Bearer ')
23
-
24
- const buildAuthorized = (apiKeyDoc: MCPAPIKeysDoc): AuthorizedMCP => ({
25
- items: pluginConfig.items.filter((item) => {
26
- switch (item.type) {
27
- case 'collectionTool':
28
- return apiKeyDoc.access.collections?.[item.collectionSlug]?.[item.key] !== false
29
- case 'globalTool':
30
- return apiKeyDoc.access.globals?.[item.globalSlug]?.[item.key] !== false
31
- case 'prompt':
32
- return apiKeyDoc.access.prompts?.[item.key] !== false
33
- case 'resource':
34
- return apiKeyDoc.access.resources?.[item.key] !== false
35
- case 'tool':
36
- return apiKeyDoc.access.tools?.[item.key] !== false
37
- }
38
- }),
39
- overrideAccess:
40
- typeof apiKeyDoc.overrideAccess === 'boolean' ? apiKeyDoc.overrideAccess : false,
41
- user: apiKeyDoc.user,
42
- })
43
-
44
- if (pluginConfig.overrideAuth) {
45
- return await pluginConfig.overrideAuth({
46
- getAPIKeyDoc: (overrideApiKey) => getAPIKeyDoc({ logger, overrideApiKey, pluginConfig, req }),
47
- getAuthorizedMCP: ({ apiKeyDoc }) => buildAuthorized(apiKeyDoc),
29
+ if (pluginConfig.overrideGetAuthorizedMCP) {
30
+ return await pluginConfig.overrideGetAuthorizedMCP({
31
+ overrideAccess,
48
32
  pluginConfig,
49
33
  req,
50
34
  })
51
35
  }
52
36
 
53
- if (process.env.NODE_ENV === 'development' && !hasBearerToken) {
54
- logger.info('Dev mode: skipping API key check, using session user')
55
- return buildAuthorized({
56
- id: -1,
57
- access: {},
58
- overrideAccess: true,
59
- user: req.user ?? null,
60
- })
37
+ if (req.headers) {
38
+ const headers = new Headers(req.headers)
39
+ const hasAuthorization = headers.has('Authorization')
40
+
41
+ headers.set('DisableAutologin', 'true')
42
+ req.user = (await req.payload.auth({ headers, req })).user
43
+
44
+ if (hasAuthorization && !req.user) {
45
+ throw new UnauthorizedError(req.t)
46
+ }
61
47
  }
62
48
 
63
- return buildAuthorized(await getAPIKeyDoc({ logger, pluginConfig, req }))
49
+ return {
50
+ items: await filterMCPItems({
51
+ items: pluginConfig.items,
52
+ overrideAccess,
53
+ req,
54
+ }),
55
+ overrideAccess,
56
+ user: req.user,
57
+ }
64
58
  }
65
59
 
66
- const getAPIKeyDoc = async ({
67
- logger,
68
- overrideApiKey,
69
- pluginConfig,
60
+ export const filterMCPItems = async ({
61
+ items,
62
+ overrideAccess,
70
63
  req,
71
64
  }: {
72
- logger: ReturnType<typeof getLogger>
73
- overrideApiKey?: string
74
- pluginConfig: ReturnType<typeof getPluginConfig>
65
+ items: MCPItem[]
66
+ overrideAccess: boolean
75
67
  req: PayloadRequest
76
- }): Promise<MCPAPIKeysDoc> => {
77
- const authHeader = req.headers.get('Authorization')
78
- const hasBearerToken = authHeader?.startsWith('Bearer ')
79
-
80
- const apiKey =
81
- overrideApiKey ?? (hasBearerToken ? authHeader?.replace('Bearer ', '').trim() || null : null)
82
-
83
- if (!apiKey) {
84
- throw new UnauthorizedError()
85
- }
86
-
87
- const sha256APIKeyIndex = crypto
88
- .createHmac('sha256', req.payload.secret)
89
- .update(apiKey)
90
- .digest('hex')
91
-
92
- const doc = await req.payload.db.findOne<MCPAPIKeysDoc>({
93
- collection: 'payload-mcp-api-keys',
94
- req,
95
- where: {
96
- apiKeyIndex: { equals: sha256APIKeyIndex },
97
- },
98
- })
99
-
100
- if (!doc || !doc.user) {
101
- throw new UnauthorizedError()
68
+ }): Promise<MCPItem[]> => {
69
+ // Match Payload core: overrideAccess bypasses access evaluation instead of
70
+ // forcing each access function to return true.
71
+ if (overrideAccess) {
72
+ return items
102
73
  }
103
74
 
104
- logger.info('API Key is valid')
75
+ const authorizedItems: MCPItem[] = []
105
76
 
106
- const userRef = doc.user
107
- const userID =
108
- typeof userRef === 'object' && userRef !== null && 'id' in userRef
109
- ? userRef.id
110
- : (userRef as unknown as DefaultDocumentIDType)
77
+ const permissions = await getAccessResults({ req })
111
78
 
112
- const user = (await req.payload.findByID({
113
- id: userID,
114
- collection: pluginConfig.userCollection,
115
- depth: 0,
116
- disableErrors: true,
117
- req,
118
- })) as null | TypedUser
119
-
120
- if (!user) {
121
- throw new UnauthorizedError()
79
+ for (const item of items) {
80
+ if (!(await checkItemAccess({ item, permissions, req }))) {
81
+ continue
82
+ }
83
+ authorizedItems.push(item)
122
84
  }
123
85
 
124
- return {
125
- ...doc,
126
- user: {
127
- ...user,
128
- _strategy: 'mcp-api-key' as const,
129
- collection: pluginConfig.userCollection,
130
- },
86
+ return authorizedItems
87
+ }
88
+
89
+ /**
90
+ * Runs MCP item access callbacks
91
+ */
92
+ const checkItemAccess = async ({
93
+ item,
94
+ permissions,
95
+ req,
96
+ }: {
97
+ item: MCPItem
98
+ permissions?: SanitizedPermissions
99
+ req: PayloadRequest
100
+ }): Promise<boolean> => {
101
+ switch (item.type) {
102
+ case 'collectionTool':
103
+ return (
104
+ !item.tool.access ||
105
+ (await item.tool.access({ collectionSlug: item.collectionSlug, permissions, req }))
106
+ )
107
+ case 'globalTool':
108
+ return (
109
+ !item.tool.access ||
110
+ (await item.tool.access({ globalSlug: item.globalSlug, permissions, req }))
111
+ )
112
+ case 'prompt':
113
+ return !item.prompt.access || (await item.prompt.access({ permissions, req }))
114
+ case 'resource':
115
+ return !item.resource.access || (await item.resource.access({ permissions, req }))
116
+ case 'tool':
117
+ return !item.tool.access || (await item.tool.access({ permissions, req }))
131
118
  }
132
119
  }
@@ -13,7 +13,20 @@ export const mcpEndpoint: PayloadHandler = async (req) => {
13
13
  req.payloadAPI = 'MCP' as const
14
14
 
15
15
  const pluginConfig = getPluginConfig({ config: req.payload.config })
16
- const authorizedMCP = await getAuthorizedMCP({ req })
16
+ const overrideAccessParam = new URL(req.url).searchParams.get('overrideAccess')
17
+
18
+ if (overrideAccessParam !== null && process.env.NODE_ENV !== 'development') {
19
+ throw new APIError('MCP overrideAccess is only available in development.', 400)
20
+ }
21
+
22
+ let overrideAccess = false
23
+ if (overrideAccessParam === 'true') {
24
+ overrideAccess = true
25
+ } else if (overrideAccessParam !== null && overrideAccessParam !== 'false') {
26
+ throw new APIError('MCP overrideAccess must be "true" or "false".', 400)
27
+ }
28
+
29
+ const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })
17
30
 
18
31
  const server = buildMcpServer({ authorizedMCP, pluginConfig, req })
19
32
 
@@ -1,2 +1,3 @@
1
1
  'use client'
2
- export { AccessField } from '../components/AccessField/index.client.js'
2
+
3
+ export {}
@@ -0,0 +1 @@
1
+ export { filterMCPItems, getAuthorizedMCP } from '../endpoint/access.js'
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { defaultUserCollection, definePlugin } from 'payload'
1
+ import { definePlugin } from 'payload'
2
2
 
3
3
  import type { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'
4
4
 
5
- import { getAPIKeysCollection } from './collection/index.js'
6
5
  import { mcpEndpoint } from './endpoint/index.js'
7
6
  import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'
8
7
 
@@ -24,16 +23,6 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
24
23
  slug: '@payloadcms/plugin-mcp',
25
24
  order: 10,
26
25
  plugin: ({ config, plugins, ...rawConfig }) => {
27
- // Our `payload-mcp-api-keys` is auth-enabled; if it'd be the only auth
28
- // collection, Payload's later sanitize would pick it as `admin.user`.
29
- // Pre-seed the default user collection to prevent that.
30
- if (!config.admin?.user) {
31
- const firstCollectionWithAuth = (config.collections ?? []).find(({ auth }) => Boolean(auth))
32
- if (!firstCollectionWithAuth) {
33
- ;(config.collections ??= []).push(defaultUserCollection)
34
- }
35
- }
36
-
37
26
  const pluginConfig = sanitizeMCPConfig({ config, pluginConfig: rawConfig })
38
27
 
39
28
  // Stash the sanitized config on plugin options so `getPluginConfig()` reads it.
@@ -43,10 +32,6 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
43
32
  registered.sanitizedOptions = pluginConfig as unknown as typeof registered.options
44
33
  }
45
34
 
46
- ;(config.collections ??= []).push(getAPIKeysCollection({ pluginConfig }))
47
-
48
- // Keep the API-keys collection registered even when disabled, so DB schema
49
- // and generated types don't drift between enabled/disabled environments.
50
35
  if (pluginConfig.disabled) {
51
36
  return config
52
37
  }
@@ -57,6 +42,13 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
57
42
  ...(config.endpoints ?? []),
58
43
  // Payload prefixes /api, so the full path is /api/mcp.
59
44
  { handler: mcpEndpoint, method: 'post', path: '/mcp' },
45
+ // Streamable HTTP's optional server=>client GET stream. We don't offer
46
+ // one, so answer 405 per spec-— clients then skip it cleanly
47
+ {
48
+ handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),
49
+ method: 'get',
50
+ path: '/mcp',
51
+ },
60
52
  ],
61
53
  }
62
54
  },
@@ -1,29 +1,21 @@
1
1
  import { McpServer, type ServerContext } from '@modelcontextprotocol/server'
2
- import { APIError, entityToStandaloneJSONSchema, type PayloadRequest } from 'payload'
2
+ import { APIError, type PayloadRequest } from 'payload'
3
+ import { z } from 'zod'
3
4
 
4
5
  import type {
5
6
  AuthorizedMCP,
7
+ CollectionMCPItem,
8
+ GlobalMCPItem,
6
9
  JsonSchemaType,
7
10
  MCPResponseOverride,
8
11
  MCPToolResponse,
9
12
  SanitizedMCPPluginConfig,
13
+ ToolInputSchema,
10
14
  } from '../types.js'
11
15
 
12
- import { toCamelCase } from '../utils/camelCase.js'
13
16
  import { getLogger } from '../utils/getLogger.js'
14
- import {
15
- getCollectionVirtualFieldNames,
16
- getGlobalVirtualFieldNames,
17
- } from '../utils/getVirtualFieldNames.js'
18
- import { removeVirtualFieldsFromSchema } from '../utils/schemaConversion/removeVirtualFieldsFromSchema.js'
19
17
  import { toStandardSchema } from '../utils/toStandardSchema.js'
20
18
 
21
- /** `findPosts`, `updateSiteSettings` — auto-prefixed wire name for collection/global tools. */
22
- const wireName = (key: string, slug: string): string => {
23
- const camel = toCamelCase(slug)
24
- return `${key}${camel.charAt(0).toUpperCase()}${camel.slice(1)}`
25
- }
26
-
27
19
  /**
28
20
  * Transport-agnostic core: registers every authorized MCP item onto a fresh
29
21
  * `McpServer` and returns it. The caller is responsible for picking a transport
@@ -64,101 +56,106 @@ export const buildMcpServer = ({
64
56
  return rest
65
57
  }
66
58
 
59
+ /**
60
+ * Runs a collection/global tool call:
61
+ * - reads `collectionSlug` / `globalSlug` from the input
62
+ * - runs access control: errors if `authorizedMCP.items` has no entry for this tool + slug
63
+ * - runs the tool handler and finalizes its response
64
+ */
65
+ const callEntityTool = async ({
66
+ input,
67
+ item,
68
+ serverContext,
69
+ }: {
70
+ input: unknown
71
+ item: CollectionMCPItem | GlobalMCPItem
72
+ serverContext: ServerContext
73
+ }): Promise<MCPToolResponse> => {
74
+ const entity = item.type === 'collectionTool' ? 'collection' : 'global'
75
+ const slugKey = item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug'
76
+ const toolInput = (input ?? {}) as Record<string, unknown>
77
+ const slug = toolInput[slugKey] as string | undefined
78
+
79
+ if (!slug) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: `Error: "${item.mcpName}" requires ${slugKey}. Use getConfigInfo to inspect ${entity} slugs.`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ }
89
+ }
90
+
91
+ const match = authorizedMCP.items.find(
92
+ (candidate): candidate is CollectionMCPItem | GlobalMCPItem =>
93
+ candidate.type === item.type &&
94
+ candidate.mcpName === item.mcpName &&
95
+ (candidate.type === 'collectionTool'
96
+ ? candidate.collectionSlug === slug
97
+ : candidate.type === 'globalTool' && candidate.globalSlug === slug),
98
+ )
99
+
100
+ if (!match) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: 'text',
105
+ text: `Error: MCP access to "${item.mcpName}" is not enabled for ${entity} "${slug}"`,
106
+ },
107
+ ],
108
+ isError: true,
109
+ }
110
+ }
111
+
112
+ const handlerArgs = {
113
+ authorizedMCP,
114
+ input: toolInput,
115
+ req,
116
+ serverContext,
117
+ }
118
+ const response = await (match.type === 'collectionTool'
119
+ ? match.tool.handler({ ...handlerArgs, collectionSlug: slug })
120
+ : match.tool.handler({ ...handlerArgs, globalSlug: slug }))
121
+
122
+ return finalizeToolResponse(response, match.tool.overrideResponse)
123
+ }
124
+
67
125
  try {
126
+ const registeredEntityTools = new Set<string>()
127
+
68
128
  for (const item of authorizedMCP.items) {
69
129
  switch (item.type) {
70
- case 'collectionTool': {
71
- const tool = item.tool
72
- const name = wireName(item.key, item.collectionSlug)
73
- let inputSchema = tool.input
74
- if (typeof inputSchema === 'function') {
75
- const collection = req.payload.collections[item.collectionSlug]?.config
76
- if (!collection) {
77
- throw new APIError(
78
- `Collection schema not found for slug: ${item.collectionSlug}`,
79
- 500,
80
- )
81
- }
82
- const collectionSchema = removeVirtualFieldsFromSchema(
83
- entityToStandaloneJSONSchema({
84
- config: req.payload.config,
85
- defaultIDType: req.payload.db.defaultIDType,
86
- entity: collection,
87
- i18n: req.i18n,
88
- }) as unknown as JsonSchemaType,
89
- getCollectionVirtualFieldNames(req.payload.config, item.collectionSlug),
90
- )
91
- inputSchema = inputSchema({ collectionSchema })
92
- }
93
- server.registerTool(
94
- name,
95
- {
96
- description: tool.description,
97
- inputSchema: inputSchema ? toStandardSchema(inputSchema) : undefined,
98
- },
99
- async (input: unknown, ctx: ServerContext) =>
100
- finalizeToolResponse(
101
- await tool.handler({
102
- authorizedMCP,
103
- collectionSlug: item.collectionSlug,
104
- input: (input ?? {}) as Record<string, unknown>,
105
- req,
106
- serverContext: ctx,
107
- }),
108
- tool.overrideResponse,
109
- ),
110
- )
111
- logger.info(`✅ Tool: ${name} Registered.`)
112
- break
113
- }
130
+ case 'collectionTool':
114
131
  case 'globalTool': {
115
- const tool = item.tool
116
- const name = wireName(item.key, item.globalSlug)
117
- let inputSchema = tool.input
118
- if (typeof inputSchema === 'function') {
119
- const globalEntity = req.payload.config.globals.find(
120
- (globalConfig) => globalConfig.slug === item.globalSlug,
121
- )
122
- if (!globalEntity) {
123
- throw new APIError(`Global schema not found for slug: ${item.globalSlug}`, 500)
124
- }
125
- const globalSchema = removeVirtualFieldsFromSchema(
126
- entityToStandaloneJSONSchema({
127
- config: req.payload.config,
128
- defaultIDType: req.payload.db.defaultIDType,
129
- entity: globalEntity,
130
- i18n: req.i18n,
131
- }) as unknown as JsonSchemaType,
132
- getGlobalVirtualFieldNames(req.payload.config, item.globalSlug),
133
- )
134
-
135
- inputSchema = inputSchema({ globalSchema })
132
+ if (registeredEntityTools.has(item.mcpName)) {
133
+ break
136
134
  }
135
+ registeredEntityTools.add(item.mcpName)
136
+
137
+ const inputSchema = withSlugInput({
138
+ name: item.type === 'collectionTool' ? 'collectionSlug' : 'globalSlug',
139
+ input: item.tool.input,
140
+ })
141
+
137
142
  server.registerTool(
138
- name,
143
+ item.mcpName,
139
144
  {
140
- description: tool.description,
141
- inputSchema: inputSchema ? toStandardSchema(inputSchema) : undefined,
145
+ annotations: item.tool.annotations,
146
+ description: item.tool.description,
147
+ inputSchema: toStandardSchema(inputSchema),
142
148
  },
143
149
  async (input: unknown, ctx: ServerContext) =>
144
- finalizeToolResponse(
145
- await tool.handler({
146
- authorizedMCP,
147
- globalSlug: item.globalSlug,
148
- input: (input ?? {}) as Record<string, unknown>,
149
- req,
150
- serverContext: ctx,
151
- }),
152
- tool.overrideResponse,
153
- ),
150
+ callEntityTool({ input, item, serverContext: ctx }),
154
151
  )
155
- logger.info(`✅ Tool: ${name} Registered.`)
152
+ logger.info(`✅ Tool: ${item.mcpName} Registered.`)
156
153
  break
157
154
  }
158
155
  case 'prompt': {
159
156
  const prompt = item.prompt
160
157
  server.registerPrompt(
161
- item.key,
158
+ item.mcpName,
162
159
  {
163
160
  argsSchema: prompt.argsSchema ? toStandardSchema(prompt.argsSchema) : undefined,
164
161
  description: prompt.description,
@@ -177,7 +174,7 @@ export const buildMcpServer = ({
177
174
  case 'resource': {
178
175
  const resource = item.resource
179
176
  server.registerResource(
180
- item.key,
177
+ item.mcpName,
181
178
  // @ts-expect-error - Overload type ambiguity (string OR ResourceTemplate is valid)
182
179
  resource.uri,
183
180
  {
@@ -200,8 +197,9 @@ export const buildMcpServer = ({
200
197
  case 'tool': {
201
198
  const tool = item.tool
202
199
  server.registerTool(
203
- item.key,
200
+ item.mcpName,
204
201
  {
202
+ annotations: tool.annotations,
205
203
  description: tool.description,
206
204
  inputSchema: tool.input ? toStandardSchema(tool.input) : undefined,
207
205
  },
@@ -216,7 +214,7 @@ export const buildMcpServer = ({
216
214
  tool.overrideResponse,
217
215
  ),
218
216
  )
219
- logger.info(`✅ Tool: ${item.key} Registered.`)
217
+ logger.info(`✅ Tool: ${item.mcpName} Registered.`)
220
218
  break
221
219
  }
222
220
  }
@@ -227,3 +225,40 @@ export const buildMcpServer = ({
227
225
 
228
226
  return server
229
227
  }
228
+
229
+ const withSlugInput = ({
230
+ name,
231
+ input,
232
+ }: {
233
+ input?: ToolInputSchema
234
+ name: 'collectionSlug' | 'globalSlug'
235
+ }): ToolInputSchema => {
236
+ const description = name === 'collectionSlug' ? 'The collection slug' : 'The global slug'
237
+ const slugSchema = z.string().describe(description)
238
+
239
+ if (!input) {
240
+ return z.object({ [name]: slugSchema })
241
+ }
242
+
243
+ if (input instanceof z.ZodObject) {
244
+ return input.extend({ [name]: slugSchema })
245
+ }
246
+
247
+ const schema = input as {
248
+ properties?: Record<string, JsonSchemaType>
249
+ required?: string[]
250
+ } & JsonSchemaType
251
+
252
+ return {
253
+ ...schema,
254
+ type: 'object',
255
+ properties: {
256
+ ...schema.properties,
257
+ [name]: {
258
+ type: 'string',
259
+ description,
260
+ },
261
+ },
262
+ required: Array.from(new Set([name, ...(schema.required ?? [])])),
263
+ }
264
+ }