@payloadcms/plugin-mcp 4.0.0-internal.cc18726 → 4.0.0-internal.cd47d20

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 (57) hide show
  1. package/dist/mcp/buildMcpServer.d.ts +1 -1
  2. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  3. package/dist/mcp/buildMcpServer.js +2 -2
  4. package/dist/mcp/buildMcpServer.js.map +1 -1
  5. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  6. package/dist/mcp/builtin/collections/authTools.js +4 -3
  7. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  8. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  9. package/dist/mcp/builtin/collections/createTool.js +31 -24
  10. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  11. package/dist/mcp/builtin/collections/deleteTool.js +6 -2
  12. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  13. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  14. package/dist/mcp/builtin/collections/duplicateTool.js +17 -4
  15. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  16. package/dist/mcp/builtin/collections/fileInput.d.ts +1 -1
  17. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
  18. package/dist/mcp/builtin/collections/fileInput.js +19 -54
  19. package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
  20. package/dist/mcp/builtin/collections/findTool.js +7 -3
  21. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  22. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  23. package/dist/mcp/builtin/collections/updateTool.js +25 -15
  24. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  25. package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
  26. package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
  27. package/dist/mcp/builtin/formatEntityError.js +55 -0
  28. package/dist/mcp/builtin/formatEntityError.js.map +1 -0
  29. package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
  30. package/dist/mcp/builtin/globals/updateTool.js +12 -16
  31. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  32. package/dist/stdio.js +4 -3
  33. package/dist/stdio.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/mcp/buildMcpServer.ts +3 -3
  36. package/src/mcp/builtin/collections/authTools.ts +5 -2
  37. package/src/mcp/builtin/collections/createTool.ts +52 -31
  38. package/src/mcp/builtin/collections/deleteTool.ts +2 -2
  39. package/src/mcp/builtin/collections/duplicateTool.ts +15 -3
  40. package/src/mcp/builtin/collections/fileInput.ts +17 -59
  41. package/src/mcp/builtin/collections/findTool.ts +3 -3
  42. package/src/mcp/builtin/collections/updateTool.ts +14 -14
  43. package/src/mcp/builtin/formatEntityError.ts +77 -0
  44. package/src/mcp/builtin/globals/updateTool.ts +6 -11
  45. package/src/stdio.ts +2 -2
  46. package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
  47. package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
  48. package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
  49. package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
  50. package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
  51. package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
  52. package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
  53. package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
  54. package/dist/mcp/builtin/validateEntityData.js +0 -82
  55. package/dist/mcp/builtin/validateEntityData.js.map +0 -1
  56. package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
  57. package/src/mcp/builtin/validateEntityData.ts +0 -129
package/dist/stdio.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/stdio.ts"],"sourcesContent":["import type { Config, Plugin, SanitizedConfig } from 'payload'\n\n/* eslint-disable no-console */\nimport { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport { createLocalReq, getPayload } from 'payload'\nimport { findConfig, loadEnv } from 'payload/node'\n\nimport type { SanitizedMCPPluginConfig } from './types.js'\n\nimport { getAuthorizedMCP } from './endpoint/access.js'\nimport { buildMcpServer } from './mcp/buildMcpServer.js'\nimport { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'\nimport { getPluginConfig } from './utils/getPluginConfig.js'\nimport { resolveProjectRoot } from './utils/resolveProjectRoot.js'\n\n/**\n * Starts Payload's MCP server over stdin and stdout.\n *\n * Set `PAYLOAD_MCP_AUTHORIZATION` to authenticate. In development,\n * `PAYLOAD_MCP_OVERRIDE_ACCESS=true` skips access checks.\n */\nexport const runMcpStdio = async (): Promise<void> => {\n // MCP clients may start this command from another folder. Move to the Payload\n // project so findConfig() can find the config.\n const projectRoot = resolveProjectRoot(fileURLToPath(import.meta.url))\n if (projectRoot) {\n process.chdir(projectRoot)\n }\n\n loadEnv()\n const configPath = findConfig()\n const configModule = await import(pathToFileURL(configPath).toString())\n const config = (await (configModule.default ?? configModule)) as SanitizedConfig\n\n /**\n * stdout is only for MCP messages. The spec says the server must not write\n * logs there and may write them to stderr instead.\n *\n * The 2.0 client library recovers from stray stdout logs, but that behavior is not\n * guaranteed. Move Payload logs to stderr so Payload does not add invalid data\n * to stdout. Keep options from configurable loggers. Replace pre-built loggers\n * because their output cannot be redirected.\n *\n * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio\n */\n const loggerConfig: { logger?: Config['logger'] } = config\n if (\n loggerConfig.logger &&\n typeof loggerConfig.logger === 'object' &&\n 'options' in loggerConfig.logger\n ) {\n loggerConfig.logger.destination = process.stderr\n } else {\n loggerConfig.logger = { destination: process.stderr, options: {} }\n }\n\n const payload = await getPayload({ config, cron: false, disableOnInit: true })\n\n let pluginConfig: SanitizedMCPPluginConfig\n try {\n pluginConfig = getPluginConfig({ config: payload.config })\n } catch {\n // The command also works when mcpPlugin() is not in the Payload config.\n pluginConfig = sanitizeMCPConfig({ config: payload.config, pluginConfig: {} })\n\n const fallbackPlugin: Plugin = (config) => config\n Object.assign(fallbackPlugin, {\n slug: '@payloadcms/plugin-mcp',\n sanitizedOptions: pluginConfig,\n })\n payload.config.plugins ??= []\n payload.config.plugins.push(fallbackPlugin)\n }\n\n const overrideAccessEnv = process.env.PAYLOAD_MCP_OVERRIDE_ACCESS\n let overrideAccess = false\n\n if (overrideAccessEnv === 'true') {\n overrideAccess = true\n } else if (overrideAccessEnv && overrideAccessEnv !== 'false') {\n throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS must be \"true\" or \"false\".')\n }\n\n if (overrideAccess && process.env.NODE_ENV !== 'development') {\n throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS is only available in development.')\n }\n\n const headers = new Headers()\n if (process.env.PAYLOAD_MCP_AUTHORIZATION) {\n headers.set('Authorization', process.env.PAYLOAD_MCP_AUTHORIZATION)\n }\n\n const req = await createLocalReq({ req: { headers } }, payload)\n req.payloadAPI = 'MCP' as const\n const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })\n\n const stdioServer = serveStdio(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {\n onerror: (err) => {\n // MCP messages use stdout, so write SDK errors to stderr.\n console.error('[payload-mcp] error serving MCP over stdio:', err)\n },\n })\n\n // Close the MCP server and Payload when the client disconnects or the process stops.\n let isShuttingDown = false\n const shutdown = async () => {\n if (isShuttingDown) {\n return\n }\n isShuttingDown = true\n\n try {\n await stdioServer.close()\n } catch (err) {\n console.error('[payload-mcp] error closing server:', err)\n }\n\n try {\n await payload.destroy()\n } catch (err) {\n console.error('[payload-mcp] error destroying payload:', err)\n }\n\n process.exit(0)\n }\n\n process.once('SIGINT', () => void shutdown())\n process.once('SIGTERM', () => void shutdown())\n process.stdin.once('close', () => void shutdown())\n}\n"],"names":["serveStdio","fileURLToPath","pathToFileURL","createLocalReq","getPayload","findConfig","loadEnv","getAuthorizedMCP","buildMcpServer","sanitizeMCPConfig","getPluginConfig","resolveProjectRoot","runMcpStdio","projectRoot","url","process","chdir","configPath","configModule","toString","config","default","loggerConfig","logger","destination","stderr","options","payload","cron","disableOnInit","pluginConfig","fallbackPlugin","Object","assign","slug","sanitizedOptions","plugins","push","overrideAccessEnv","env","PAYLOAD_MCP_OVERRIDE_ACCESS","overrideAccess","Error","NODE_ENV","headers","Headers","PAYLOAD_MCP_AUTHORIZATION","set","req","payloadAPI","authorizedMCP","stdioServer","onerror","err","console","error","isShuttingDown","shutdown","close","destroy","exit","once","stdin"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/stdio.ts"],"sourcesContent":["import type { Config, Plugin, SanitizedConfig } from 'payload'\n\n/* eslint-disable no-console */\nimport { serveStdio } from '@modelcontextprotocol/server/stdio'\nimport { fileURLToPath, pathToFileURL } from 'node:url'\nimport { createPayloadRequest, getPayload } from 'payload'\nimport { findConfig, loadEnv } from 'payload/node'\n\nimport type { SanitizedMCPPluginConfig } from './types.js'\n\nimport { getAuthorizedMCP } from './endpoint/access.js'\nimport { buildMcpServer } from './mcp/buildMcpServer.js'\nimport { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'\nimport { getPluginConfig } from './utils/getPluginConfig.js'\nimport { resolveProjectRoot } from './utils/resolveProjectRoot.js'\n\n/**\n * Starts Payload's MCP server over stdin and stdout.\n *\n * Set `PAYLOAD_MCP_AUTHORIZATION` to authenticate. In development,\n * `PAYLOAD_MCP_OVERRIDE_ACCESS=true` skips access checks.\n */\nexport const runMcpStdio = async (): Promise<void> => {\n // MCP clients may start this command from another folder. Move to the Payload\n // project so findConfig() can find the config.\n const projectRoot = resolveProjectRoot(fileURLToPath(import.meta.url))\n if (projectRoot) {\n process.chdir(projectRoot)\n }\n\n loadEnv()\n const configPath = findConfig()\n const configModule = await import(pathToFileURL(configPath).toString())\n const config = (await (configModule.default ?? configModule)) as SanitizedConfig\n\n /**\n * stdout is only for MCP messages. The spec says the server must not write\n * logs there and may write them to stderr instead.\n *\n * The 2.0 client library recovers from stray stdout logs, but that behavior is not\n * guaranteed. Move Payload logs to stderr so Payload does not add invalid data\n * to stdout. Keep options from configurable loggers. Replace pre-built loggers\n * because their output cannot be redirected.\n *\n * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio\n */\n const loggerConfig: { logger?: Config['logger'] } = config\n if (\n loggerConfig.logger &&\n typeof loggerConfig.logger === 'object' &&\n 'options' in loggerConfig.logger\n ) {\n loggerConfig.logger.destination = process.stderr\n } else {\n loggerConfig.logger = { destination: process.stderr, options: {} }\n }\n\n const payload = await getPayload({ config, cron: false, disableOnInit: true })\n\n let pluginConfig: SanitizedMCPPluginConfig\n try {\n pluginConfig = getPluginConfig({ config: payload.config })\n } catch {\n // The command also works when mcpPlugin() is not in the Payload config.\n pluginConfig = sanitizeMCPConfig({ config: payload.config, pluginConfig: {} })\n\n const fallbackPlugin: Plugin = (config) => config\n Object.assign(fallbackPlugin, {\n slug: '@payloadcms/plugin-mcp',\n sanitizedOptions: pluginConfig,\n })\n payload.config.plugins ??= []\n payload.config.plugins.push(fallbackPlugin)\n }\n\n const overrideAccessEnv = process.env.PAYLOAD_MCP_OVERRIDE_ACCESS\n let overrideAccess = false\n\n if (overrideAccessEnv === 'true') {\n overrideAccess = true\n } else if (overrideAccessEnv && overrideAccessEnv !== 'false') {\n throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS must be \"true\" or \"false\".')\n }\n\n if (overrideAccess && process.env.NODE_ENV !== 'development') {\n throw new Error('PAYLOAD_MCP_OVERRIDE_ACCESS is only available in development.')\n }\n\n const headers = new Headers()\n if (process.env.PAYLOAD_MCP_AUTHORIZATION) {\n headers.set('Authorization', process.env.PAYLOAD_MCP_AUTHORIZATION)\n }\n\n const req = await createPayloadRequest({ payload, req: { headers } })\n req.payloadAPI = 'MCP' as const\n const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })\n\n const stdioServer = serveStdio(() => buildMcpServer({ authorizedMCP, pluginConfig, req }), {\n onerror: (err) => {\n // MCP messages use stdout, so write SDK errors to stderr.\n console.error('[payload-mcp] error serving MCP over stdio:', err)\n },\n })\n\n // Close the MCP server and Payload when the client disconnects or the process stops.\n let isShuttingDown = false\n const shutdown = async () => {\n if (isShuttingDown) {\n return\n }\n isShuttingDown = true\n\n try {\n await stdioServer.close()\n } catch (err) {\n console.error('[payload-mcp] error closing server:', err)\n }\n\n try {\n await payload.destroy()\n } catch (err) {\n console.error('[payload-mcp] error destroying payload:', err)\n }\n\n process.exit(0)\n }\n\n process.once('SIGINT', () => void shutdown())\n process.once('SIGTERM', () => void shutdown())\n process.stdin.once('close', () => void shutdown())\n}\n"],"names":["serveStdio","fileURLToPath","pathToFileURL","createPayloadRequest","getPayload","findConfig","loadEnv","getAuthorizedMCP","buildMcpServer","sanitizeMCPConfig","getPluginConfig","resolveProjectRoot","runMcpStdio","projectRoot","url","process","chdir","configPath","configModule","toString","config","default","loggerConfig","logger","destination","stderr","options","payload","cron","disableOnInit","pluginConfig","fallbackPlugin","Object","assign","slug","sanitizedOptions","plugins","push","overrideAccessEnv","env","PAYLOAD_MCP_OVERRIDE_ACCESS","overrideAccess","Error","NODE_ENV","headers","Headers","PAYLOAD_MCP_AUTHORIZATION","set","req","payloadAPI","authorizedMCP","stdioServer","onerror","err","console","error","isShuttingDown","shutdown","close","destroy","exit","once","stdin"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payloadcms/plugin-mcp",
3
- "version": "4.0.0-internal.cc18726",
3
+ "version": "4.0.0-internal.cd47d20",
4
4
  "description": "MCP (Model Context Protocol) capabilities with Payload",
5
5
  "keywords": [
6
6
  "plugin",
@@ -60,10 +60,10 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@payloadcms/eslint-config": "3.28.0",
63
- "payload": "4.0.0-internal.cc18726"
63
+ "payload": "4.0.0-internal.cd47d20"
64
64
  },
65
65
  "peerDependencies": {
66
- "payload": "4.0.0-internal.cc18726"
66
+ "payload": "4.0.0-internal.cd47d20"
67
67
  },
68
68
  "//deps_notes": {
69
69
  "tsx": "Required by the `bin.js` stdio mcp. Keep the version pinned to whatever `payload` ships to avoid duplicate installs."
@@ -75,7 +75,7 @@
75
75
  "build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
76
76
  "build:types": "tsc --emitDeclarationOnly --outDir dist",
77
77
  "clean": "rimraf {dist,*.tsbuildinfo}",
78
- "copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
78
+ "copyfiles": "copyfiles -u 1 -e \"**/*.spec.*\" \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
79
79
  "lint": "eslint .",
80
80
  "lint:fix": "eslint . --fix",
81
81
  "pack:plugin": "pnpm build && pnpm pack"
@@ -17,8 +17,6 @@ import type {
17
17
  import { getLogger } from '../utils/getLogger.js'
18
18
  import { toStandardSchema } from '../utils/toStandardSchema.js'
19
19
 
20
- z.config(z.locales.en())
21
-
22
20
  /**
23
21
  * Serving-entry-agnostic core: registers every authorized MCP item onto a fresh
24
22
  * `McpServer` and returns it. The HTTP and stdio entry point callers provide fresh
@@ -27,7 +25,7 @@ z.config(z.locales.en())
27
25
  *
28
26
  * `req` is the request context handlers see. For HTTP it's the live
29
27
  * `PayloadRequest` derived from the incoming HTTP request; for stdio it's a
30
- * synthesized one built via `createLocalReq`.
28
+ * synthesized one built via `createPayloadRequest`.
31
29
  */
32
30
  export const buildMcpServer = ({
33
31
  authorizedMCP,
@@ -38,6 +36,8 @@ export const buildMcpServer = ({
38
36
  pluginConfig: SanitizedMCPPluginConfig
39
37
  req: PayloadRequest
40
38
  }): McpServer => {
39
+ z.config(z.locales.en())
40
+
41
41
  const serverOptions = pluginConfig.mcp?.serverOptions || {}
42
42
  const server = new McpServer(
43
43
  { name: 'Payload MCP Server', version: '1.0.0', ...serverOptions.serverInfo },
@@ -1,4 +1,4 @@
1
- import { createLocalReq, strictObject, z } from 'payload'
1
+ import { createPayloadRequest, strictObject, z } from 'payload'
2
2
 
3
3
  import type { MCPToolResponse } from '../../../types.js'
4
4
 
@@ -53,7 +53,10 @@ export const authCollectionTool = defineCollectionTool({
53
53
  if (input.headers) {
54
54
  authHeaders = new Headers(input.headers)
55
55
  }
56
- const authReq = await createLocalReq({ req: { headers: authHeaders } }, req.payload)
56
+ const authReq = await createPayloadRequest({
57
+ payload: req.payload,
58
+ req: { headers: authHeaders },
59
+ })
57
60
  const result = await req.payload.auth({ headers: authHeaders, req: authReq })
58
61
 
59
62
  if (result.user) {
@@ -1,16 +1,17 @@
1
1
  import {
2
2
  createDocumentsInputSchema,
3
3
  getCollectionVirtualFieldNames,
4
+ hasDraftValidationEnabled,
4
5
  stripVirtualFields,
5
6
  transformPointDataToPayload,
7
+ validateCollectionData,
6
8
  } from 'payload'
7
9
 
8
10
  import { defaultAccess } from '../../../defaultAccess.js'
9
11
  import { defineCollectionTool } from '../../../defineTool.js'
10
12
  import { getLogger } from '../../../utils/getLogger.js'
11
- import { validateCollectionData } from '../validateEntityData.js'
13
+ import { formatEntityError } from '../formatEntityError.js'
12
14
  import { fileInputSchema, resolveFile } from './fileInput.js'
13
- import { formatCollectionError } from './formatCollectionError.js'
14
15
 
15
16
  const DEFAULT_DESCRIPTION =
16
17
  'Create one or more documents. Each can have different data or a file. Prefer uploadReference after upload, externalURL for URLs, or base64 for small local files.'
@@ -29,43 +30,41 @@ export const createDocumentsTool = defineCollectionTool({
29
30
  input: createDocumentsInputSchema({ file: fileInputSchema }),
30
31
  }).handler(async ({ slug, authorizedMCP, input, req }) => {
31
32
  const payload = req.payload
33
+ const collectionConfig = payload.collections[slug]?.config
32
34
  const logger = getLogger({ payload })
33
- const { depth, documents, draft, fallbackLocale, locale, populate, publishAllLocales, select } =
34
- input
35
+ const {
36
+ depth,
37
+ documents,
38
+ draft,
39
+ fallbackLocale,
40
+ locale,
41
+ populate,
42
+ publishAllLocales,
43
+ returning,
44
+ select,
45
+ } = input
46
+ const shouldUsePartialSchema =
47
+ draft === true && collectionConfig !== undefined && !hasDraftValidationEnabled(collectionConfig)
35
48
 
36
49
  logger.info(`Creating ${documents.length} documents in collection: ${slug}`)
37
50
 
38
51
  try {
39
52
  const virtualFieldNames = getCollectionVirtualFieldNames(payload.config, slug)
40
- const docs: Array<{ doc: Record<string, unknown>; index: number }> = []
53
+ const docs: Array<
54
+ { doc: Record<string, unknown>; index: number } | { id: number | string; index: number }
55
+ > = []
41
56
  const errors: Array<{ index: number; message: string }> = []
42
57
  let validationSchema: Record<string, unknown> | undefined
43
58
 
44
59
  for (const [index, document] of documents.entries()) {
45
60
  try {
46
61
  const inputData = stripVirtualFields(document.data, virtualFieldNames)
47
- const validationError = validateCollectionData({ slug, data: inputData, req })
48
-
49
- if (validationError) {
50
- const firstContent = validationError.content[0]
51
- const validationContent = validationError.structuredContent as
52
- | Record<string, unknown>
53
- | undefined
54
- const schema = validationContent?.schema
55
-
56
- if (!validationSchema && schema && typeof schema === 'object') {
57
- validationSchema = schema as Record<string, unknown>
58
- }
59
-
60
- errors.push({
61
- index,
62
- message:
63
- firstContent?.type === 'text'
64
- ? (firstContent.text.split('\n\nUse this schema')[0] ?? 'Invalid document data')
65
- : 'Invalid document data',
66
- })
67
- continue
68
- }
62
+ validateCollectionData({
63
+ slug,
64
+ data: inputData,
65
+ partial: shouldUsePartialSchema,
66
+ req,
67
+ })
69
68
 
70
69
  const parsedData = transformPointDataToPayload(inputData)
71
70
  const file = await resolveFile({ slug, input: document.file, req })
@@ -81,15 +80,37 @@ export const createDocumentsTool = defineCollectionTool({
81
80
  ...(file ? { file } : {}),
82
81
  ...(locale ? { locale } : {}),
83
82
  ...(fallbackLocale !== undefined ? { fallbackLocale } : {}),
84
- ...(select ? { select } : {}),
83
+ select: returning ? select : { id: true },
85
84
  })
86
85
 
87
- docs.push({ doc: result as Record<string, unknown>, index })
86
+ docs.push(
87
+ returning ? { doc: result as Record<string, unknown>, index } : { id: result.id, index },
88
+ )
88
89
  } catch (error) {
89
90
  const message = error instanceof Error ? error.message : 'Unknown error'
91
+ const formattedError = formatEntityError({
92
+ slug,
93
+ action: 'creating',
94
+ entity: 'collection',
95
+ error,
96
+ req,
97
+ })
98
+ const structuredContent = formattedError.structuredContent as
99
+ | { schema?: unknown }
100
+ | undefined
101
+ const schema = structuredContent?.schema
102
+ const errorContent = formattedError.content[0]
103
+ const formattedMessage =
104
+ errorContent?.type === 'text'
105
+ ? (errorContent.text.split('\n\nUse this schema')[0] ?? message)
106
+ : message
107
+
108
+ if (!validationSchema && schema && typeof schema === 'object') {
109
+ validationSchema = schema as Record<string, unknown>
110
+ }
90
111
 
91
112
  logger.error(`Error creating document at index ${index} in ${slug}: ${message}`)
92
- errors.push({ index, message })
113
+ errors.push({ index, message: formattedMessage })
93
114
  }
94
115
  }
95
116
 
@@ -119,6 +140,6 @@ export const createDocumentsTool = defineCollectionTool({
119
140
  const message = error instanceof Error ? error.message : 'Unknown error'
120
141
 
121
142
  logger.error(`Error creating documents in ${slug}: ${message}`)
122
- return formatCollectionError({ slug, action: 'creating', error, req })
143
+ return formatEntityError({ slug, action: 'creating', entity: 'collection', error, req })
123
144
  }
124
145
  })
@@ -1,4 +1,4 @@
1
- import { deleteDocumentsInputSchema } from 'payload'
1
+ import { deleteDocumentsInputSchema, parseDocumentID } from 'payload'
2
2
 
3
3
  import { defaultAccess } from '../../../defaultAccess.js'
4
4
  import { defineCollectionTool } from '../../../defineTool.js'
@@ -32,7 +32,7 @@ export const deleteDocumentsTool = defineCollectionTool({
32
32
  try {
33
33
  if (id !== undefined) {
34
34
  const result = await payload.delete({
35
- id,
35
+ id: parseDocumentID({ id, collectionSlug: slug, payload }),
36
36
  collection: slug,
37
37
  depth,
38
38
  overrideAccess: authorizedMCP.overrideAccess,
@@ -1,14 +1,16 @@
1
1
  import {
2
2
  duplicateDocumentInputSchema,
3
3
  getCollectionVirtualFieldNames,
4
+ parseDocumentID,
4
5
  stripVirtualFields,
5
6
  transformPointDataToPayload,
7
+ validateCollectionData,
6
8
  } from 'payload'
7
9
 
8
10
  import { defaultAccess } from '../../../defaultAccess.js'
9
11
  import { defineCollectionTool } from '../../../defineTool.js'
10
12
  import { getLogger } from '../../../utils/getLogger.js'
11
- import { formatCollectionError } from './formatCollectionError.js'
13
+ import { formatEntityError } from '../formatEntityError.js'
12
14
 
13
15
  const DEFAULT_DESCRIPTION =
14
16
  'Duplicate a document in any collection by passing the collection slug and source document ID.'
@@ -36,10 +38,20 @@ export const duplicateDocumentTool = defineCollectionTool({
36
38
  try {
37
39
  const virtualFieldNames = getCollectionVirtualFieldNames(payload.config, slug)
38
40
  const inputData = data ? stripVirtualFields(data, virtualFieldNames) : undefined
41
+
42
+ if (inputData) {
43
+ validateCollectionData({
44
+ slug,
45
+ data: inputData,
46
+ partial: true,
47
+ req,
48
+ })
49
+ }
50
+
39
51
  const parsedData = inputData ? transformPointDataToPayload(inputData) : undefined
40
52
 
41
53
  const result = await payload.duplicate({
42
- id,
54
+ id: parseDocumentID({ id, collectionSlug: slug, payload }),
43
55
  collection: slug,
44
56
  depth,
45
57
  draft,
@@ -67,6 +79,6 @@ export const duplicateDocumentTool = defineCollectionTool({
67
79
  } catch (error) {
68
80
  const errorMessage = error instanceof Error ? error.message : 'Unknown error'
69
81
  logger.error(`Error duplicating document in ${slug}: ${errorMessage}`)
70
- return formatCollectionError({ slug, action: 'duplicating', error, req })
82
+ return formatEntityError({ slug, action: 'duplicating', entity: 'collection', error, req })
71
83
  }
72
84
  })
@@ -1,7 +1,11 @@
1
- import type { CollectionSlug, File, FileData, PayloadRequest } from 'payload'
1
+ import type { CollectionSlug, File, PayloadRequest } from 'payload'
2
2
 
3
3
  import { APIError, z } from 'payload'
4
- import { getExternalFile, getFileFromUploadInstructions, isURLAllowed } from 'payload/internal'
4
+ import {
5
+ externalURLInputSchema,
6
+ getFileFromUploadInstructions,
7
+ resolveURLUploadInput,
8
+ } from 'payload/internal'
5
9
  import { sanitizeFilename } from 'payload/shared'
6
10
 
7
11
  const mimeTypeSchema = z
@@ -30,11 +34,7 @@ export const fileInputSchema = z
30
34
  mimeType: mimeTypeSchema.check(z.describe('The file MIME type, for example image/png.')),
31
35
  source: z.literal('base64'),
32
36
  }),
33
- z.strictObject({
34
- name: z.optional(z.string().check(z.minLength(1))).check(z.describe('File name override.')),
35
- source: z.literal('externalURL'),
36
- url: z.url().check(z.describe('The http or https URL to download.')),
37
- }),
37
+ externalURLInputSchema,
38
38
  z.strictObject({
39
39
  file: uploadFileSchema.check(z.describe('getUploadInstructions file field post-upload.')),
40
40
  source: z.literal('uploadReference'),
@@ -75,6 +75,10 @@ export async function resolveFile({
75
75
  }
76
76
  }
77
77
 
78
+ if (input.source === 'externalURL') {
79
+ return resolveURLUploadInput({ slug, input, req })
80
+ }
81
+
78
82
  const uploadConfig = req.payload.collections[slug]?.config.upload
79
83
 
80
84
  if (!uploadConfig) {
@@ -82,48 +86,12 @@ export async function resolveFile({
82
86
  }
83
87
 
84
88
  const maxFileSize = req.payload.config.upload.limits?.fileSize
85
- let file: File
86
-
87
- if (input.source === 'base64') {
88
- const data = decodeBase64({ maxFileSize, value: input.data })
89
-
90
- file = {
91
- name: sanitizeFilename(input.name),
92
- data,
93
- mimetype: input.mimeType,
94
- size: data.length,
95
- }
96
- } else {
97
- if (uploadConfig.pasteURL === false) {
98
- throw new APIError(`Uploading files from URLs is disabled for collection "${slug}".`, 400)
99
- }
100
-
101
- const url = new URL(input.url)
102
-
103
- if (!['http:', 'https:'].includes(url.protocol)) {
104
- throw new APIError('File URLs must use http or https.', 400)
105
- }
106
-
107
- if (
108
- typeof uploadConfig.pasteURL === 'object' &&
109
- !isURLAllowed(input.url, uploadConfig.pasteURL.allowList)
110
- ) {
111
- throw new APIError('The provided file URL is not allowed.', 400)
112
- }
113
-
114
- file = await getExternalFile({
115
- data: {
116
- filename: sanitizeFilename(input.name || getURLFilename(url)),
117
- url: input.url,
118
- } as FileData,
119
- req,
120
- uploadConfig: {
121
- ...uploadConfig,
122
- externalFileHeaderFilter: uploadConfig.externalFileHeaderFilter ?? (() => ({})),
123
- },
124
- })
125
- file.mimetype = file.mimetype?.split(';')[0] || 'application/octet-stream'
126
- file.size = file.data.length
89
+ const data = decodeBase64({ maxFileSize, value: input.data })
90
+ const file: File = {
91
+ name: sanitizeFilename(input.name),
92
+ data,
93
+ mimetype: input.mimeType,
94
+ size: data.length,
127
95
  }
128
96
 
129
97
  if (maxFileSize !== undefined && Number.isFinite(maxFileSize) && file.size > maxFileSize) {
@@ -157,13 +125,3 @@ function decodeBase64({ maxFileSize, value }: { maxFileSize?: number; value: str
157
125
 
158
126
  return data
159
127
  }
160
-
161
- function getURLFilename(url: URL): string {
162
- const pathSegment = url.pathname.split('/').pop() || 'upload'
163
-
164
- try {
165
- return decodeURIComponent(pathSegment)
166
- } catch {
167
- return pathSegment
168
- }
169
- }
@@ -1,4 +1,4 @@
1
- import { findDocumentsInputSchema } from 'payload'
1
+ import { findDocumentsInputSchema, parseDocumentID } from 'payload'
2
2
 
3
3
  import { defaultAccess } from '../../../defaultAccess.js'
4
4
  import { defineCollectionTool } from '../../../defineTool.js'
@@ -45,10 +45,10 @@ export const findDocumentsTool = defineCollectionTool({
45
45
  )
46
46
 
47
47
  try {
48
- if (id) {
48
+ if (id !== undefined) {
49
49
  try {
50
50
  const doc = await payload.findByID({
51
- id,
51
+ id: parseDocumentID({ id, collectionSlug: slug, payload }),
52
52
  collection: slug,
53
53
  depth,
54
54
  overrideAccess: authorizedMCP.overrideAccess,
@@ -3,17 +3,18 @@ import type { Where } from 'payload'
3
3
  import {
4
4
  getCollectionInputSchema,
5
5
  getCollectionVirtualFieldNames,
6
+ parseDocumentID,
6
7
  stripVirtualFields,
7
8
  transformPointDataToPayload,
8
9
  updateDocumentInputSchema,
10
+ validateCollectionData,
9
11
  } from 'payload'
10
12
 
11
13
  import { defaultAccess } from '../../../defaultAccess.js'
12
14
  import { defineCollectionTool } from '../../../defineTool.js'
13
15
  import { getLogger } from '../../../utils/getLogger.js'
14
- import { validateCollectionData } from '../validateEntityData.js'
16
+ import { formatEntityError } from '../formatEntityError.js'
15
17
  import { fileInputSchema, resolveFile } from './fileInput.js'
16
- import { formatCollectionError } from './formatCollectionError.js'
17
18
 
18
19
  const DEFAULT_DESCRIPTION =
19
20
  'Update documents. Prefer uploadReference after upload, externalURL for URLs, or base64 for small local files.'
@@ -46,6 +47,7 @@ export const updateDocumentTool = defineCollectionTool({
46
47
  overrideLock,
47
48
  populate,
48
49
  publishAllLocales,
50
+ returning,
49
51
  select,
50
52
  sort,
51
53
  trash,
@@ -60,17 +62,13 @@ export const updateDocumentTool = defineCollectionTool({
60
62
  try {
61
63
  const virtualFieldNames = getCollectionVirtualFieldNames(payload.config, slug)
62
64
  const inputData = stripVirtualFields(data, virtualFieldNames)
63
- const validationError = validateCollectionData({
65
+ validateCollectionData({
64
66
  slug,
65
67
  data: inputData,
66
68
  partial: true,
67
69
  req,
68
70
  })
69
71
 
70
- if (validationError) {
71
- return validationError
72
- }
73
-
74
72
  const parsedData = transformPointDataToPayload(inputData)
75
73
  const file = await resolveFile({ slug, input: fileInput, req })
76
74
 
@@ -78,7 +76,7 @@ export const updateDocumentTool = defineCollectionTool({
78
76
 
79
77
  if (id !== undefined) {
80
78
  const result = await payload.update({
81
- id,
79
+ id: parseDocumentID({ id, collectionSlug: slug, payload }),
82
80
  collection: slug,
83
81
  data: parsedData,
84
82
  depth,
@@ -90,20 +88,22 @@ export const updateDocumentTool = defineCollectionTool({
90
88
  populate,
91
89
  publishAllLocales,
92
90
  req,
93
- select,
91
+ select: returning ? select : { id: true },
94
92
  trash,
95
93
  unpublishAllLocales,
96
94
  ...(file ? { file } : {}),
97
95
  })
98
96
 
97
+ const responseResult = returning ? result : { id: result.id }
98
+
99
99
  return {
100
100
  content: [
101
101
  {
102
102
  type: 'text',
103
- text: `Document updated successfully in collection "${slug}"!\nUpdated document:\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
103
+ text: `Document updated successfully in collection "${slug}"!\nResult:\n\`\`\`json\n${JSON.stringify(responseResult)}\n\`\`\``,
104
104
  },
105
105
  ],
106
- doc: result as Record<string, unknown>,
106
+ doc: responseResult as Record<string, unknown>,
107
107
  }
108
108
  }
109
109
 
@@ -120,7 +120,7 @@ export const updateDocumentTool = defineCollectionTool({
120
120
  populate,
121
121
  publishAllLocales,
122
122
  req,
123
- select,
123
+ select: returning ? select : { id: true },
124
124
  sort,
125
125
  trash,
126
126
  unpublishAllLocales,
@@ -128,7 +128,7 @@ export const updateDocumentTool = defineCollectionTool({
128
128
  ...(file ? { file } : {}),
129
129
  })
130
130
 
131
- const docs = result.docs || []
131
+ const docs = returning ? result.docs : result.docs.map(({ id }) => ({ id }))
132
132
  const errors = result.errors || []
133
133
 
134
134
  let responseText = `Multiple documents updated in collection "${slug}"!\nUpdated: ${docs.length} documents\nErrors: ${errors.length}\n---`
@@ -168,6 +168,6 @@ export const updateDocumentTool = defineCollectionTool({
168
168
  } catch (error) {
169
169
  const errorMessage = error instanceof Error ? error.message : 'Unknown error'
170
170
  logger.error(`Error updating document in ${slug}: ${errorMessage}`)
171
- return formatCollectionError({ slug, action: 'updating', error, req })
171
+ return formatEntityError({ slug, action: 'updating', entity: 'collection', error, req })
172
172
  }
173
173
  })
@@ -0,0 +1,77 @@
1
+ import type { CollectionSlug, GlobalSlug, PayloadRequest } from 'payload'
2
+
3
+ import { getCollectionInputSchema, getGlobalInputSchema, ValidationError } from 'payload'
4
+
5
+ import type { MCPToolResponse } from '../../types.js'
6
+
7
+ export const formatEntityError = ({
8
+ slug,
9
+ action,
10
+ entity,
11
+ error,
12
+ req,
13
+ }:
14
+ | {
15
+ action: 'creating' | 'duplicating' | 'updating'
16
+ entity: 'collection'
17
+ error: unknown
18
+ req: PayloadRequest
19
+ slug: CollectionSlug
20
+ }
21
+ | {
22
+ action: 'updating'
23
+ entity: 'global'
24
+ error: unknown
25
+ req: PayloadRequest
26
+ slug: GlobalSlug
27
+ }): MCPToolResponse => {
28
+ const message = error instanceof Error ? error.message : 'Unknown error'
29
+ const target = entity === 'collection' ? `document in collection "${slug}"` : `global "${slug}"`
30
+
31
+ if (!isValidationError(error, message)) {
32
+ return {
33
+ content: [{ type: 'text', text: `Error ${action} ${target}: ${message}` }],
34
+ isError: true,
35
+ }
36
+ }
37
+
38
+ const schema =
39
+ entity === 'collection'
40
+ ? getCollectionInputSchema({ collectionSlug: slug, req })
41
+ : getGlobalInputSchema({ globalSlug: slug, req })
42
+ const errors = error instanceof ValidationError ? error.data.errors : [{ message }]
43
+ const validationMessage = errors
44
+ .map((issue) => `${'path' in issue ? `${issue.path}: ` : ''}${issue.message}`)
45
+ .join('\n')
46
+ const schemaText = schema
47
+ ? `\n\nUse this schema for data:\n\`\`\`json\n${JSON.stringify(schema)}\n\`\`\``
48
+ : ''
49
+
50
+ return {
51
+ content: [
52
+ { type: 'text', text: `Error ${action} ${target}: ${validationMessage}${schemaText}` },
53
+ ],
54
+ isError: true,
55
+ ...(schema
56
+ ? {
57
+ structuredContent: {
58
+ slug,
59
+ errors,
60
+ schema,
61
+ },
62
+ }
63
+ : {}),
64
+ }
65
+ }
66
+
67
+ const isValidationError = (error: unknown, message: string): boolean => {
68
+ if (error instanceof ValidationError) {
69
+ return true
70
+ }
71
+
72
+ const name = error && typeof error === 'object' && 'name' in error ? error.name : undefined
73
+
74
+ return (
75
+ name === 'CastError' || message.includes('Cast to ') || message.includes('validation failed')
76
+ )
77
+ }
@@ -3,12 +3,13 @@ import {
3
3
  stripVirtualFields,
4
4
  transformPointDataToPayload,
5
5
  updateGlobalInputSchema,
6
+ validateGlobalData,
6
7
  } from 'payload'
7
8
 
8
9
  import { defaultAccess } from '../../../defaultAccess.js'
9
10
  import { defineGlobalTool } from '../../../defineTool.js'
10
11
  import { getLogger } from '../../../utils/getLogger.js'
11
- import { validateGlobalData } from '../validateEntityData.js'
12
+ import { formatEntityError } from '../formatEntityError.js'
12
13
 
13
14
  const DEFAULT_DESCRIPTION = 'Update any Payload global by passing the global slug and data.'
14
15
 
@@ -45,11 +46,7 @@ export const updateGlobalTool = defineGlobalTool({
45
46
  try {
46
47
  const virtualFieldNames = getGlobalVirtualFieldNames(payload.config, slug)
47
48
  const inputData = stripVirtualFields(data, virtualFieldNames)
48
- const validationError = validateGlobalData({ slug, data: inputData, req })
49
-
50
- if (validationError) {
51
- return validationError
52
- }
49
+ validateGlobalData({ slug, data: inputData, req })
53
50
 
54
51
  const parsedData = transformPointDataToPayload(inputData)
55
52
 
@@ -88,10 +85,8 @@ export const updateGlobalTool = defineGlobalTool({
88
85
  doc: result as Record<string, unknown>,
89
86
  }
90
87
  } catch (error) {
91
- const errorMessage = error instanceof Error ? error.message : 'Unknown error'
92
- logger.error(`Error updating global ${slug}: ${errorMessage}`)
93
- return {
94
- content: [{ type: 'text', text: `Error updating global "${slug}": ${errorMessage}` }],
95
- }
88
+ const message = error instanceof Error ? error.message : 'Unknown error'
89
+ logger.error(`Error updating global ${slug}: ${message}`)
90
+ return formatEntityError({ slug, action: 'updating', entity: 'global', error, req })
96
91
  }
97
92
  })