@payloadcms/plugin-mcp 3.70.0 → 3.71.0-internal.727c7a4

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 (52) hide show
  1. package/dist/collections/createApiKeysCollection.d.ts +1 -1
  2. package/dist/collections/createApiKeysCollection.d.ts.map +1 -1
  3. package/dist/collections/createApiKeysCollection.js +10 -75
  4. package/dist/collections/createApiKeysCollection.js.map +1 -1
  5. package/dist/endpoints/mcp.d.ts.map +1 -1
  6. package/dist/endpoints/mcp.js +1 -0
  7. package/dist/endpoints/mcp.js.map +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +4 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/mcp/getMcpHandler.d.ts.map +1 -1
  12. package/dist/mcp/getMcpHandler.js +24 -10
  13. package/dist/mcp/getMcpHandler.js.map +1 -1
  14. package/dist/mcp/tools/global/find.d.ts +5 -0
  15. package/dist/mcp/tools/global/find.d.ts.map +1 -0
  16. package/dist/mcp/tools/global/find.js +59 -0
  17. package/dist/mcp/tools/global/find.js.map +1 -0
  18. package/dist/mcp/tools/global/update.d.ts +6 -0
  19. package/dist/mcp/tools/global/update.d.ts.map +1 -0
  20. package/dist/mcp/tools/global/update.js +97 -0
  21. package/dist/mcp/tools/global/update.js.map +1 -0
  22. package/dist/mcp/tools/schemas.d.ts +55 -17
  23. package/dist/mcp/tools/schemas.d.ts.map +1 -1
  24. package/dist/mcp/tools/schemas.js +18 -0
  25. package/dist/mcp/tools/schemas.js.map +1 -1
  26. package/dist/types.d.ts +40 -1
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/utils/adminEntitySettings.d.ts +17 -0
  30. package/dist/utils/adminEntitySettings.d.ts.map +1 -0
  31. package/dist/utils/adminEntitySettings.js +41 -0
  32. package/dist/utils/adminEntitySettings.js.map +1 -0
  33. package/dist/utils/createApiKeyFields.d.ts +15 -0
  34. package/dist/utils/createApiKeyFields.d.ts.map +1 -0
  35. package/dist/utils/createApiKeyFields.js +57 -0
  36. package/dist/utils/createApiKeyFields.js.map +1 -0
  37. package/dist/utils/getEnabledSlugs.d.ts +13 -0
  38. package/dist/utils/getEnabledSlugs.d.ts.map +1 -0
  39. package/dist/utils/getEnabledSlugs.js +32 -0
  40. package/dist/utils/getEnabledSlugs.js.map +1 -0
  41. package/package.json +4 -4
  42. package/src/collections/createApiKeysCollection.ts +11 -111
  43. package/src/endpoints/mcp.ts +1 -0
  44. package/src/index.ts +4 -0
  45. package/src/mcp/getMcpHandler.ts +62 -26
  46. package/src/mcp/tools/global/find.ts +104 -0
  47. package/src/mcp/tools/global/update.ts +168 -0
  48. package/src/mcp/tools/schemas.ts +50 -0
  49. package/src/types.ts +59 -1
  50. package/src/utils/adminEntitySettings.ts +40 -0
  51. package/src/utils/createApiKeyFields.ts +72 -0
  52. package/src/utils/getEnabledSlugs.ts +42 -0
@@ -0,0 +1,104 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import type { PayloadRequest, TypedUser } from 'payload'
3
+
4
+ import type { PluginMCPServerConfig } from '../../../types.js'
5
+
6
+ import { toCamelCase } from '../../../utils/camelCase.js'
7
+ import { toolSchemas } from '../schemas.js'
8
+
9
+ export const findGlobalTool = (
10
+ server: McpServer,
11
+ req: PayloadRequest,
12
+ user: TypedUser,
13
+ verboseLogs: boolean,
14
+ globalSlug: string,
15
+ globals: PluginMCPServerConfig['globals'],
16
+ ) => {
17
+ const tool = async (
18
+ depth: number = 0,
19
+ locale?: string,
20
+ fallbackLocale?: string,
21
+ ): Promise<{
22
+ content: Array<{
23
+ text: string
24
+ type: 'text'
25
+ }>
26
+ }> => {
27
+ const payload = req.payload
28
+
29
+ if (verboseLogs) {
30
+ payload.logger.info(
31
+ `[payload-mcp] Reading global: ${globalSlug}, depth: ${depth}${locale ? `, locale: ${locale}` : ''}`,
32
+ )
33
+ }
34
+
35
+ try {
36
+ const findOptions: Parameters<typeof payload.findGlobal>[0] = {
37
+ slug: globalSlug,
38
+ depth,
39
+ user,
40
+ }
41
+
42
+ // Add locale parameters if provided
43
+ if (locale) {
44
+ findOptions.locale = locale
45
+ }
46
+ if (fallbackLocale) {
47
+ findOptions.fallbackLocale = fallbackLocale
48
+ }
49
+
50
+ const result = await payload.findGlobal(findOptions)
51
+
52
+ if (verboseLogs) {
53
+ payload.logger.info(`[payload-mcp] Found global: ${globalSlug}`)
54
+ }
55
+
56
+ const response = {
57
+ content: [
58
+ {
59
+ type: 'text' as const,
60
+ text: `Global "${globalSlug}":
61
+ \`\`\`json
62
+ ${JSON.stringify(result, null, 2)}
63
+ \`\`\``,
64
+ },
65
+ ],
66
+ }
67
+
68
+ return (globals?.[globalSlug]?.overrideResponse?.(response, result, req) || response) as {
69
+ content: Array<{
70
+ text: string
71
+ type: 'text'
72
+ }>
73
+ }
74
+ } catch (error) {
75
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
76
+ payload.logger.error(`[payload-mcp] Error reading global ${globalSlug}: ${errorMessage}`)
77
+ const response = {
78
+ content: [
79
+ {
80
+ type: 'text' as const,
81
+ text: `❌ **Error reading global "${globalSlug}":** ${errorMessage}`,
82
+ },
83
+ ],
84
+ }
85
+ return (globals?.[globalSlug]?.overrideResponse?.(response, {}, req) || response) as {
86
+ content: Array<{
87
+ text: string
88
+ type: 'text'
89
+ }>
90
+ }
91
+ }
92
+ }
93
+
94
+ if (globals?.[globalSlug]?.enabled) {
95
+ server.tool(
96
+ `find${globalSlug.charAt(0).toUpperCase() + toCamelCase(globalSlug).slice(1)}`,
97
+ `${toolSchemas.findGlobal.description.trim()}\n\n${globals?.[globalSlug]?.description || ''}`,
98
+ toolSchemas.findGlobal.parameters.shape,
99
+ async ({ depth, fallbackLocale, locale }) => {
100
+ return await tool(depth, locale, fallbackLocale)
101
+ },
102
+ )
103
+ }
104
+ }
@@ -0,0 +1,168 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import type { JSONSchema4 } from 'json-schema'
3
+ import type { PayloadRequest, TypedUser } from 'payload'
4
+
5
+ import { z } from 'zod'
6
+
7
+ import type { PluginMCPServerConfig } from '../../../types.js'
8
+
9
+ import { toCamelCase } from '../../../utils/camelCase.js'
10
+ import { convertCollectionSchemaToZod } from '../../../utils/convertCollectionSchemaToZod.js'
11
+ import { toolSchemas } from '../schemas.js'
12
+
13
+ export const updateGlobalTool = (
14
+ server: McpServer,
15
+ req: PayloadRequest,
16
+ user: TypedUser,
17
+ verboseLogs: boolean,
18
+ globalSlug: string,
19
+ globals: PluginMCPServerConfig['globals'],
20
+ schema: JSONSchema4,
21
+ ) => {
22
+ const tool = async (
23
+ data: string,
24
+ draft: boolean = false,
25
+ depth: number = 0,
26
+ locale?: string,
27
+ fallbackLocale?: string,
28
+ ): Promise<{
29
+ content: Array<{
30
+ text: string
31
+ type: 'text'
32
+ }>
33
+ }> => {
34
+ const payload = req.payload
35
+
36
+ if (verboseLogs) {
37
+ payload.logger.info(
38
+ `[payload-mcp] Updating global: ${globalSlug}, draft: ${draft}${locale ? `, locale: ${locale}` : ''}`,
39
+ )
40
+ }
41
+
42
+ try {
43
+ // Parse the data JSON
44
+ let parsedData: Record<string, unknown>
45
+ try {
46
+ parsedData = JSON.parse(data)
47
+ if (verboseLogs) {
48
+ payload.logger.info(
49
+ `[payload-mcp] Parsed data for ${globalSlug}: ${JSON.stringify(parsedData)}`,
50
+ )
51
+ }
52
+ } catch (_parseError) {
53
+ payload.logger.error(`[payload-mcp] Invalid JSON data provided: ${data}`)
54
+ const response = {
55
+ content: [{ type: 'text' as const, text: 'Error: Invalid JSON data provided' }],
56
+ }
57
+ return (globals?.[globalSlug]?.overrideResponse?.(response, {}, req) || response) as {
58
+ content: Array<{
59
+ text: string
60
+ type: 'text'
61
+ }>
62
+ }
63
+ }
64
+
65
+ const updateOptions: Parameters<typeof payload.updateGlobal>[0] = {
66
+ slug: globalSlug,
67
+ data: parsedData,
68
+ depth,
69
+ draft,
70
+ user,
71
+ }
72
+
73
+ // Add locale parameters if provided
74
+ if (locale) {
75
+ updateOptions.locale = locale
76
+ }
77
+ if (fallbackLocale) {
78
+ updateOptions.fallbackLocale = fallbackLocale
79
+ }
80
+
81
+ const result = await payload.updateGlobal(updateOptions)
82
+
83
+ if (verboseLogs) {
84
+ payload.logger.info(`[payload-mcp] Successfully updated global: ${globalSlug}`)
85
+ }
86
+
87
+ const response = {
88
+ content: [
89
+ {
90
+ type: 'text' as const,
91
+ text: `Global "${globalSlug}" updated successfully!
92
+ \`\`\`json
93
+ ${JSON.stringify(result, null, 2)}
94
+ \`\`\``,
95
+ },
96
+ ],
97
+ }
98
+
99
+ return (globals?.[globalSlug]?.overrideResponse?.(response, result, req) || response) as {
100
+ content: Array<{
101
+ text: string
102
+ type: 'text'
103
+ }>
104
+ }
105
+ } catch (error) {
106
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
107
+ payload.logger.error(`[payload-mcp] Error updating global ${globalSlug}: ${errorMessage}`)
108
+
109
+ const response = {
110
+ content: [
111
+ {
112
+ type: 'text' as const,
113
+ text: `Error updating global "${globalSlug}": ${errorMessage}`,
114
+ },
115
+ ],
116
+ }
117
+
118
+ return (globals?.[globalSlug]?.overrideResponse?.(response, {}, req) || response) as {
119
+ content: Array<{
120
+ text: string
121
+ type: 'text'
122
+ }>
123
+ }
124
+ }
125
+ }
126
+
127
+ if (globals?.[globalSlug]?.enabled) {
128
+ const convertedFields = convertCollectionSchemaToZod(schema)
129
+
130
+ // Make all fields optional for partial updates (PATCH-style)
131
+ const optionalFields = Object.fromEntries(
132
+ Object.entries(convertedFields.shape).map(([key, value]) => [key, (value as any).optional()]),
133
+ )
134
+
135
+ const updateGlobalSchema = z.object({
136
+ ...optionalFields,
137
+ depth: z.number().optional().describe('Optional: Depth of relationships to populate'),
138
+ draft: z.boolean().optional().describe('Optional: Whether to save as draft (default: false)'),
139
+ fallbackLocale: z
140
+ .string()
141
+ .optional()
142
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
143
+ locale: z
144
+ .string()
145
+ .optional()
146
+ .describe(
147
+ 'Optional: locale code to update data in (e.g., "en", "es"). Use "all" to update all locales for localized fields',
148
+ ),
149
+ })
150
+
151
+ server.tool(
152
+ `update${globalSlug.charAt(0).toUpperCase() + toCamelCase(globalSlug).slice(1)}`,
153
+ `${toolSchemas.updateGlobal.description.trim()}\n\n${globals?.[globalSlug]?.description || ''}`,
154
+ updateGlobalSchema.shape,
155
+ async (params: Record<string, unknown>) => {
156
+ const { depth, draft, fallbackLocale, locale, ...rest } = params
157
+ const data = JSON.stringify(rest)
158
+ return await tool(
159
+ data,
160
+ draft as boolean,
161
+ depth as number,
162
+ locale as string,
163
+ fallbackLocale as string,
164
+ )
165
+ },
166
+ )
167
+ }
168
+ }
@@ -1,6 +1,30 @@
1
1
  import { z } from 'zod'
2
2
 
3
3
  export const toolSchemas = {
4
+ findGlobal: {
5
+ description: 'Find a Payload global singleton configuration.',
6
+ parameters: z.object({
7
+ depth: z
8
+ .number()
9
+ .int()
10
+ .min(0)
11
+ .max(10)
12
+ .optional()
13
+ .default(0)
14
+ .describe('Depth of population for relationships'),
15
+ fallbackLocale: z
16
+ .string()
17
+ .optional()
18
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
19
+ locale: z
20
+ .string()
21
+ .optional()
22
+ .describe(
23
+ 'Optional: locale code to retrieve data in (e.g., "en", "es"). Use "all" to retrieve all locales for localized fields',
24
+ ),
25
+ }),
26
+ },
27
+
4
28
  findResources: {
5
29
  description: 'Find documents in a collection by ID or where clause using Find or FindByID.',
6
30
  parameters: z.object({
@@ -147,6 +171,32 @@ export const toolSchemas = {
147
171
  }),
148
172
  },
149
173
 
174
+ updateGlobal: {
175
+ description: 'Update a Payload global singleton configuration.',
176
+ parameters: z.object({
177
+ data: z.string().describe('JSON string containing the data to update'),
178
+ depth: z
179
+ .number()
180
+ .int()
181
+ .min(0)
182
+ .max(10)
183
+ .optional()
184
+ .default(0)
185
+ .describe('Depth of population for relationships'),
186
+ draft: z.boolean().optional().default(false).describe('Whether to update as a draft'),
187
+ fallbackLocale: z
188
+ .string()
189
+ .optional()
190
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
191
+ locale: z
192
+ .string()
193
+ .optional()
194
+ .describe(
195
+ 'Optional: locale code to update data in (e.g., "en", "es"). Use "all" to update all locales for localized fields',
196
+ ),
197
+ }),
198
+ },
199
+
150
200
  // Experimental Below This Line
151
201
  createCollection: {
152
202
  description: 'Creates a new collection with specified fields and configuration.',
package/src/types.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { CollectionConfig, CollectionSlug, PayloadRequest, TypedUser } from 'payload'
1
+ import type {
2
+ CollectionConfig,
3
+ CollectionSlug,
4
+ GlobalSlug,
5
+ PayloadRequest,
6
+ TypedUser,
7
+ } from 'payload'
2
8
  import type { z } from 'zod'
3
9
 
4
10
  import { type ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -115,6 +121,51 @@ export type PluginMCPServerConfig = {
115
121
  }
116
122
  }
117
123
  }
124
+ /**
125
+ * Set the globals that should be available as resources via MCP.
126
+ * Globals are singleton configuration objects (e.g., site settings, navigation).
127
+ * Note: Globals only support find and update operations.
128
+ */
129
+ globals?: Partial<
130
+ Record<
131
+ GlobalSlug,
132
+ {
133
+ /**
134
+ * Set the description of the global. This is used by MCP clients to determine when to use the global as a resource.
135
+ */
136
+ description?: string
137
+ /**
138
+ * Set the enabled capabilities of the global. Admins can then allow or disallow the use of the capability by MCP clients.
139
+ * Note: Globals only support find and update operations as they are singletons.
140
+ */
141
+ enabled:
142
+ | {
143
+ find?: boolean
144
+ update?: boolean
145
+ }
146
+ | boolean
147
+
148
+ /**
149
+ * Override the response generated by the MCP client. This allows you to modify the response that is sent to the MCP client. This is useful for adding additional data to the response, data normalization, or verifying data.
150
+ */
151
+ overrideResponse?: (
152
+ response: {
153
+ content: Array<{
154
+ text: string
155
+ type: string
156
+ }>
157
+ },
158
+ doc: Record<string, unknown>,
159
+ req: PayloadRequest,
160
+ ) => {
161
+ content: Array<{
162
+ text: string
163
+ type: string
164
+ }>
165
+ }
166
+ }
167
+ >
168
+ >
118
169
  /**
119
170
  * MCP Server options.
120
171
  */
@@ -349,6 +400,11 @@ export type MCPAccessSettings = {
349
400
  find?: boolean
350
401
  update?: boolean
351
402
  }
403
+ custom?: Record<string, boolean>
404
+ globals?: {
405
+ find?: boolean
406
+ update?: boolean
407
+ }
352
408
  jobs?: {
353
409
  create?: boolean
354
410
  run?: boolean
@@ -360,6 +416,8 @@ export type MCPAccessSettings = {
360
416
  user: TypedUser
361
417
  } & Record<string, unknown>
362
418
 
419
+ export type EntityConfig = PluginMCPServerConfig['collections'] | PluginMCPServerConfig['globals']
420
+
363
421
  export type FieldDefinition = {
364
422
  description?: string
365
423
  name: string
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Defines the default admin entity settings for Collections and Globals.
3
+ * This is used to create the MCP API key permission fields for the API Keys collection.
4
+ */
5
+ export const adminEntitySettings = {
6
+ collection: [
7
+ {
8
+ name: 'find',
9
+ description: (slug: string) => `Allow clients to find ${slug}.`,
10
+ label: 'Find',
11
+ },
12
+ {
13
+ name: 'create',
14
+ description: (slug: string) => `Allow clients to create ${slug}.`,
15
+ label: 'Create',
16
+ },
17
+ {
18
+ name: 'update',
19
+ description: (slug: string) => `Allow clients to update ${slug}.`,
20
+ label: 'Update',
21
+ },
22
+ {
23
+ name: 'delete',
24
+ description: (slug: string) => `Allow clients to delete ${slug}.`,
25
+ label: 'Delete',
26
+ },
27
+ ],
28
+ global: [
29
+ {
30
+ name: 'find',
31
+ description: (slug: string) => `Allow clients to find ${slug} global.`,
32
+ label: 'Find',
33
+ },
34
+ {
35
+ name: 'update',
36
+ description: (slug: string) => `Allow clients to update ${slug} global.`,
37
+ label: 'Update',
38
+ },
39
+ ],
40
+ }
@@ -0,0 +1,72 @@
1
+ import type { Field } from 'payload'
2
+
3
+ import type { EntityConfig } from '../types.js'
4
+
5
+ import { adminEntitySettings } from './adminEntitySettings.js'
6
+ import { toCamelCase } from './camelCase.js'
7
+ import { getEnabledSlugs } from './getEnabledSlugs.js'
8
+ /**
9
+ * Creates MCP API key permission fields using collections or globals.
10
+ * Generates collapsible field groups with checkboxes for each enabled operation.
11
+ *
12
+ * @param config - The collections or globals configuration object
13
+ * @param configType - The type of configuration ('collection' or 'global')
14
+ * @returns Array of fields for the MCP API Keys collection
15
+ */
16
+ export const createApiKeyFields = ({
17
+ config,
18
+ configType,
19
+ }: {
20
+ config: EntityConfig | undefined
21
+ configType: 'collection' | 'global'
22
+ }): Field[] => {
23
+ const operations = adminEntitySettings[configType]
24
+ const enabledSlugs = getEnabledSlugs(config, configType)
25
+
26
+ return enabledSlugs.map((slug) => {
27
+ const entityConfig = config?.[slug]
28
+
29
+ const enabledOperations = operations.filter((operation) => {
30
+ // If fully enabled, all operations are available
31
+ if (entityConfig?.enabled === true) {
32
+ return true
33
+ }
34
+
35
+ // If partially enabled, check if this specific operation is enabled
36
+ const enabled = entityConfig?.enabled
37
+ if (typeof enabled !== 'boolean' && enabled) {
38
+ const operationEnabled = enabled[operation.name as keyof typeof enabled]
39
+ return typeof operationEnabled === 'boolean' && operationEnabled === true
40
+ }
41
+
42
+ return false
43
+ })
44
+
45
+ // Generate checkbox fields for each enabled operation
46
+ const operationFields = enabledOperations.map((operation) => ({
47
+ name: operation.name,
48
+ type: 'checkbox' as const,
49
+ admin: {
50
+ description: operation.description(slug),
51
+ },
52
+ defaultValue: false,
53
+ label: operation.label,
54
+ }))
55
+
56
+ return {
57
+ type: 'collapsible' as const,
58
+ admin: {
59
+ position: 'sidebar' as const,
60
+ },
61
+ fields: [
62
+ {
63
+ name: toCamelCase(slug),
64
+ type: 'group' as const,
65
+ fields: operationFields,
66
+ label: configType,
67
+ },
68
+ ],
69
+ label: `${slug.charAt(0).toUpperCase() + toCamelCase(slug).slice(1)}`,
70
+ }
71
+ })
72
+ }
@@ -0,0 +1,42 @@
1
+ import type { EntityConfig } from '../types.js'
2
+
3
+ import { adminEntitySettings } from './adminEntitySettings.js'
4
+
5
+ /**
6
+ * Extracts enabled slugs from collections or globals configuration.
7
+ * A slug is considered enabled if:
8
+ * 1. enabled is set to true (fully enabled)
9
+ * 2. enabled is an object with at least one operation set to true
10
+ *
11
+ * @param config - The collections or globals configuration object
12
+ * @param configType - The type of configuration ('collection' or 'global')
13
+ * @returns Array of enabled slugs
14
+ */
15
+ export const getEnabledSlugs = (
16
+ config: EntityConfig | undefined,
17
+ configType: 'collection' | 'global',
18
+ ): string[] => {
19
+ return Object.keys(config || {}).filter((slug) => {
20
+ const entityConfig = config?.[slug]
21
+ const operations = adminEntitySettings[configType]
22
+
23
+ // Check if fully enabled (boolean true)
24
+ const fullyEnabled =
25
+ typeof entityConfig?.enabled === 'boolean' && entityConfig?.enabled === true
26
+
27
+ if (fullyEnabled) {
28
+ return true
29
+ }
30
+
31
+ // Check if partially enabled (at least one operation is enabled)
32
+ const enabled = entityConfig?.enabled
33
+ if (typeof enabled !== 'boolean' && enabled) {
34
+ return operations.some((operation) => {
35
+ const operationEnabled = enabled[operation.name as keyof typeof enabled]
36
+ return typeof operationEnabled === 'boolean' && operationEnabled === true
37
+ })
38
+ }
39
+
40
+ return false
41
+ })
42
+ }