@payloadcms/plugin-mcp 4.0.0-internal.9496957 → 4.0.0-internal.970009e
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/bin.js +0 -7
- package/dist/endpoint/access.d.ts.map +1 -1
- package/dist/endpoint/access.js +15 -3
- package/dist/endpoint/access.js.map +1 -1
- package/dist/endpoint/index.d.ts.map +1 -1
- package/dist/endpoint/index.js +46 -4
- package/dist/endpoint/index.js.map +1 -1
- package/dist/endpoint/oauth.d.ts +39 -0
- package/dist/endpoint/oauth.d.ts.map +1 -0
- package/dist/endpoint/oauth.js +92 -0
- package/dist/endpoint/oauth.js.map +1 -0
- package/dist/endpoint/oauthLoginToken.d.ts +20 -0
- package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
- package/dist/endpoint/oauthLoginToken.js +158 -0
- package/dist/endpoint/oauthLoginToken.js.map +1 -0
- package/dist/endpoint/oauthServer.d.ts +12 -0
- package/dist/endpoint/oauthServer.d.ts.map +1 -0
- package/dist/endpoint/oauthServer.js +377 -0
- package/dist/endpoint/oauthServer.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/buildMcpServer.d.ts +1 -1
- package/dist/mcp/buildMcpServer.d.ts.map +1 -1
- package/dist/mcp/buildMcpServer.js +3 -3
- 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 +12 -6
- 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 +6 -2
- package/dist/mcp/builtin/collections/duplicateTool.js.map +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 +8 -4
- package/dist/mcp/builtin/collections/findTool.js.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +19 -3
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.js +21 -8
- package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findTool.js.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +19 -3
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.js +3 -0
- package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
- package/dist/stdio.js +4 -3
- package/dist/stdio.js.map +1 -1
- package/dist/types.d.ts +91 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/toStandardSchema.js.map +1 -1
- package/package.json +23 -26
- package/src/endpoint/access.ts +12 -3
- package/src/endpoint/index.ts +41 -3
- package/src/endpoint/oauth.ts +91 -0
- package/src/endpoint/oauthLoginToken.ts +210 -0
- package/src/endpoint/oauthServer.ts +428 -0
- package/src/index.ts +40 -5
- package/src/mcp/buildMcpServer.ts +5 -5
- package/src/mcp/builtin/collections/authTools.ts +7 -4
- package/src/mcp/builtin/collections/createTool.ts +26 -6
- package/src/mcp/builtin/collections/deleteTool.ts +3 -3
- package/src/mcp/builtin/collections/duplicateTool.ts +3 -2
- package/src/mcp/builtin/collections/fileInput.ts +17 -59
- package/src/mcp/builtin/collections/findTool.ts +4 -4
- package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +12 -1
- package/src/mcp/builtin/collections/restoreVersionTool.ts +1 -1
- package/src/mcp/builtin/collections/updateTool.ts +10 -6
- package/src/mcp/builtin/collections/uploadInstructionsTool.ts +1 -1
- package/src/mcp/builtin/globals/findTool.ts +1 -1
- package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +11 -1
- package/src/mcp/builtin/globals/updateTool.ts +1 -1
- package/src/mcp/sanitizeMCPConfig.ts +1 -0
- package/src/stdio.ts +2 -2
- package/src/types.ts +93 -3
- package/src/utils/toStandardSchema.ts +1 -1
- package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
- package/dist/mcp/builtin/collections/fileInput.spec.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { definePlugin } from 'payload'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
AuthorizedMCP,
|
|
5
|
+
MCPBuiltinOAuthConfig,
|
|
6
|
+
MCPExternalOAuthConfig,
|
|
7
|
+
MCPOAuthConfig,
|
|
8
|
+
MCPOAuthTokens,
|
|
9
|
+
MCPPluginConfig,
|
|
10
|
+
SanitizedMCPPluginConfig,
|
|
11
|
+
} from './types.js'
|
|
4
12
|
|
|
5
13
|
import { mcpEndpoint } from './endpoint/index.js'
|
|
14
|
+
import { oauthProtectedResourceEndpoint } from './endpoint/oauth.js'
|
|
15
|
+
import { oauthServerEndpoints } from './endpoint/oauthServer.js'
|
|
6
16
|
import { sanitizeMCPConfig } from './mcp/sanitizeMCPConfig.js'
|
|
7
17
|
|
|
8
18
|
declare module 'payload' {
|
|
@@ -13,11 +23,27 @@ declare module 'payload' {
|
|
|
13
23
|
/** After the plugin's `plugin` callback runs, `options` holds the sanitized config. */
|
|
14
24
|
'@payloadcms/plugin-mcp': MCPPluginConfig
|
|
15
25
|
}
|
|
26
|
+
export interface RequestContext {
|
|
27
|
+
/** Set on MCP requests before they're authenticated. */
|
|
28
|
+
mcp?: {
|
|
29
|
+
/** The MCP endpoint's address, which OAuth access tokens for MCP are issued for. */
|
|
30
|
+
resource: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
16
33
|
}
|
|
17
34
|
|
|
18
|
-
export type {
|
|
35
|
+
export type {
|
|
36
|
+
AuthorizedMCP,
|
|
37
|
+
MCPBuiltinOAuthConfig,
|
|
38
|
+
MCPExternalOAuthConfig,
|
|
39
|
+
MCPOAuthConfig,
|
|
40
|
+
MCPOAuthTokens,
|
|
41
|
+
MCPPluginConfig,
|
|
42
|
+
SanitizedMCPPluginConfig,
|
|
43
|
+
}
|
|
19
44
|
|
|
20
45
|
export { defineCollectionTool, defineGlobalTool, definePrompt, defineTool } from './defineTool.js'
|
|
46
|
+
export { oauthProtectedResourceHandler } from './endpoint/oauth.js'
|
|
21
47
|
|
|
22
48
|
export const mcpPlugin = definePlugin<MCPPluginConfig>({
|
|
23
49
|
slug: '@payloadcms/plugin-mcp',
|
|
@@ -29,7 +55,7 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
|
|
|
29
55
|
const registered = plugins['@payloadcms/plugin-mcp']
|
|
30
56
|
if (registered) {
|
|
31
57
|
// @ts-expect-error
|
|
32
|
-
registered.sanitizedOptions = pluginConfig
|
|
58
|
+
registered.sanitizedOptions = pluginConfig
|
|
33
59
|
}
|
|
34
60
|
|
|
35
61
|
if (pluginConfig.disabled) {
|
|
@@ -40,15 +66,24 @@ export const mcpPlugin = definePlugin<MCPPluginConfig>({
|
|
|
40
66
|
...config,
|
|
41
67
|
endpoints: [
|
|
42
68
|
...(config.endpoints ?? []),
|
|
43
|
-
// Payload prefixes /api, so the full path is /api/mcp.
|
|
44
|
-
|
|
69
|
+
// Payload prefixes /api, so the full path is /api/mcp. getAuthorizedMCP authenticates the
|
|
70
|
+
// request with dev auto-login turned off, so Payload doesn't authenticate it first.
|
|
71
|
+
{ disableAuth: true, handler: mcpEndpoint, method: 'post', path: '/mcp' },
|
|
45
72
|
// Streamable HTTP's optional server=>client GET stream. We don't offer
|
|
46
73
|
// one, so answer 405 per spec-— clients then skip it cleanly
|
|
47
74
|
{
|
|
75
|
+
disableAuth: true,
|
|
48
76
|
handler: () => new Response(null, { headers: { Allow: 'POST' }, status: 405 }),
|
|
49
77
|
method: 'get',
|
|
50
78
|
path: '/mcp',
|
|
51
79
|
},
|
|
80
|
+
{
|
|
81
|
+
disableAuth: true,
|
|
82
|
+
handler: oauthProtectedResourceEndpoint,
|
|
83
|
+
method: 'get',
|
|
84
|
+
path: '/mcp/oauth-protected-resource',
|
|
85
|
+
},
|
|
86
|
+
...oauthServerEndpoints,
|
|
52
87
|
],
|
|
53
88
|
}
|
|
54
89
|
},
|
|
@@ -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 },
|
|
@@ -250,7 +250,7 @@ const withSlugInput = ({ input }: { input?: ToolInputSchema }): ToolInputSchema
|
|
|
250
250
|
const slugSchema = z.string().check(z.describe(description))
|
|
251
251
|
|
|
252
252
|
if (!input) {
|
|
253
|
-
return strictObject({ slug: slugSchema })
|
|
253
|
+
return strictObject({ slug: slugSchema })
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
if (typeof input === 'object' && input !== null && '~standard' in input) {
|
|
@@ -261,7 +261,7 @@ const withSlugInput = ({ input }: { input?: ToolInputSchema }): ToolInputSchema
|
|
|
261
261
|
return input
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
-
return addSlugToJSONSchema({ description, schema
|
|
264
|
+
return addSlugToJSONSchema({ description, schema })
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
return addSlugToJSONSchema({ description, schema: input })
|
|
@@ -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) {
|
|
@@ -232,7 +235,7 @@ export const unlockCollectionTool = defineCollectionTool({
|
|
|
232
235
|
text: `# Account Unlocked\n\n**Collection:** ${slug}\n**Email:** ${input.email}\n**Result:** ${result ? 'Success' : 'Failed'}`,
|
|
233
236
|
},
|
|
234
237
|
],
|
|
235
|
-
doc: { result }
|
|
238
|
+
doc: { result },
|
|
236
239
|
}
|
|
237
240
|
} catch (error) {
|
|
238
241
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -267,7 +270,7 @@ export const verifyCollectionTool = defineCollectionTool({
|
|
|
267
270
|
text: `# Email Verification Successful\n\n**Collection:** ${slug}\n**Result:** ${result ? 'Success' : 'Failed'}`,
|
|
268
271
|
},
|
|
269
272
|
],
|
|
270
|
-
doc: { result }
|
|
273
|
+
doc: { result },
|
|
271
274
|
}
|
|
272
275
|
} catch (error) {
|
|
273
276
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createDocumentsInputSchema,
|
|
3
3
|
getCollectionVirtualFieldNames,
|
|
4
|
+
hasDraftValidationEnabled,
|
|
4
5
|
stripVirtualFields,
|
|
5
6
|
transformPointDataToPayload,
|
|
6
7
|
validateCollectionData,
|
|
@@ -29,22 +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
|
-
validateCollectionData({
|
|
62
|
+
validateCollectionData({
|
|
63
|
+
slug,
|
|
64
|
+
data: inputData,
|
|
65
|
+
partial: shouldUsePartialSchema,
|
|
66
|
+
req,
|
|
67
|
+
})
|
|
48
68
|
|
|
49
69
|
const parsedData = transformPointDataToPayload(inputData)
|
|
50
70
|
const file = await resolveFile({ slug, input: document.file, req })
|
|
@@ -60,10 +80,10 @@ export const createDocumentsTool = defineCollectionTool({
|
|
|
60
80
|
...(file ? { file } : {}),
|
|
61
81
|
...(locale ? { locale } : {}),
|
|
62
82
|
...(fallbackLocale !== undefined ? { fallbackLocale } : {}),
|
|
63
|
-
|
|
83
|
+
select: returning ? select : { id: true },
|
|
64
84
|
})
|
|
65
85
|
|
|
66
|
-
docs.push({ doc: result
|
|
86
|
+
docs.push(returning ? { doc: result, index } : { id: result.id, index })
|
|
67
87
|
} catch (error) {
|
|
68
88
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
69
89
|
const formattedError = formatEntityError({
|
|
@@ -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,
|
|
@@ -48,7 +48,7 @@ export const deleteDocumentsTool = defineCollectionTool({
|
|
|
48
48
|
text: `Document deleted successfully from collection "${slug}"!\nDeleted document:\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
49
49
|
},
|
|
50
50
|
],
|
|
51
|
-
doc: result
|
|
51
|
+
doc: result,
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
duplicateDocumentInputSchema,
|
|
3
3
|
getCollectionVirtualFieldNames,
|
|
4
|
+
parseDocumentID,
|
|
4
5
|
stripVirtualFields,
|
|
5
6
|
transformPointDataToPayload,
|
|
6
7
|
validateCollectionData,
|
|
@@ -50,7 +51,7 @@ export const duplicateDocumentTool = defineCollectionTool({
|
|
|
50
51
|
const parsedData = inputData ? transformPointDataToPayload(inputData) : undefined
|
|
51
52
|
|
|
52
53
|
const result = await payload.duplicate({
|
|
53
|
-
id,
|
|
54
|
+
id: parseDocumentID({ id, collectionSlug: slug, payload }),
|
|
54
55
|
collection: slug,
|
|
55
56
|
depth,
|
|
56
57
|
draft,
|
|
@@ -73,7 +74,7 @@ export const duplicateDocumentTool = defineCollectionTool({
|
|
|
73
74
|
text: `Document duplicated successfully in collection "${slug}"!\nDuplicated document:\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
74
75
|
},
|
|
75
76
|
],
|
|
76
|
-
doc: result
|
|
77
|
+
doc: result,
|
|
77
78
|
}
|
|
78
79
|
} catch (error) {
|
|
79
80
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -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,
|
|
@@ -69,7 +69,7 @@ export const findDocumentsTool = defineCollectionTool({
|
|
|
69
69
|
text: `Document from collection "${slug}":\n${JSON.stringify(doc)}`,
|
|
70
70
|
},
|
|
71
71
|
],
|
|
72
|
-
doc
|
|
72
|
+
doc,
|
|
73
73
|
}
|
|
74
74
|
} catch {
|
|
75
75
|
logger.warn(`Document not found with ID: ${id} in collection: ${slug}`)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAccessResults, getCollectionInputSchema, getCollectionSchemaInputSchema } from 'payload'
|
|
2
|
+
import { getLLMInstructions } from 'payload/internal'
|
|
2
3
|
|
|
3
4
|
import { defaultAccess } from '../../../defaultAccess.js'
|
|
4
5
|
import { defineCollectionTool } from '../../../defineTool.js'
|
|
@@ -16,7 +17,8 @@ export const getCollectionSchemaTool = defineCollectionTool({
|
|
|
16
17
|
readOnlyHint: true,
|
|
17
18
|
title: 'Get Collection Schema',
|
|
18
19
|
},
|
|
19
|
-
description:
|
|
20
|
+
description:
|
|
21
|
+
'Get the input schema and LLM instructions before creating or updating documents in a collection.',
|
|
20
22
|
input: getCollectionSchemaInputSchema,
|
|
21
23
|
}).handler(async ({ slug, authorizedMCP, req }) => {
|
|
22
24
|
const permissions = authorizedMCP.overrideAccess
|
|
@@ -64,17 +66,26 @@ export const getCollectionSchemaTool = defineCollectionTool({
|
|
|
64
66
|
}
|
|
65
67
|
: { enabled: false }
|
|
66
68
|
|
|
69
|
+
const instructions = await getLLMInstructions({
|
|
70
|
+
slug,
|
|
71
|
+
type: 'collection',
|
|
72
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
73
|
+
req,
|
|
74
|
+
})
|
|
75
|
+
|
|
67
76
|
return {
|
|
68
77
|
content: [
|
|
69
78
|
{
|
|
70
79
|
type: 'text',
|
|
71
80
|
text: `Schema for collection "${slug}":\n\`\`\`json\n${JSON.stringify(inputSchema)}\n\`\`\`\nUpload configuration:\n\`\`\`json\n${JSON.stringify(upload)}\n\`\`\``,
|
|
72
81
|
},
|
|
82
|
+
...(instructions ? [{ type: 'text' as const, text: instructions }] : []),
|
|
73
83
|
],
|
|
74
84
|
structuredContent: {
|
|
75
85
|
slug,
|
|
76
86
|
schema: inputSchema,
|
|
77
87
|
upload,
|
|
88
|
+
...(instructions ? { instructions } : {}),
|
|
78
89
|
},
|
|
79
90
|
}
|
|
80
91
|
})
|
|
@@ -47,7 +47,7 @@ export const restoreVersionTool = defineCollectionTool({
|
|
|
47
47
|
text: `Version "${id}" restored successfully in collection "${slug}"!\nRestored document:\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
48
48
|
},
|
|
49
49
|
],
|
|
50
|
-
doc: result
|
|
50
|
+
doc: result,
|
|
51
51
|
}
|
|
52
52
|
} catch (error) {
|
|
53
53
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -3,6 +3,7 @@ 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,
|
|
@@ -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,
|
|
@@ -74,7 +76,7 @@ export const updateDocumentTool = defineCollectionTool({
|
|
|
74
76
|
|
|
75
77
|
if (id !== undefined) {
|
|
76
78
|
const result = await payload.update({
|
|
77
|
-
id,
|
|
79
|
+
id: parseDocumentID({ id, collectionSlug: slug, payload }),
|
|
78
80
|
collection: slug,
|
|
79
81
|
data: parsedData,
|
|
80
82
|
depth,
|
|
@@ -86,20 +88,22 @@ export const updateDocumentTool = defineCollectionTool({
|
|
|
86
88
|
populate,
|
|
87
89
|
publishAllLocales,
|
|
88
90
|
req,
|
|
89
|
-
select,
|
|
91
|
+
select: returning ? select : { id: true },
|
|
90
92
|
trash,
|
|
91
93
|
unpublishAllLocales,
|
|
92
94
|
...(file ? { file } : {}),
|
|
93
95
|
})
|
|
94
96
|
|
|
97
|
+
const responseResult = returning ? result : { id: result.id }
|
|
98
|
+
|
|
95
99
|
return {
|
|
96
100
|
content: [
|
|
97
101
|
{
|
|
98
102
|
type: 'text',
|
|
99
|
-
text: `Document updated successfully in collection "${slug}"!\
|
|
103
|
+
text: `Document updated successfully in collection "${slug}"!\nResult:\n\`\`\`json\n${JSON.stringify(responseResult)}\n\`\`\``,
|
|
100
104
|
},
|
|
101
105
|
],
|
|
102
|
-
doc:
|
|
106
|
+
doc: responseResult,
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
@@ -116,7 +120,7 @@ export const updateDocumentTool = defineCollectionTool({
|
|
|
116
120
|
populate,
|
|
117
121
|
publishAllLocales,
|
|
118
122
|
req,
|
|
119
|
-
select,
|
|
123
|
+
select: returning ? select : { id: true },
|
|
120
124
|
sort,
|
|
121
125
|
trash,
|
|
122
126
|
unpublishAllLocales,
|
|
@@ -124,7 +128,7 @@ export const updateDocumentTool = defineCollectionTool({
|
|
|
124
128
|
...(file ? { file } : {}),
|
|
125
129
|
})
|
|
126
130
|
|
|
127
|
-
const docs = result.docs
|
|
131
|
+
const docs = returning ? result.docs : result.docs.map(({ id }) => ({ id }))
|
|
128
132
|
const errors = result.errors || []
|
|
129
133
|
|
|
130
134
|
let responseText = `Multiple documents updated in collection "${slug}"!\nUpdated: ${docs.length} documents\nErrors: ${errors.length}\n---`
|
|
@@ -9,7 +9,7 @@ export const getUploadInstructionsTool = defineCollectionTool({
|
|
|
9
9
|
defaultAccess(args) &&
|
|
10
10
|
Boolean(
|
|
11
11
|
args.permissions?.collections?.[args.slug]?.create ||
|
|
12
|
-
|
|
12
|
+
args.permissions?.collections?.[args.slug]?.update,
|
|
13
13
|
),
|
|
14
14
|
annotations: {
|
|
15
15
|
destructiveHint: false,
|
|
@@ -54,7 +54,7 @@ export const findGlobalTool = defineGlobalTool({
|
|
|
54
54
|
text: `Global "${slug}":\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
55
55
|
},
|
|
56
56
|
],
|
|
57
|
-
doc: result
|
|
57
|
+
doc: result,
|
|
58
58
|
}
|
|
59
59
|
} catch (error) {
|
|
60
60
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getAccessResults, getGlobalInputSchema, getGlobalSchemaInputSchema } from 'payload'
|
|
2
|
+
import { getLLMInstructions } from 'payload/internal'
|
|
2
3
|
|
|
3
4
|
import { defaultAccess } from '../../../defaultAccess.js'
|
|
4
5
|
import { defineGlobalTool } from '../../../defineTool.js'
|
|
@@ -16,7 +17,7 @@ export const getGlobalSchemaTool = defineGlobalTool({
|
|
|
16
17
|
readOnlyHint: true,
|
|
17
18
|
title: 'Get Global Schema',
|
|
18
19
|
},
|
|
19
|
-
description: 'Get the input schema
|
|
20
|
+
description: 'Get the input schema and LLM instructions before updating a global.',
|
|
20
21
|
input: getGlobalSchemaInputSchema,
|
|
21
22
|
}).handler(async ({ slug, authorizedMCP, req }) => {
|
|
22
23
|
const permissions = authorizedMCP.overrideAccess
|
|
@@ -48,16 +49,25 @@ export const getGlobalSchemaTool = defineGlobalTool({
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
const instructions = await getLLMInstructions({
|
|
53
|
+
slug,
|
|
54
|
+
type: 'global',
|
|
55
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
56
|
+
req,
|
|
57
|
+
})
|
|
58
|
+
|
|
51
59
|
return {
|
|
52
60
|
content: [
|
|
53
61
|
{
|
|
54
62
|
type: 'text',
|
|
55
63
|
text: `Schema for global "${slug}":\n\`\`\`json\n${JSON.stringify(inputSchema)}\n\`\`\``,
|
|
56
64
|
},
|
|
65
|
+
...(instructions ? [{ type: 'text' as const, text: instructions }] : []),
|
|
57
66
|
],
|
|
58
67
|
structuredContent: {
|
|
59
68
|
slug,
|
|
60
69
|
schema: inputSchema,
|
|
70
|
+
...(instructions ? { instructions } : {}),
|
|
61
71
|
},
|
|
62
72
|
}
|
|
63
73
|
})
|
|
@@ -82,7 +82,7 @@ export const updateGlobalTool = defineGlobalTool({
|
|
|
82
82
|
text: `Global "${slug}" updated successfully!\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
83
83
|
},
|
|
84
84
|
],
|
|
85
|
-
doc: result
|
|
85
|
+
doc: result,
|
|
86
86
|
}
|
|
87
87
|
} catch (error) {
|
|
88
88
|
const message = error instanceof Error ? error.message : 'Unknown error'
|
|
@@ -103,6 +103,7 @@ export const sanitizeMCPConfig = ({
|
|
|
103
103
|
hooks: pluginConfig.hooks,
|
|
104
104
|
items,
|
|
105
105
|
mcp: pluginConfig.mcp,
|
|
106
|
+
oauth: pluginConfig.oauth === false ? undefined : (pluginConfig.oauth ?? { type: 'builtin' }),
|
|
106
107
|
overrideGetAuthorizedMCP: pluginConfig.overrideGetAuthorizedMCP,
|
|
107
108
|
}
|
|
108
109
|
}
|
package/src/stdio.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Config, Plugin, SanitizedConfig } from 'payload'
|
|
|
3
3
|
/* eslint-disable no-console */
|
|
4
4
|
import { serveStdio } from '@modelcontextprotocol/server/stdio'
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
-
import {
|
|
6
|
+
import { createPayloadRequest, getPayload } from 'payload'
|
|
7
7
|
import { findConfig, loadEnv } from 'payload/node'
|
|
8
8
|
|
|
9
9
|
import type { SanitizedMCPPluginConfig } from './types.js'
|
|
@@ -91,7 +91,7 @@ export const runMcpStdio = async (): Promise<void> => {
|
|
|
91
91
|
headers.set('Authorization', process.env.PAYLOAD_MCP_AUTHORIZATION)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const req = await
|
|
94
|
+
const req = await createPayloadRequest({ payload, req: { headers } })
|
|
95
95
|
req.payloadAPI = 'MCP' as const
|
|
96
96
|
const authorizedMCP = await getAuthorizedMCP({ overrideAccess, req })
|
|
97
97
|
|