@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.
- package/dist/mcp/buildMcpServer.d.ts +1 -1
- package/dist/mcp/buildMcpServer.d.ts.map +1 -1
- package/dist/mcp/buildMcpServer.js +2 -2
- package/dist/mcp/buildMcpServer.js.map +1 -1
- package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/authTools.js +4 -3
- package/dist/mcp/builtin/collections/authTools.js.map +1 -1
- package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/createTool.js +31 -24
- package/dist/mcp/builtin/collections/createTool.js.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.js +6 -2
- package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.js +17 -4
- package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.d.ts +1 -1
- package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/fileInput.js +19 -54
- package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
- package/dist/mcp/builtin/collections/findTool.js +7 -3
- package/dist/mcp/builtin/collections/findTool.js.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.js +25 -15
- package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
- package/dist/mcp/builtin/formatEntityError.d.ts +16 -0
- package/dist/mcp/builtin/formatEntityError.d.ts.map +1 -0
- package/dist/mcp/builtin/formatEntityError.js +55 -0
- package/dist/mcp/builtin/formatEntityError.js.map +1 -0
- package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.js +12 -16
- package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
- package/dist/stdio.js +4 -3
- package/dist/stdio.js.map +1 -1
- package/package.json +4 -4
- package/src/mcp/buildMcpServer.ts +3 -3
- package/src/mcp/builtin/collections/authTools.ts +5 -2
- package/src/mcp/builtin/collections/createTool.ts +52 -31
- package/src/mcp/builtin/collections/deleteTool.ts +2 -2
- package/src/mcp/builtin/collections/duplicateTool.ts +15 -3
- package/src/mcp/builtin/collections/fileInput.ts +17 -59
- package/src/mcp/builtin/collections/findTool.ts +3 -3
- package/src/mcp/builtin/collections/updateTool.ts +14 -14
- package/src/mcp/builtin/formatEntityError.ts +77 -0
- package/src/mcp/builtin/globals/updateTool.ts +6 -11
- package/src/stdio.ts +2 -2
- package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
- package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts +0 -9
- package/dist/mcp/builtin/collections/formatCollectionError.d.ts.map +0 -1
- package/dist/mcp/builtin/collections/formatCollectionError.js +0 -60
- package/dist/mcp/builtin/collections/formatCollectionError.js.map +0 -1
- package/dist/mcp/builtin/validateEntityData.d.ts +0 -14
- package/dist/mcp/builtin/validateEntityData.d.ts.map +0 -1
- package/dist/mcp/builtin/validateEntityData.js +0 -82
- package/dist/mcp/builtin/validateEntityData.js.map +0 -1
- package/src/mcp/builtin/collections/formatCollectionError.ts +0 -84
- 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 {
|
|
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.
|
|
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.
|
|
63
|
+
"payload": "4.0.0-internal.cd47d20"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"payload": "4.0.0-internal.
|
|
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 `
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
34
|
-
|
|
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<
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
83
|
+
select: returning ? select : { id: true },
|
|
85
84
|
})
|
|
86
85
|
|
|
87
|
-
docs.push(
|
|
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
|
|
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 {
|
|
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
|
|
82
|
+
return formatEntityError({ slug, action: 'duplicating', entity: 'collection', error, req })
|
|
71
83
|
}
|
|
72
84
|
})
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type { CollectionSlug, File,
|
|
1
|
+
import type { CollectionSlug, File, PayloadRequest } from 'payload'
|
|
2
2
|
|
|
3
3
|
import { APIError, z } from 'payload'
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 {
|
|
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
|
-
|
|
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}"!\
|
|
103
|
+
text: `Document updated successfully in collection "${slug}"!\nResult:\n\`\`\`json\n${JSON.stringify(responseResult)}\n\`\`\``,
|
|
104
104
|
},
|
|
105
105
|
],
|
|
106
|
-
doc:
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
92
|
-
logger.error(`Error updating global ${slug}: ${
|
|
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
|
})
|