@payloadcms/plugin-mcp 4.0.0-internal.a1317b2 → 4.0.0-internal.a2d053c
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 +23 -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 +94 -0
- package/dist/endpoint/oauth.js.map +1 -0
- package/dist/endpoint/oauthLoginToken.d.ts +21 -0
- package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
- package/dist/endpoint/oauthLoginToken.js +79 -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 +362 -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 +8 -5
- 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 +92 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/seal.d.ts +18 -0
- package/dist/utils/seal.d.ts.map +1 -0
- package/dist/utils/seal.js +27 -0
- package/dist/utils/seal.js.map +1 -0
- package/dist/utils/toStandardSchema.js.map +1 -1
- package/package.json +23 -26
- package/src/endpoint/access.ts +22 -3
- package/src/endpoint/index.ts +41 -3
- package/src/endpoint/oauth.ts +92 -0
- package/src/endpoint/oauthLoginToken.ts +91 -0
- package/src/endpoint/oauthServer.ts +396 -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 +16 -5
- 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 +94 -3
- package/src/utils/seal.ts +46 -0
- 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
|
@@ -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
|
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CallToolResult,
|
|
3
3
|
ContentBlock,
|
|
4
|
+
Implementation,
|
|
4
5
|
JsonSchemaType,
|
|
5
|
-
|
|
6
|
+
McpServerOptions,
|
|
6
7
|
ResourceTemplate,
|
|
7
8
|
ServerContext,
|
|
8
9
|
StandardSchemaWithJSON,
|
|
@@ -279,9 +280,24 @@ export type MCPPluginConfig = {
|
|
|
279
280
|
afterToolCall?: MCPAfterToolCallHook[]
|
|
280
281
|
}
|
|
281
282
|
mcp?: {
|
|
283
|
+
/**
|
|
284
|
+
* Largest HTTP request body, in bytes, that the MCP endpoint accepts. Larger requests
|
|
285
|
+
* are answered with `413`. Base64 file uploads count toward this limit.
|
|
286
|
+
*
|
|
287
|
+
* @default 4194304 (4 MiB, the MCP SDK default)
|
|
288
|
+
*/
|
|
289
|
+
maxRequestBodySize?: number
|
|
282
290
|
serverOptions?: MCPServerOptions
|
|
283
291
|
verboseLogs?: boolean
|
|
284
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* How MCP clients sign in. MCP requests without an authenticated user get a `401` that points
|
|
295
|
+
* to the protected resource metadata (RFC 9728). Set to `false` to only accept API keys and
|
|
296
|
+
* anonymous requests.
|
|
297
|
+
*
|
|
298
|
+
* @default { type: 'builtin' }
|
|
299
|
+
*/
|
|
300
|
+
oauth?: false | MCPOAuthConfig
|
|
285
301
|
/**
|
|
286
302
|
* Replace the default MCP authorization resolver.
|
|
287
303
|
*
|
|
@@ -299,13 +315,88 @@ export type MCPPluginConfig = {
|
|
|
299
315
|
tools?: MCPTopLevelToolsMap
|
|
300
316
|
}
|
|
301
317
|
|
|
318
|
+
export type MCPOAuthConfig = MCPBuiltinOAuthConfig | MCPExternalOAuthConfig
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* This Payload app is the authorization server. Users sign in to the admin panel and allow the
|
|
322
|
+
* MCP client, which then gets an access token for them.
|
|
323
|
+
*/
|
|
324
|
+
export type MCPBuiltinOAuthConfig = {
|
|
325
|
+
/**
|
|
326
|
+
* Creates the tokens the MCP client gets when the user clicks Allow. `req` is the user's own
|
|
327
|
+
* browser request, so `req.user` is the user who allowed it. Pass `authenticate` too if
|
|
328
|
+
* Payload's auth strategies don't accept the access token.
|
|
329
|
+
*
|
|
330
|
+
* @default a Payload login token for that user that only works for MCP, with a refresh token when
|
|
331
|
+
* the collection uses sessions
|
|
332
|
+
*/
|
|
333
|
+
issueToken?: (args: { req: PayloadRequest }) => MaybePromise<MCPOAuthTokens>
|
|
334
|
+
/**
|
|
335
|
+
* Trades a refresh token from `issueToken`, or from an earlier refresh, for new tokens. Return
|
|
336
|
+
* `null` when it's no longer valid, and the client sends the user through sign-in again. `req`
|
|
337
|
+
* is the MCP client's request, without a user. Only used together with `issueToken`.
|
|
338
|
+
*
|
|
339
|
+
* OAuth requires noticing copied refresh tokens: return a new refresh token every time, and
|
|
340
|
+
* revoke the tokens when an already used refresh token comes back.
|
|
341
|
+
*/
|
|
342
|
+
refreshToken?: (args: {
|
|
343
|
+
refreshToken: string
|
|
344
|
+
req: PayloadRequest
|
|
345
|
+
}) => MaybePromise<MCPOAuthTokens | null>
|
|
346
|
+
type: 'builtin'
|
|
347
|
+
} & MCPOAuthSharedConfig
|
|
348
|
+
|
|
349
|
+
export type MCPOAuthTokens = {
|
|
350
|
+
accessToken: string
|
|
351
|
+
/** Seconds until the access token expires. */
|
|
352
|
+
expiresIn?: number
|
|
353
|
+
/** Lets the client get new tokens without the user, through `refreshToken`. */
|
|
354
|
+
refreshToken?: string
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Another service, e.g. your identity provider, is the authorization server. */
|
|
358
|
+
export type MCPExternalOAuthConfig = {
|
|
359
|
+
/**
|
|
360
|
+
* The authorization servers that issue access tokens for MCP clients. Clients require each one
|
|
361
|
+
* to exactly match the `issuer` in that server's metadata, so pass a function when the exact
|
|
362
|
+
* value is only known at runtime, e.g. from the server's OpenID configuration or per host.
|
|
363
|
+
*/
|
|
364
|
+
authorizationServers: ((args: { req: PayloadRequest }) => MaybePromise<string[]>) | string[]
|
|
365
|
+
/**
|
|
366
|
+
* Scopes MCP clients should request, e.g. `['openid', 'email', 'profile']`. Without them,
|
|
367
|
+
* clients request none, which some authorization servers reject or answer with a token that
|
|
368
|
+
* doesn't identify the user.
|
|
369
|
+
*/
|
|
370
|
+
scopes?: string[]
|
|
371
|
+
type: 'external'
|
|
372
|
+
} & MCPOAuthSharedConfig
|
|
373
|
+
|
|
374
|
+
type MCPOAuthSharedConfig = {
|
|
375
|
+
/**
|
|
376
|
+
* Called on MCP requests with a bearer token that Payload's auth strategies didn't accept.
|
|
377
|
+
* Return the Payload user, including its `collection`, or `null` to answer with the OAuth challenge.
|
|
378
|
+
*/
|
|
379
|
+
authenticate?: (args: {
|
|
380
|
+
req: PayloadRequest
|
|
381
|
+
token: string
|
|
382
|
+
}) => MaybePromise<PayloadRequest['user']>
|
|
383
|
+
/**
|
|
384
|
+
* The URL clients call to reach the MCP endpoint.
|
|
385
|
+
*
|
|
386
|
+
* @default the absolute URL of `/api/mcp`, based on `serverURL` or the request URL
|
|
387
|
+
*/
|
|
388
|
+
resource?: string
|
|
389
|
+
}
|
|
390
|
+
|
|
302
391
|
export type SanitizedMCPPluginConfig = {
|
|
303
392
|
items: MCPItem[]
|
|
393
|
+
/** `undefined` when OAuth is turned off. */
|
|
394
|
+
oauth?: MCPOAuthConfig
|
|
304
395
|
} & Pick<MCPPluginConfig, 'disabled' | 'hooks' | 'mcp' | 'overrideGetAuthorizedMCP'>
|
|
305
396
|
|
|
306
397
|
export type MCPServerOptions = {
|
|
307
|
-
options?:
|
|
308
|
-
serverInfo?: Partial<
|
|
398
|
+
options?: McpServerOptions
|
|
399
|
+
serverInfo?: Partial<Implementation>
|
|
309
400
|
}
|
|
310
401
|
|
|
311
402
|
/**
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Encrypts data for the client to hold, so it doesn't need to be stored. `type` keeps one kind
|
|
5
|
+
* from being used as another, e.g. a client ID as an authorization code.
|
|
6
|
+
*/
|
|
7
|
+
export const seal = ({
|
|
8
|
+
type,
|
|
9
|
+
data,
|
|
10
|
+
expiresIn,
|
|
11
|
+
req,
|
|
12
|
+
}: {
|
|
13
|
+
data: object
|
|
14
|
+
/** Seconds */
|
|
15
|
+
expiresIn?: number
|
|
16
|
+
req: PayloadRequest
|
|
17
|
+
type: string
|
|
18
|
+
}): string =>
|
|
19
|
+
req.payload.encrypt(
|
|
20
|
+
JSON.stringify({ ...data, type, exp: expiresIn && Date.now() + expiresIn * 1000 }),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
export const unseal = <T>({
|
|
24
|
+
type,
|
|
25
|
+
req,
|
|
26
|
+
value,
|
|
27
|
+
}: {
|
|
28
|
+
req: PayloadRequest
|
|
29
|
+
type: string
|
|
30
|
+
value: null | string
|
|
31
|
+
}): T | undefined => {
|
|
32
|
+
// Only the v1 format is authenticated. Older formats decrypt anything without an error.
|
|
33
|
+
if (!value?.startsWith('v1:')) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Previous secrets can be public, like a default secret from source code, so only client IDs
|
|
39
|
+
// accept them. Client IDs grant nothing, and this keeps clients registered when the secret changes.
|
|
40
|
+
const secret = type === 'client' ? undefined : req.payload.config.secret
|
|
41
|
+
const data = JSON.parse(req.payload.decrypt(value, { secret }))
|
|
42
|
+
return data.type === type && (!data.exp || data.exp > Date.now()) ? data : undefined
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -5,5 +5,5 @@ import type { ToolInputSchema } from '../types.js'
|
|
|
5
5
|
/** Normalize a tool/prompt input to a Standard Schema. Raw JSON Schema gets wrapped; Standard Schema instances pass through. */
|
|
6
6
|
export const toStandardSchema = (schema: ToolInputSchema): StandardSchemaWithJSON =>
|
|
7
7
|
typeof schema === 'object' && schema !== null && '~standard' in schema
|
|
8
|
-
?
|
|
8
|
+
? schema
|
|
9
9
|
: fromJsonSchema(schema)
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { fileInputSchema, resolveFile } from './fileInput.js';
|
|
3
|
-
describe('MCP file input', ()=>{
|
|
4
|
-
it('should reject malformed MIME types', ()=>{
|
|
5
|
-
const result = fileInputSchema.safeParse({
|
|
6
|
-
data: 'aGVsbG8=',
|
|
7
|
-
mimeType: 'text/plain\r\nx-test: value',
|
|
8
|
-
name: 'hello.txt',
|
|
9
|
-
source: 'base64'
|
|
10
|
-
});
|
|
11
|
-
expect(result.success).toBe(false);
|
|
12
|
-
});
|
|
13
|
-
it('should accept a prepared upload', ()=>{
|
|
14
|
-
const result = fileInputSchema.safeParse({
|
|
15
|
-
file: {
|
|
16
|
-
filename: 'image.png',
|
|
17
|
-
mimeType: 'image/png',
|
|
18
|
-
size: 123,
|
|
19
|
-
uploadReference: {
|
|
20
|
-
uploadId: 'upload-id'
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
source: 'uploadReference'
|
|
24
|
-
});
|
|
25
|
-
expect(result.success).toBe(true);
|
|
26
|
-
});
|
|
27
|
-
it('should reject oversized base64 files', async ()=>{
|
|
28
|
-
const req = createRequest({
|
|
29
|
-
maxFileSize: 4
|
|
30
|
-
});
|
|
31
|
-
await expect(resolveFile({
|
|
32
|
-
slug: 'media',
|
|
33
|
-
input: {
|
|
34
|
-
data: Buffer.from('hello').toString('base64'),
|
|
35
|
-
mimeType: 'text/plain',
|
|
36
|
-
name: 'hello.txt',
|
|
37
|
-
source: 'base64'
|
|
38
|
-
},
|
|
39
|
-
req
|
|
40
|
-
})).rejects.toThrow('File exceeds the 4 byte upload limit.');
|
|
41
|
-
});
|
|
42
|
-
it('should reject URLs outside the collection allowlist', async ()=>{
|
|
43
|
-
const req = createRequest({
|
|
44
|
-
allowList: [
|
|
45
|
-
{
|
|
46
|
-
hostname: 'assets.example.com',
|
|
47
|
-
protocol: 'https'
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
});
|
|
51
|
-
await expect(resolveFile({
|
|
52
|
-
slug: 'media',
|
|
53
|
-
input: {
|
|
54
|
-
source: 'externalURL',
|
|
55
|
-
url: 'https://example.com/image.png'
|
|
56
|
-
},
|
|
57
|
-
req
|
|
58
|
-
})).rejects.toThrow('The provided file URL is not allowed.');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
function createRequest({ allowList, maxFileSize }) {
|
|
62
|
-
return {
|
|
63
|
-
payload: {
|
|
64
|
-
collections: {
|
|
65
|
-
media: {
|
|
66
|
-
config: {
|
|
67
|
-
upload: allowList ? {
|
|
68
|
-
pasteURL: {
|
|
69
|
-
allowList
|
|
70
|
-
}
|
|
71
|
-
} : {}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
config: {
|
|
76
|
-
upload: {
|
|
77
|
-
limits: {
|
|
78
|
-
fileSize: maxFileSize
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
//# sourceMappingURL=fileInput.spec.js.map
|