@payloadcms/plugin-mcp 3.65.0-canary.0 → 3.65.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 (39) hide show
  1. package/dist/endpoints/mcp.js.map +1 -1
  2. package/dist/mcp/getMcpHandler.d.ts.map +1 -1
  3. package/dist/mcp/getMcpHandler.js +14 -3
  4. package/dist/mcp/getMcpHandler.js.map +1 -1
  5. package/dist/mcp/helpers/fileValidation.js +22 -22
  6. package/dist/mcp/helpers/fileValidation.js.map +1 -1
  7. package/dist/mcp/tools/resource/create.d.ts.map +1 -1
  8. package/dist/mcp/tools/resource/create.js +24 -9
  9. package/dist/mcp/tools/resource/create.js.map +1 -1
  10. package/dist/mcp/tools/resource/delete.d.ts.map +1 -1
  11. package/dist/mcp/tools/resource/delete.js +11 -5
  12. package/dist/mcp/tools/resource/delete.js.map +1 -1
  13. package/dist/mcp/tools/resource/find.d.ts.map +1 -1
  14. package/dist/mcp/tools/resource/find.js +18 -6
  15. package/dist/mcp/tools/resource/find.js.map +1 -1
  16. package/dist/mcp/tools/resource/update.d.ts.map +1 -1
  17. package/dist/mcp/tools/resource/update.js +26 -8
  18. package/dist/mcp/tools/resource/update.js.map +1 -1
  19. package/dist/mcp/tools/schemas.d.ts +33 -9
  20. package/dist/mcp/tools/schemas.d.ts.map +1 -1
  21. package/dist/mcp/tools/schemas.js +21 -4
  22. package/dist/mcp/tools/schemas.js.map +1 -1
  23. package/dist/types.d.ts +39 -8
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/types.js.map +1 -1
  26. package/dist/utils/convertCollectionSchemaToZod.d.ts.map +1 -1
  27. package/dist/utils/convertCollectionSchemaToZod.js +1 -2
  28. package/dist/utils/convertCollectionSchemaToZod.js.map +1 -1
  29. package/package.json +3 -3
  30. package/src/endpoints/mcp.ts +1 -1
  31. package/src/mcp/getMcpHandler.ts +31 -4
  32. package/src/mcp/helpers/fileValidation.ts +22 -22
  33. package/src/mcp/tools/resource/create.ts +42 -7
  34. package/src/mcp/tools/resource/delete.ts +8 -4
  35. package/src/mcp/tools/resource/find.ts +10 -4
  36. package/src/mcp/tools/resource/update.ts +28 -7
  37. package/src/mcp/tools/schemas.ts +49 -3
  38. package/src/types.ts +58 -16
  39. package/src/utils/convertCollectionSchemaToZod.ts +1 -2
@@ -2,6 +2,8 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
2
  import type { JSONSchema4 } from 'json-schema'
3
3
  import type { PayloadRequest, TypedUser } from 'payload'
4
4
 
5
+ import { z } from 'zod'
6
+
5
7
  import type { PluginMCPServerConfig } from '../../../types.js'
6
8
 
7
9
  import { toCamelCase } from '../../../utils/camelCase.js'
@@ -18,6 +20,9 @@ export const createResourceTool = (
18
20
  ) => {
19
21
  const tool = async (
20
22
  data: string,
23
+ draft: boolean,
24
+ locale?: string,
25
+ fallbackLocale?: string,
21
26
  ): Promise<{
22
27
  content: Array<{
23
28
  text: string
@@ -27,7 +32,9 @@ export const createResourceTool = (
27
32
  const payload = req.payload
28
33
 
29
34
  if (verboseLogs) {
30
- payload.logger.info(`[payload-mcp] Creating resource in collection: ${collectionSlug}`)
35
+ payload.logger.info(
36
+ `[payload-mcp] Creating resource in collection: ${collectionSlug}${locale ? ` with locale: ${locale}` : ''}`,
37
+ )
31
38
  }
32
39
 
33
40
  try {
@@ -50,11 +57,13 @@ export const createResourceTool = (
50
57
  // Create the resource
51
58
  const result = await payload.create({
52
59
  collection: collectionSlug,
53
- req,
54
- // TODO: Move the override to a `beforeChange` hook and extend the payloadAPI context req to include MCP request info.
55
- data: collections?.[collectionSlug]?.override?.(parsedData, req) || parsedData,
60
+ data: parsedData,
61
+ draft,
56
62
  overrideAccess: false,
63
+ req,
57
64
  user,
65
+ ...(locale && { locale }),
66
+ ...(fallbackLocale && { fallbackLocale }),
58
67
  })
59
68
 
60
69
  if (verboseLogs) {
@@ -110,13 +119,39 @@ ${JSON.stringify(result, null, 2)}
110
119
  if (collections?.[collectionSlug]?.enabled) {
111
120
  const convertedFields = convertCollectionSchemaToZod(schema)
112
121
 
122
+ // Create a new schema that combines the converted fields with create-specific parameters
123
+ const createResourceSchema = z.object({
124
+ ...convertedFields.shape,
125
+ draft: z
126
+ .boolean()
127
+ .optional()
128
+ .default(false)
129
+ .describe('Whether to create the document as a draft'),
130
+ fallbackLocale: z
131
+ .string()
132
+ .optional()
133
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
134
+ locale: z
135
+ .string()
136
+ .optional()
137
+ .describe(
138
+ 'Optional: locale code to create the document in (e.g., "en", "es"). Defaults to the default locale',
139
+ ),
140
+ })
141
+
113
142
  server.tool(
114
143
  `create${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
115
144
  `${collections?.[collectionSlug]?.description || toolSchemas.createResource.description.trim()}`,
116
- convertedFields.shape,
145
+ createResourceSchema.shape,
117
146
  async (params: Record<string, unknown>) => {
118
- const data = JSON.stringify(params)
119
- return await tool(data)
147
+ const { draft, fallbackLocale, locale, ...fieldData } = params
148
+ const data = JSON.stringify(fieldData)
149
+ return await tool(
150
+ data,
151
+ draft as boolean,
152
+ locale as string | undefined,
153
+ fallbackLocale as string | undefined,
154
+ )
120
155
  },
121
156
  )
122
157
  }
@@ -15,9 +15,11 @@ export const deleteResourceTool = (
15
15
  collections: PluginMCPServerConfig['collections'],
16
16
  ) => {
17
17
  const tool = async (
18
- id?: string,
18
+ id?: number | string,
19
19
  where?: string,
20
20
  depth: number = 0,
21
+ locale?: string,
22
+ fallbackLocale?: string,
21
23
  ): Promise<{
22
24
  content: Array<{
23
25
  text: string
@@ -28,7 +30,7 @@ export const deleteResourceTool = (
28
30
 
29
31
  if (verboseLogs) {
30
32
  payload.logger.info(
31
- `[payload-mcp] Deleting resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}`,
33
+ `[payload-mcp] Deleting resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}${locale ? `, locale: ${locale}` : ''}`,
32
34
  )
33
35
  }
34
36
 
@@ -80,6 +82,8 @@ export const deleteResourceTool = (
80
82
  overrideAccess: false,
81
83
  req,
82
84
  user,
85
+ ...(locale && { locale }),
86
+ ...(fallbackLocale && { fallbackLocale }),
83
87
  }
84
88
 
85
89
  // Delete by ID or where clause
@@ -204,8 +208,8 @@ ${JSON.stringify(errors, null, 2)}
204
208
  `delete${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
205
209
  `${collections?.[collectionSlug]?.description || toolSchemas.deleteResource.description.trim()}`,
206
210
  toolSchemas.deleteResource.parameters.shape,
207
- async ({ id, depth, where }) => {
208
- return await tool(id, where, depth)
211
+ async ({ id, depth, fallbackLocale, locale, where }) => {
212
+ return await tool(id, where, depth, locale, fallbackLocale)
209
213
  },
210
214
  )
211
215
  }
@@ -15,11 +15,13 @@ export const findResourceTool = (
15
15
  collections: PluginMCPServerConfig['collections'],
16
16
  ) => {
17
17
  const tool = async (
18
- id?: string,
18
+ id?: number | string,
19
19
  limit: number = 10,
20
20
  page: number = 1,
21
21
  sort?: string,
22
22
  where?: string,
23
+ locale?: string,
24
+ fallbackLocale?: string,
23
25
  ): Promise<{
24
26
  content: Array<{
25
27
  text: string
@@ -30,7 +32,7 @@ export const findResourceTool = (
30
32
 
31
33
  if (verboseLogs) {
32
34
  payload.logger.info(
33
- `[payload-mcp] Reading resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ''}, limit: ${limit}, page: ${page}`,
35
+ `[payload-mcp] Reading resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ''}, limit: ${limit}, page: ${page}${locale ? `, locale: ${locale}` : ''}`,
34
36
  )
35
37
  }
36
38
 
@@ -67,6 +69,8 @@ export const findResourceTool = (
67
69
  overrideAccess: false,
68
70
  req,
69
71
  user,
72
+ ...(locale && { locale }),
73
+ ...(fallbackLocale && { fallbackLocale }),
70
74
  })
71
75
 
72
76
  if (verboseLogs) {
@@ -120,6 +124,8 @@ ${JSON.stringify(doc, null, 2)}`,
120
124
  page,
121
125
  req,
122
126
  user,
127
+ ...(locale && { locale }),
128
+ ...(fallbackLocale && { fallbackLocale }),
123
129
  }
124
130
 
125
131
  if (sort) {
@@ -190,8 +196,8 @@ Page: ${result.page} of ${result.totalPages}
190
196
  `find${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
191
197
  `${collections?.[collectionSlug]?.description || toolSchemas.findResources.description.trim()}`,
192
198
  toolSchemas.findResources.parameters.shape,
193
- async ({ id, limit, page, sort, where }) => {
194
- return await tool(id, limit, page, sort, where)
199
+ async ({ id, fallbackLocale, limit, locale, page, sort, where }) => {
200
+ return await tool(id, limit, page, sort, where, locale, fallbackLocale)
195
201
  },
196
202
  )
197
203
  }
@@ -20,13 +20,15 @@ export const updateResourceTool = (
20
20
  ) => {
21
21
  const tool = async (
22
22
  data: string,
23
- id?: string,
23
+ id?: number | string,
24
24
  where?: string,
25
25
  draft: boolean = false,
26
26
  depth: number = 0,
27
27
  overrideLock: boolean = true,
28
28
  filePath?: string,
29
29
  overwriteExistingFiles: boolean = false,
30
+ locale?: string,
31
+ fallbackLocale?: string,
30
32
  ): Promise<{
31
33
  content: Array<{
32
34
  text: string
@@ -37,7 +39,7 @@ export const updateResourceTool = (
37
39
 
38
40
  if (verboseLogs) {
39
41
  payload.logger.info(
40
- `[payload-mcp] Updating resource in collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}, draft: ${draft}`,
42
+ `[payload-mcp] Updating resource in collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}, draft: ${draft}${locale ? `, locale: ${locale}` : ''}`,
41
43
  )
42
44
  }
43
45
 
@@ -120,6 +122,8 @@ export const updateResourceTool = (
120
122
  user,
121
123
  ...(filePath && { filePath }),
122
124
  ...(overwriteExistingFiles && { overwriteExistingFiles }),
125
+ ...(locale && { locale }),
126
+ ...(fallbackLocale && { fallbackLocale }),
123
127
  }
124
128
 
125
129
  if (verboseLogs) {
@@ -127,7 +131,7 @@ export const updateResourceTool = (
127
131
  }
128
132
  const result = await payload.update({
129
133
  ...updateOptions,
130
- data: collections?.[collectionSlug]?.override?.(parsedData, req) || parsedData,
134
+ data: parsedData,
131
135
  } as any)
132
136
 
133
137
  if (verboseLogs) {
@@ -168,6 +172,8 @@ ${JSON.stringify(result, null, 2)}
168
172
  where: whereClause,
169
173
  ...(filePath && { filePath }),
170
174
  ...(overwriteExistingFiles && { overwriteExistingFiles }),
175
+ ...(locale && { locale }),
176
+ ...(fallbackLocale && { fallbackLocale }),
171
177
  }
172
178
 
173
179
  if (verboseLogs) {
@@ -175,7 +181,7 @@ ${JSON.stringify(result, null, 2)}
175
181
  }
176
182
  const result = await payload.update({
177
183
  ...updateOptions,
178
- data: collections?.[collectionSlug]?.override?.(parsedData, req) || parsedData,
184
+ data: parsedData,
179
185
  } as any)
180
186
 
181
187
  const bulkResult = result as { docs?: unknown[]; errors?: unknown[] }
@@ -255,9 +261,10 @@ ${JSON.stringify(errors, null, 2)}
255
261
  const convertedFields = convertCollectionSchemaToZod(schema)
256
262
 
257
263
  // Create a new schema that combines the converted fields with update-specific parameters
264
+ // Use .partial() to make all fields optional for partial updates
258
265
  const updateResourceSchema = z.object({
259
- ...convertedFields.shape,
260
- id: z.string().optional().describe('The ID of the document to update'),
266
+ ...convertedFields.partial().shape,
267
+ id: z.union([z.string(), z.number()]).optional().describe('The ID of the document to update'),
261
268
  depth: z
262
269
  .number()
263
270
  .optional()
@@ -268,7 +275,17 @@ ${JSON.stringify(errors, null, 2)}
268
275
  .optional()
269
276
  .default(false)
270
277
  .describe('Whether to update the document as a draft'),
278
+ fallbackLocale: z
279
+ .string()
280
+ .optional()
281
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
271
282
  filePath: z.string().optional().describe('File path for file uploads'),
283
+ locale: z
284
+ .string()
285
+ .optional()
286
+ .describe(
287
+ 'Optional: locale code to update the document in (e.g., "en", "es"). Defaults to the default locale',
288
+ ),
272
289
  overrideLock: z
273
290
  .boolean()
274
291
  .optional()
@@ -294,7 +311,9 @@ ${JSON.stringify(errors, null, 2)}
294
311
  id,
295
312
  depth,
296
313
  draft,
314
+ fallbackLocale,
297
315
  filePath,
316
+ locale,
298
317
  overrideLock,
299
318
  overwriteExistingFiles,
300
319
  where,
@@ -304,13 +323,15 @@ ${JSON.stringify(errors, null, 2)}
304
323
  const data = JSON.stringify(fieldData)
305
324
  return await tool(
306
325
  data,
307
- id as string | undefined,
326
+ id as number | string | undefined,
308
327
  where as string | undefined,
309
328
  draft as boolean,
310
329
  depth as number,
311
330
  overrideLock as boolean,
312
331
  filePath as string | undefined,
313
332
  overwriteExistingFiles as boolean,
333
+ locale as string | undefined,
334
+ fallbackLocale as string | undefined,
314
335
  )
315
336
  },
316
337
  )
@@ -5,11 +5,15 @@ export const toolSchemas = {
5
5
  description: 'Find documents in a collection by ID or where clause using Find or FindByID.',
6
6
  parameters: z.object({
7
7
  id: z
8
- .string()
8
+ .union([z.string(), z.number()])
9
9
  .optional()
10
10
  .describe(
11
11
  'Optional: specific document ID to retrieve. If not provided, returns all documents',
12
12
  ),
13
+ fallbackLocale: z
14
+ .string()
15
+ .optional()
16
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
13
17
  limit: z
14
18
  .number()
15
19
  .int()
@@ -18,6 +22,12 @@ export const toolSchemas = {
18
22
  .optional()
19
23
  .default(10)
20
24
  .describe('Maximum number of documents to return (default: 10, max: 100)'),
25
+ locale: z
26
+ .string()
27
+ .optional()
28
+ .describe(
29
+ 'Optional: locale code to retrieve data in (e.g., "en", "es"). Use "all" to retrieve all locales for localized fields',
30
+ ),
21
31
  page: z
22
32
  .number()
23
33
  .int()
@@ -47,13 +57,26 @@ export const toolSchemas = {
47
57
  .optional()
48
58
  .default(false)
49
59
  .describe('Whether to create the document as a draft'),
60
+ fallbackLocale: z
61
+ .string()
62
+ .optional()
63
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
64
+ locale: z
65
+ .string()
66
+ .optional()
67
+ .describe(
68
+ 'Optional: locale code to create the document in (e.g., "en", "es"). Defaults to the default locale',
69
+ ),
50
70
  }),
51
71
  },
52
72
 
53
73
  updateResource: {
54
74
  description: 'Update documents in a collection by ID or where clause.',
55
75
  parameters: z.object({
56
- id: z.string().optional().describe('Optional: specific document ID to update'),
76
+ id: z
77
+ .union([z.string(), z.number()])
78
+ .optional()
79
+ .describe('Optional: specific document ID to update'),
57
80
  data: z.string().describe('JSON string containing the data to update'),
58
81
  depth: z
59
82
  .number()
@@ -64,7 +87,17 @@ export const toolSchemas = {
64
87
  .default(0)
65
88
  .describe('Depth of population for relationships'),
66
89
  draft: z.boolean().optional().default(false).describe('Whether to update as a draft'),
90
+ fallbackLocale: z
91
+ .string()
92
+ .optional()
93
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
67
94
  filePath: z.string().optional().describe('Optional: absolute file path for file uploads'),
95
+ locale: z
96
+ .string()
97
+ .optional()
98
+ .describe(
99
+ 'Optional: locale code to update the document in (e.g., "en", "es"). Defaults to the default locale',
100
+ ),
68
101
  overrideLock: z
69
102
  .boolean()
70
103
  .optional()
@@ -85,7 +118,10 @@ export const toolSchemas = {
85
118
  deleteResource: {
86
119
  description: 'Delete documents in a collection by ID or where clause.',
87
120
  parameters: z.object({
88
- id: z.string().optional().describe('Optional: specific document ID to delete'),
121
+ id: z
122
+ .union([z.string(), z.number()])
123
+ .optional()
124
+ .describe('Optional: specific document ID to delete'),
89
125
  depth: z
90
126
  .number()
91
127
  .int()
@@ -94,6 +130,16 @@ export const toolSchemas = {
94
130
  .optional()
95
131
  .default(0)
96
132
  .describe('Depth of population for relationships in response'),
133
+ fallbackLocale: z
134
+ .string()
135
+ .optional()
136
+ .describe('Optional: fallback locale code to use when requested locale is not available'),
137
+ locale: z
138
+ .string()
139
+ .optional()
140
+ .describe(
141
+ 'Optional: locale code for the operation (e.g., "en", "es"). Defaults to the default locale',
142
+ ),
97
143
  where: z
98
144
  .string()
99
145
  .optional()
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CollectionConfig, CollectionSlug, PayloadRequest } from 'payload'
1
+ import type { CollectionConfig, CollectionSlug, PayloadRequest, TypedUser } from 'payload'
2
2
  import type { z } from 'zod'
3
3
 
4
4
  import { type ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -26,13 +26,6 @@ export type PluginMCPServerConfig = {
26
26
  update?: boolean
27
27
  }
28
28
  | boolean
29
- /**
30
- * Override the data generated by the MCP client. This allows you to modify the data that is sent to the MCP client. This is useful for adding additional data to the response, data normalization, or verifying data.
31
- */
32
- override?: (
33
- original: Record<string, unknown>,
34
- req: PayloadRequest,
35
- ) => Record<string, unknown>
36
29
 
37
30
  /**
38
31
  * 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.
@@ -142,7 +135,29 @@ export type PluginMCPServerConfig = {
142
135
  /**
143
136
  * Set the handler of the prompt. This is the function that will be called when the prompt is used.
144
137
  */
145
- handler: (...args: any) => any
138
+ handler: (
139
+ args: Record<string, unknown>,
140
+ req: PayloadRequest,
141
+ _extra: unknown,
142
+ ) =>
143
+ | {
144
+ messages: Array<{
145
+ content: {
146
+ text: string
147
+ type: 'text'
148
+ }
149
+ role: 'assistant' | 'user'
150
+ }>
151
+ }
152
+ | Promise<{
153
+ messages: Array<{
154
+ content: {
155
+ text: string
156
+ type: 'text'
157
+ }
158
+ role: 'assistant' | 'user'
159
+ }>
160
+ }>
146
161
  /**
147
162
  * Set the function name of the prompt.
148
163
  */
@@ -164,8 +179,21 @@ export type PluginMCPServerConfig = {
164
179
  description: string
165
180
  /**
166
181
  * Set the handler of the resource. This is the function that will be called when the resource is used.
182
+ * The handler can have either 3 arguments (when no args are passed) or 4 arguments (when args are passed).
167
183
  */
168
- handler: (...args: any) => any
184
+ handler: (...args: any[]) =>
185
+ | {
186
+ contents: Array<{
187
+ text: string
188
+ uri: string
189
+ }>
190
+ }
191
+ | Promise<{
192
+ contents: Array<{
193
+ text: string
194
+ uri: string
195
+ }>
196
+ }>
169
197
  /**
170
198
  * Set the mime type of the resource.
171
199
  * example: 'text/plain'
@@ -199,12 +227,25 @@ export type PluginMCPServerConfig = {
199
227
  /**
200
228
  * Set the handler of the tool. This is the function that will be called when the tool is used.
201
229
  */
202
- handler: (args: Record<string, unknown>) => Promise<{
203
- content: Array<{
204
- text: string
205
- type: 'text'
206
- }>
207
- }>
230
+ handler: (
231
+ args: Record<string, unknown>,
232
+ req: PayloadRequest,
233
+ _extra: unknown,
234
+ ) =>
235
+ | {
236
+ content: Array<{
237
+ text: string
238
+ type: 'text'
239
+ }>
240
+ role?: string
241
+ }
242
+ | Promise<{
243
+ content: Array<{
244
+ text: string
245
+ type: 'text'
246
+ }>
247
+ role?: string
248
+ }>
208
249
  /**
209
250
  * Set the name of the tool. This is the name that will be used to identify the tool. LLMs will interperate the name to determine when to use the tool.
210
251
  */
@@ -316,6 +357,7 @@ export type MCPAccessSettings = {
316
357
  'payload-mcp-prompt'?: Record<string, boolean>
317
358
  'payload-mcp-resource'?: Record<string, boolean>
318
359
  'payload-mcp-tool'?: Record<string, boolean>
360
+ user: TypedUser
319
361
  } & Record<string, unknown>
320
362
 
321
363
  export type FieldDefinition = {
@@ -30,6 +30,5 @@ export const convertCollectionSchemaToZod = (schema: JSONSchema4) => {
30
30
  * 5. No user input or external data is involved in the schema generation process
31
31
  */
32
32
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
33
- const zodSchema = new Function('z', `return ${transpileResult.outputText}`)(z)
34
- return zodSchema
33
+ return new Function('z', `return ${transpileResult.outputText}`)(z)
35
34
  }