@payloadcms/plugin-mcp 4.0.0-internal.a0ef1b8 → 4.0.0-internal.a1317b2
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/LICENSE.md +1 -1
- package/bin.js +7 -0
- package/dist/defaultAccess.d.ts +3 -0
- package/dist/defaultAccess.d.ts.map +1 -0
- package/dist/defaultAccess.js +3 -0
- package/dist/defaultAccess.js.map +1 -0
- package/dist/defineTool.d.ts +8 -8
- package/dist/defineTool.d.ts.map +1 -1
- package/dist/endpoint/access.d.ts +12 -5
- package/dist/endpoint/access.d.ts.map +1 -1
- package/dist/endpoint/access.js +75 -88
- package/dist/endpoint/access.js.map +1 -1
- package/dist/endpoint/index.d.ts +1 -1
- package/dist/endpoint/index.d.ts.map +1 -1
- package/dist/endpoint/index.js +68 -17
- package/dist/endpoint/index.js.map +1 -1
- package/dist/exports/client.d.ts +1 -1
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +1 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/internal.d.ts +2 -0
- package/dist/exports/internal.d.ts.map +1 -0
- package/dist/exports/internal.js +3 -0
- package/dist/exports/internal.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -20
- package/dist/index.js.map +1 -1
- package/dist/mcp/buildMcpServer.d.ts +5 -5
- package/dist/mcp/buildMcpServer.d.ts.map +1 -1
- package/dist/mcp/buildMcpServer.js +145 -80
- 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 +122 -61
- package/dist/mcp/builtin/collections/authTools.js.map +1 -1
- package/dist/mcp/builtin/collections/countTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/countTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/countTool.js +63 -0
- package/dist/mcp/builtin/collections/countTool.js.map +1 -0
- package/dist/mcp/builtin/collections/countVersionsTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/countVersionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/countVersionsTool.js +60 -0
- package/dist/mcp/builtin/collections/countVersionsTool.js.map +1 -0
- package/dist/mcp/builtin/collections/createTool.d.ts +1 -1
- package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/createTool.js +111 -68
- package/dist/mcp/builtin/collections/createTool.js.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.d.ts +1 -1
- package/dist/mcp/builtin/collections/deleteTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/deleteTool.js +51 -61
- package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
- package/dist/mcp/builtin/collections/duplicateTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/duplicateTool.js +86 -0
- package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -0
- package/dist/mcp/builtin/collections/fileInput.d.ts +28 -0
- package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/fileInput.js +123 -0
- package/dist/mcp/builtin/collections/fileInput.js.map +1 -0
- package/dist/mcp/builtin/collections/fileInput.spec.js +86 -0
- package/dist/mcp/builtin/collections/fileInput.spec.js.map +1 -0
- package/dist/mcp/builtin/collections/findDistinctTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/findDistinctTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/findDistinctTool.js +77 -0
- package/dist/mcp/builtin/collections/findDistinctTool.js.map +1 -0
- package/dist/mcp/builtin/collections/findTool.d.ts +1 -1
- package/dist/mcp/builtin/collections/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/findTool.js +55 -72
- package/dist/mcp/builtin/collections/findTool.js.map +1 -1
- package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/findVersionByIDTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/findVersionByIDTool.js +74 -0
- package/dist/mcp/builtin/collections/findVersionByIDTool.js.map +1 -0
- package/dist/mcp/builtin/collections/findVersionsTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/findVersionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/findVersionsTool.js +84 -0
- package/dist/mcp/builtin/collections/findVersionsTool.js.map +1 -0
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +87 -0
- package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -0
- package/dist/mcp/builtin/collections/restoreVersionTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/restoreVersionTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/restoreVersionTool.js +69 -0
- package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -0
- package/dist/mcp/builtin/collections/updateTool.d.ts +1 -1
- package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/collections/updateTool.js +104 -136
- package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
- package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts +2 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js +55 -0
- package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -0
- 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/getConfigInfoTool.d.ts +2 -0
- package/dist/mcp/builtin/getConfigInfoTool.d.ts.map +1 -0
- package/dist/mcp/builtin/getConfigInfoTool.js +69 -0
- package/dist/mcp/builtin/getConfigInfoTool.js.map +1 -0
- package/dist/mcp/builtin/globals/countVersionsTool.d.ts +2 -0
- package/dist/mcp/builtin/globals/countVersionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/globals/countVersionsTool.js +60 -0
- package/dist/mcp/builtin/globals/countVersionsTool.js.map +1 -0
- package/dist/mcp/builtin/globals/findTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/findTool.js +27 -36
- package/dist/mcp/builtin/globals/findTool.js.map +1 -1
- package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts +2 -0
- package/dist/mcp/builtin/globals/findVersionByIDTool.d.ts.map +1 -0
- package/dist/mcp/builtin/globals/findVersionByIDTool.js +68 -0
- package/dist/mcp/builtin/globals/findVersionByIDTool.js.map +1 -0
- package/dist/mcp/builtin/globals/findVersionsTool.d.ts +2 -0
- package/dist/mcp/builtin/globals/findVersionsTool.d.ts.map +1 -0
- package/dist/mcp/builtin/globals/findVersionsTool.js +78 -0
- package/dist/mcp/builtin/globals/findVersionsTool.js.map +1 -0
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts +2 -0
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -0
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +65 -0
- package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -0
- package/dist/mcp/builtin/globals/restoreVersionTool.d.ts +2 -0
- package/dist/mcp/builtin/globals/restoreVersionTool.d.ts.map +1 -0
- package/dist/mcp/builtin/globals/restoreVersionTool.js +70 -0
- package/dist/mcp/builtin/globals/restoreVersionTool.js.map +1 -0
- package/dist/mcp/builtin/globals/updateTool.d.ts.map +1 -1
- package/dist/mcp/builtin/globals/updateTool.js +44 -62
- package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
- package/dist/mcp/builtinTools.d.ts +150 -19
- package/dist/mcp/builtinTools.d.ts.map +1 -1
- package/dist/mcp/builtinTools.js +125 -20
- package/dist/mcp/builtinTools.js.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.d.ts +0 -1
- package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
- package/dist/mcp/sanitizeMCPConfig.js +124 -58
- package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
- package/dist/stdio.d.ts +3 -3
- package/dist/stdio.d.ts.map +1 -1
- package/dist/stdio.js +84 -46
- package/dist/stdio.js.map +1 -1
- package/dist/types.d.ts +114 -112
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -1
- package/dist/utils/camelCase.d.ts.map +1 -1
- package/dist/utils/getPluginConfig.d.ts +1 -1
- package/dist/utils/getPluginConfig.js +1 -1
- package/dist/utils/getPluginConfig.js.map +1 -1
- package/dist/utils/resolveProjectRoot.d.ts.map +1 -1
- package/dist/utils/toStandardSchema.d.ts.map +1 -1
- package/package.json +11 -18
- package/src/defaultAccess.ts +3 -0
- package/src/endpoint/access.ts +80 -101
- package/src/endpoint/index.ts +58 -16
- package/src/exports/client.ts +2 -1
- package/src/exports/internal.ts +1 -0
- package/src/index.ts +12 -20
- package/src/mcp/buildMcpServer.ts +179 -114
- package/src/mcp/builtin/collections/authTools.ts +119 -75
- package/src/mcp/builtin/collections/countTool.ts +61 -0
- package/src/mcp/builtin/collections/countVersionsTool.ts +60 -0
- package/src/mcp/builtin/collections/createTool.ts +104 -88
- package/src/mcp/builtin/collections/deleteTool.ts +48 -74
- package/src/mcp/builtin/collections/duplicateTool.ts +83 -0
- package/src/mcp/builtin/collections/fileInput.spec.ts +93 -0
- package/src/mcp/builtin/collections/fileInput.ts +169 -0
- package/src/mcp/builtin/collections/findDistinctTool.ts +67 -0
- package/src/mcp/builtin/collections/findTool.ts +55 -108
- package/src/mcp/builtin/collections/findVersionByIDTool.ts +66 -0
- package/src/mcp/builtin/collections/findVersionsTool.ts +83 -0
- package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +80 -0
- package/src/mcp/builtin/collections/restoreVersionTool.ts +65 -0
- package/src/mcp/builtin/collections/updateTool.ts +101 -142
- package/src/mcp/builtin/collections/uploadInstructionsTool.ts +64 -0
- package/src/mcp/builtin/formatEntityError.ts +77 -0
- package/src/mcp/builtin/getConfigInfoTool.ts +68 -0
- package/src/mcp/builtin/globals/countVersionsTool.ts +60 -0
- package/src/mcp/builtin/globals/findTool.ts +27 -57
- package/src/mcp/builtin/globals/findVersionByIDTool.ts +64 -0
- package/src/mcp/builtin/globals/findVersionsTool.ts +69 -0
- package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +63 -0
- package/src/mcp/builtin/globals/restoreVersionTool.ts +68 -0
- package/src/mcp/builtin/globals/updateTool.ts +51 -79
- package/src/mcp/builtinTools.ts +118 -41
- package/src/mcp/sanitizeMCPConfig.ts +120 -78
- package/src/stdio.ts +79 -46
- package/src/types.ts +130 -117
- package/src/utils/getPluginConfig.ts +1 -1
- package/dist/@types/assets.d.js +0 -2
- package/dist/@types/assets.d.js.map +0 -1
- package/dist/collection/getAccessField.d.ts +0 -12
- package/dist/collection/getAccessField.d.ts.map +0 -1
- package/dist/collection/getAccessField.js +0 -57
- package/dist/collection/getAccessField.js.map +0 -1
- package/dist/collection/index.d.ts +0 -6
- package/dist/collection/index.d.ts.map +0 -1
- package/dist/collection/index.js +0 -60
- package/dist/collection/index.js.map +0 -1
- package/dist/components/AccessField/index.client.d.ts +0 -10
- package/dist/components/AccessField/index.client.d.ts.map +0 -1
- package/dist/components/AccessField/index.client.js +0 -305
- package/dist/components/AccessField/index.client.js.map +0 -1
- package/dist/components/AccessField/index.css +0 -93
- package/dist/utils/getVirtualFieldNames.d.ts +0 -14
- package/dist/utils/getVirtualFieldNames.d.ts.map +0 -1
- package/dist/utils/getVirtualFieldNames.js +0 -35
- package/dist/utils/getVirtualFieldNames.js.map +0 -1
- package/dist/utils/localAPIDefaults.d.ts +0 -20
- package/dist/utils/localAPIDefaults.d.ts.map +0 -1
- package/dist/utils/localAPIDefaults.js +0 -19
- package/dist/utils/localAPIDefaults.js.map +0 -1
- package/dist/utils/schemaConversion/buildToolInput.d.ts +0 -29
- package/dist/utils/schemaConversion/buildToolInput.d.ts.map +0 -1
- package/dist/utils/schemaConversion/buildToolInput.js +0 -51
- package/dist/utils/schemaConversion/buildToolInput.js.map +0 -1
- package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts +0 -7
- package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js +0 -20
- package/dist/utils/schemaConversion/removeVirtualFieldsFromSchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts +0 -15
- package/dist/utils/schemaConversion/sanitizeEntitySchema.d.ts.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js +0 -464
- package/dist/utils/schemaConversion/sanitizeEntitySchema.js.map +0 -1
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js +0 -158
- package/dist/utils/schemaConversion/sanitizeEntitySchema.spec.js.map +0 -1
- package/dist/utils/transformPointDataToPayload.d.ts +0 -7
- package/dist/utils/transformPointDataToPayload.d.ts.map +0 -1
- package/dist/utils/transformPointDataToPayload.js +0 -28
- package/dist/utils/transformPointDataToPayload.js.map +0 -1
- package/src/@types/assets.d.ts +0 -3
- package/src/collection/getAccessField.ts +0 -64
- package/src/collection/index.ts +0 -64
- package/src/components/AccessField/index.client.tsx +0 -344
- package/src/components/AccessField/index.css +0 -93
- package/src/utils/getVirtualFieldNames.ts +0 -53
- package/src/utils/localAPIDefaults.ts +0 -22
- package/src/utils/schemaConversion/buildToolInput.ts +0 -68
- package/src/utils/schemaConversion/removeVirtualFieldsFromSchema.ts +0 -27
- package/src/utils/schemaConversion/sanitizeEntitySchema.spec.ts +0 -103
- package/src/utils/schemaConversion/sanitizeEntitySchema.ts +0 -529
- package/src/utils/transformPointDataToPayload.ts +0 -40
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { CollectionSlug, File, FileData, PayloadRequest } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { APIError, z } from 'payload'
|
|
4
|
+
import { getExternalFile, getFileFromUploadInstructions, isURLAllowed } from 'payload/internal'
|
|
5
|
+
import { sanitizeFilename } from 'payload/shared'
|
|
6
|
+
|
|
7
|
+
const mimeTypeSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.check(
|
|
10
|
+
z.regex(
|
|
11
|
+
/^[!#$%&'*+.^`|~\w-]+\/[!#$%&'*+.^`|~\w-]+$/,
|
|
12
|
+
'MIME type must use the type/subtype format',
|
|
13
|
+
),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const uploadFileSchema = z.strictObject({
|
|
17
|
+
filename: z.string(),
|
|
18
|
+
mimeType: z.string(),
|
|
19
|
+
size: z.int().check(z.nonnegative()),
|
|
20
|
+
uploadReference: z.record(z.string(), z.unknown()),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const fileInputSchema = z
|
|
24
|
+
.discriminatedUnion('source', [
|
|
25
|
+
z.strictObject({
|
|
26
|
+
name: z.string().check(z.minLength(1), z.describe('The file name, including its extension.')),
|
|
27
|
+
data: z
|
|
28
|
+
.string()
|
|
29
|
+
.check(z.describe('The base64-encoded file bytes, without a data URL prefix.')),
|
|
30
|
+
mimeType: mimeTypeSchema.check(z.describe('The file MIME type, for example image/png.')),
|
|
31
|
+
source: z.literal('base64'),
|
|
32
|
+
}),
|
|
33
|
+
z.strictObject({
|
|
34
|
+
name: z.optional(z.string().check(z.minLength(1))).check(z.describe('File name override.')),
|
|
35
|
+
source: z.literal('externalURL'),
|
|
36
|
+
url: z.url().check(z.describe('The http or https URL to download.')),
|
|
37
|
+
}),
|
|
38
|
+
z.strictObject({
|
|
39
|
+
file: uploadFileSchema.check(z.describe('getUploadInstructions file field post-upload.')),
|
|
40
|
+
source: z.literal('uploadReference'),
|
|
41
|
+
}),
|
|
42
|
+
])
|
|
43
|
+
.check(
|
|
44
|
+
z.describe(
|
|
45
|
+
'A file for an upload collection. Prefer uploadReference after its upload succeeds; use base64 only for small local files or externalURL for an online file.',
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
type FileInput = z.infer<typeof fileInputSchema>
|
|
50
|
+
|
|
51
|
+
export async function resolveFile({
|
|
52
|
+
slug,
|
|
53
|
+
input,
|
|
54
|
+
req,
|
|
55
|
+
}: {
|
|
56
|
+
input?: FileInput
|
|
57
|
+
req: PayloadRequest
|
|
58
|
+
slug: CollectionSlug
|
|
59
|
+
}): Promise<File | undefined> {
|
|
60
|
+
if (!input) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (input.source === 'uploadReference') {
|
|
65
|
+
try {
|
|
66
|
+
return await getFileFromUploadInstructions({ collectionSlug: slug, file: input.file, req })
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof Error && error.message === 'Staged upload was not found.') {
|
|
69
|
+
throw new APIError(
|
|
70
|
+
'Staged upload not found. Complete the upload action first, or use base64 for small local files.',
|
|
71
|
+
400,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const uploadConfig = req.payload.collections[slug]?.config.upload
|
|
79
|
+
|
|
80
|
+
if (!uploadConfig) {
|
|
81
|
+
throw new APIError(`Collection "${slug}" does not support file uploads.`, 400)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const maxFileSize = req.payload.config.upload.limits?.fileSize
|
|
85
|
+
let file: File
|
|
86
|
+
|
|
87
|
+
if (input.source === 'base64') {
|
|
88
|
+
const data = decodeBase64({ maxFileSize, value: input.data })
|
|
89
|
+
|
|
90
|
+
file = {
|
|
91
|
+
name: sanitizeFilename(input.name),
|
|
92
|
+
data,
|
|
93
|
+
mimetype: input.mimeType,
|
|
94
|
+
size: data.length,
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
if (uploadConfig.pasteURL === false) {
|
|
98
|
+
throw new APIError(`Uploading files from URLs is disabled for collection "${slug}".`, 400)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const url = new URL(input.url)
|
|
102
|
+
|
|
103
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
104
|
+
throw new APIError('File URLs must use http or https.', 400)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (
|
|
108
|
+
typeof uploadConfig.pasteURL === 'object' &&
|
|
109
|
+
!isURLAllowed(input.url, uploadConfig.pasteURL.allowList)
|
|
110
|
+
) {
|
|
111
|
+
throw new APIError('The provided file URL is not allowed.', 400)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
file = await getExternalFile({
|
|
115
|
+
data: {
|
|
116
|
+
filename: sanitizeFilename(input.name || getURLFilename(url)),
|
|
117
|
+
url: input.url,
|
|
118
|
+
} as FileData,
|
|
119
|
+
req,
|
|
120
|
+
uploadConfig: {
|
|
121
|
+
...uploadConfig,
|
|
122
|
+
externalFileHeaderFilter: uploadConfig.externalFileHeaderFilter ?? (() => ({})),
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
file.mimetype = file.mimetype?.split(';')[0] || 'application/octet-stream'
|
|
126
|
+
file.size = file.data.length
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (maxFileSize !== undefined && Number.isFinite(maxFileSize) && file.size > maxFileSize) {
|
|
130
|
+
throw new APIError(`File exceeds the ${maxFileSize} byte upload limit.`, 400)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return file
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function decodeBase64({ maxFileSize, value }: { maxFileSize?: number; value: string }): Buffer {
|
|
137
|
+
const normalized = value.replace(/\s/g, '')
|
|
138
|
+
|
|
139
|
+
if (!/^[a-z0-9+/]*={0,2}$/i.test(normalized) || normalized.length % 4 === 1) {
|
|
140
|
+
throw new APIError('File data must be valid base64.', 400)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (maxFileSize !== undefined && Number.isFinite(maxFileSize)) {
|
|
144
|
+
const paddingLength = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0
|
|
145
|
+
const decodedSize = Math.floor((normalized.length * 3) / 4) - paddingLength
|
|
146
|
+
|
|
147
|
+
if (decodedSize > maxFileSize) {
|
|
148
|
+
throw new APIError(`File exceeds the ${maxFileSize} byte upload limit.`, 400)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const data = Buffer.from(normalized, 'base64')
|
|
153
|
+
|
|
154
|
+
if (data.toString('base64').replace(/=+$/, '') !== normalized.replace(/=+$/, '')) {
|
|
155
|
+
throw new APIError('File data must be valid base64.', 400)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return data
|
|
159
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { findDistinctInputSchema } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { defaultAccess } from '../../../defaultAccess.js'
|
|
4
|
+
import { defineCollectionTool } from '../../../defineTool.js'
|
|
5
|
+
import { getLogger } from '../../../utils/getLogger.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DESCRIPTION =
|
|
8
|
+
'Find distinct values for a field in any collection by passing the collection slug and field path.'
|
|
9
|
+
|
|
10
|
+
export const findDistinctTool = defineCollectionTool({
|
|
11
|
+
access: (args) =>
|
|
12
|
+
defaultAccess(args) && Boolean(args.permissions?.collections?.[args.slug]?.read),
|
|
13
|
+
annotations: {
|
|
14
|
+
destructiveHint: false,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
title: 'Find Distinct',
|
|
19
|
+
},
|
|
20
|
+
description: DEFAULT_DESCRIPTION,
|
|
21
|
+
input: findDistinctInputSchema,
|
|
22
|
+
}).handler(async ({ slug, authorizedMCP, input, req }) => {
|
|
23
|
+
const payload = req.payload
|
|
24
|
+
const logger = getLogger({ payload })
|
|
25
|
+
const { depth, field, limit, locale, page, populate, sort, trash, where } = input
|
|
26
|
+
|
|
27
|
+
logger.info(`Finding distinct values in collection: ${slug}, field: ${field}`)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await payload.findDistinct({
|
|
31
|
+
collection: slug,
|
|
32
|
+
depth,
|
|
33
|
+
field,
|
|
34
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
35
|
+
req,
|
|
36
|
+
...(limit ? { limit } : {}),
|
|
37
|
+
...(locale ? { locale } : {}),
|
|
38
|
+
...(page ? { page } : {}),
|
|
39
|
+
...(populate ? { populate } : {}),
|
|
40
|
+
...(sort ? { sort } : {}),
|
|
41
|
+
...(trash !== undefined ? { trash } : {}),
|
|
42
|
+
...(where ? { where } : {}),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `Distinct values for "${field}" in collection "${slug}":\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
doc: result as unknown as Record<string, unknown>,
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
56
|
+
logger.error(`Error finding distinct values in ${slug}: ${errorMessage}`)
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: `❌ **Error finding distinct values in collection "${slug}":** ${errorMessage}`,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
})
|
|
@@ -1,140 +1,83 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod'
|
|
1
|
+
import { findDocumentsInputSchema } from 'payload'
|
|
4
2
|
|
|
3
|
+
import { defaultAccess } from '../../../defaultAccess.js'
|
|
5
4
|
import { defineCollectionTool } from '../../../defineTool.js'
|
|
6
5
|
import { getLogger } from '../../../utils/getLogger.js'
|
|
7
|
-
import { localAPIDefaults } from '../../../utils/localAPIDefaults.js'
|
|
8
6
|
|
|
9
7
|
const DEFAULT_DESCRIPTION =
|
|
10
|
-
'Find documents in
|
|
8
|
+
'Find documents in any collection by passing the collection slug and optional ID or where clause.'
|
|
11
9
|
|
|
12
|
-
export const
|
|
10
|
+
export const findDocumentsTool = defineCollectionTool({
|
|
11
|
+
access: (args) =>
|
|
12
|
+
defaultAccess(args) && Boolean(args.permissions?.collections?.[args.slug]?.read),
|
|
13
|
+
annotations: {
|
|
14
|
+
destructiveHint: false,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
title: 'Find Documents',
|
|
19
|
+
},
|
|
13
20
|
description: DEFAULT_DESCRIPTION,
|
|
14
|
-
input:
|
|
15
|
-
|
|
16
|
-
.union([z.string(), z.number()])
|
|
17
|
-
.describe(
|
|
18
|
-
'Optional: specific document ID to retrieve. If not provided, returns all documents',
|
|
19
|
-
)
|
|
20
|
-
.optional(),
|
|
21
|
-
depth: z
|
|
22
|
-
.number()
|
|
23
|
-
.int()
|
|
24
|
-
.min(0)
|
|
25
|
-
.max(10)
|
|
26
|
-
.describe('How many levels deep to populate relationships (default: 0)')
|
|
27
|
-
.optional()
|
|
28
|
-
.default(0),
|
|
29
|
-
draft: z
|
|
30
|
-
.boolean()
|
|
31
|
-
.describe(
|
|
32
|
-
'Optional: whether the document should be queried from the versions table/collection or not.',
|
|
33
|
-
)
|
|
34
|
-
.optional(),
|
|
35
|
-
fallbackLocale: z
|
|
36
|
-
.string()
|
|
37
|
-
.describe('Optional: fallback locale code to use when requested locale is not available')
|
|
38
|
-
.optional(),
|
|
39
|
-
limit: z
|
|
40
|
-
.number()
|
|
41
|
-
.int()
|
|
42
|
-
.min(1)
|
|
43
|
-
.max(100)
|
|
44
|
-
.describe('Maximum number of documents to return (default: 10, max: 100)')
|
|
45
|
-
.optional()
|
|
46
|
-
.default(10),
|
|
47
|
-
locale: z
|
|
48
|
-
.string()
|
|
49
|
-
.describe(
|
|
50
|
-
'Optional: locale code to retrieve data in (e.g., "en", "es"). Use "all" to retrieve all locales for localized fields',
|
|
51
|
-
)
|
|
52
|
-
.optional(),
|
|
53
|
-
page: z
|
|
54
|
-
.number()
|
|
55
|
-
.int()
|
|
56
|
-
.min(1)
|
|
57
|
-
.describe('Page number for pagination (default: 1)')
|
|
58
|
-
.optional()
|
|
59
|
-
.default(1),
|
|
60
|
-
select: z
|
|
61
|
-
.string()
|
|
62
|
-
.describe(
|
|
63
|
-
"Optional: define exactly which fields you'd like to return in the response (JSON), e.g., '{\"title\": true}'",
|
|
64
|
-
)
|
|
65
|
-
.optional(),
|
|
66
|
-
sort: z
|
|
67
|
-
.string()
|
|
68
|
-
.describe('Field to sort by (e.g., "createdAt", "-updatedAt" for descending)')
|
|
69
|
-
.optional(),
|
|
70
|
-
where: z
|
|
71
|
-
.string()
|
|
72
|
-
.describe(
|
|
73
|
-
'Optional JSON string for where clause filtering (e.g., \'{"title": {"contains": "test"}}\')',
|
|
74
|
-
)
|
|
75
|
-
.optional(),
|
|
76
|
-
}),
|
|
77
|
-
}).handler(async ({ authorizedMCP, collectionSlug, input, req }) => {
|
|
21
|
+
input: findDocumentsInputSchema,
|
|
22
|
+
}).handler(async ({ slug, authorizedMCP, input, req }) => {
|
|
78
23
|
const payload = req.payload
|
|
79
24
|
const logger = getLogger({ payload })
|
|
80
25
|
|
|
81
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
id,
|
|
28
|
+
depth,
|
|
29
|
+
draft,
|
|
30
|
+
fallbackLocale,
|
|
31
|
+
joins,
|
|
32
|
+
limit,
|
|
33
|
+
locale,
|
|
34
|
+
page,
|
|
35
|
+
pagination,
|
|
36
|
+
populate,
|
|
37
|
+
select,
|
|
38
|
+
sort,
|
|
39
|
+
trash,
|
|
40
|
+
where,
|
|
41
|
+
} = input
|
|
82
42
|
|
|
83
43
|
logger.info(
|
|
84
|
-
`Reading document from collection: ${
|
|
44
|
+
`Reading document from collection: ${slug}${id ? ` with ID: ${id}` : ''}, limit: ${limit}, page: ${page}${locale ? `, locale: ${locale}` : ''}`,
|
|
85
45
|
)
|
|
86
46
|
|
|
87
47
|
try {
|
|
88
|
-
let whereClause: Record<string, unknown> = {}
|
|
89
|
-
if (where) {
|
|
90
|
-
try {
|
|
91
|
-
whereClause = JSON.parse(where) as Record<string, unknown>
|
|
92
|
-
} catch {
|
|
93
|
-
logger.warn(`Invalid where clause JSON: ${where}`)
|
|
94
|
-
return { content: [{ type: 'text', text: 'Error: Invalid JSON in where clause' }] }
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
let selectClause: SelectType | undefined
|
|
99
|
-
if (select) {
|
|
100
|
-
try {
|
|
101
|
-
selectClause = JSON.parse(select) as SelectType
|
|
102
|
-
} catch {
|
|
103
|
-
logger.warn(`Invalid select clause JSON: ${select}`)
|
|
104
|
-
return { content: [{ type: 'text', text: 'Error: Invalid JSON in select clause' }] }
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
48
|
if (id) {
|
|
109
49
|
try {
|
|
110
50
|
const doc = await payload.findByID({
|
|
111
51
|
id,
|
|
112
|
-
collection:
|
|
52
|
+
collection: slug,
|
|
113
53
|
depth,
|
|
54
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
114
55
|
req,
|
|
115
|
-
...
|
|
116
|
-
...(
|
|
56
|
+
...(select && { select }),
|
|
57
|
+
...(populate && { populate }),
|
|
58
|
+
...(joins !== undefined && { joins }),
|
|
117
59
|
...(locale && { locale }),
|
|
118
|
-
...(fallbackLocale && { fallbackLocale }),
|
|
60
|
+
...(fallbackLocale !== undefined && { fallbackLocale }),
|
|
119
61
|
...(draft !== undefined && { draft }),
|
|
62
|
+
...(trash !== undefined && { trash }),
|
|
120
63
|
})
|
|
121
64
|
|
|
122
65
|
return {
|
|
123
66
|
content: [
|
|
124
67
|
{
|
|
125
68
|
type: 'text',
|
|
126
|
-
text: `Document from collection "${
|
|
69
|
+
text: `Document from collection "${slug}":\n${JSON.stringify(doc)}`,
|
|
127
70
|
},
|
|
128
71
|
],
|
|
129
72
|
doc: doc as Record<string, unknown>,
|
|
130
73
|
}
|
|
131
74
|
} catch {
|
|
132
|
-
logger.warn(`Document not found with ID: ${id} in collection: ${
|
|
75
|
+
logger.warn(`Document not found with ID: ${id} in collection: ${slug}`)
|
|
133
76
|
return {
|
|
134
77
|
content: [
|
|
135
78
|
{
|
|
136
79
|
type: 'text',
|
|
137
|
-
text: `Error: Document with ID "${id}" not found in collection "${
|
|
80
|
+
text: `Error: Document with ID "${id}" not found in collection "${slug}"`,
|
|
138
81
|
},
|
|
139
82
|
],
|
|
140
83
|
}
|
|
@@ -142,28 +85,32 @@ export const findCollectionTool = defineCollectionTool({
|
|
|
142
85
|
}
|
|
143
86
|
|
|
144
87
|
const findOptions: Parameters<typeof payload.find>[0] = {
|
|
145
|
-
collection:
|
|
88
|
+
collection: slug,
|
|
146
89
|
depth,
|
|
147
90
|
limit,
|
|
91
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
148
92
|
page,
|
|
149
93
|
req,
|
|
150
|
-
...
|
|
151
|
-
...(
|
|
94
|
+
...(select && { select }),
|
|
95
|
+
...(populate && { populate }),
|
|
96
|
+
...(joins !== undefined && { joins }),
|
|
152
97
|
...(locale && { locale }),
|
|
153
|
-
...(fallbackLocale && { fallbackLocale }),
|
|
98
|
+
...(fallbackLocale !== undefined && { fallbackLocale }),
|
|
154
99
|
...(draft !== undefined && { draft }),
|
|
100
|
+
...(pagination !== undefined && { pagination }),
|
|
101
|
+
...(trash !== undefined && { trash }),
|
|
155
102
|
}
|
|
156
103
|
|
|
157
104
|
if (sort) {
|
|
158
105
|
findOptions.sort = sort
|
|
159
106
|
}
|
|
160
|
-
if (
|
|
161
|
-
findOptions.where =
|
|
107
|
+
if (where) {
|
|
108
|
+
findOptions.where = where
|
|
162
109
|
}
|
|
163
110
|
|
|
164
111
|
const result = await payload.find(findOptions)
|
|
165
112
|
|
|
166
|
-
let responseText = `Collection: "${
|
|
113
|
+
let responseText = `Collection: "${slug}"\nTotal: ${result.totalDocs} documents\nPage: ${result.page} of ${result.totalPages}\n`
|
|
167
114
|
for (const doc of result.docs) {
|
|
168
115
|
responseText += `\n\`\`\`json\n${JSON.stringify(doc)}\n\`\`\``
|
|
169
116
|
}
|
|
@@ -174,12 +121,12 @@ export const findCollectionTool = defineCollectionTool({
|
|
|
174
121
|
}
|
|
175
122
|
} catch (error) {
|
|
176
123
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
177
|
-
logger.error(`Error reading documents from collection ${
|
|
124
|
+
logger.error(`Error reading documents from collection ${slug}: ${errorMessage}`)
|
|
178
125
|
return {
|
|
179
126
|
content: [
|
|
180
127
|
{
|
|
181
128
|
type: 'text',
|
|
182
|
-
text: `❌ **Error reading documents from collection "${
|
|
129
|
+
text: `❌ **Error reading documents from collection "${slug}":** ${errorMessage}`,
|
|
183
130
|
},
|
|
184
131
|
],
|
|
185
132
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { findVersionByIDInputSchema } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { defaultAccess } from '../../../defaultAccess.js'
|
|
4
|
+
import { defineCollectionTool } from '../../../defineTool.js'
|
|
5
|
+
import { getLogger } from '../../../utils/getLogger.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DESCRIPTION =
|
|
8
|
+
'Find a specific document version in any version-enabled collection by passing the collection slug and version ID.'
|
|
9
|
+
|
|
10
|
+
export const findVersionByIDTool = defineCollectionTool({
|
|
11
|
+
access: (args) =>
|
|
12
|
+
defaultAccess(args) && Boolean(args.permissions?.collections?.[args.slug]?.readVersions),
|
|
13
|
+
annotations: {
|
|
14
|
+
destructiveHint: false,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
title: 'Find Version By ID',
|
|
19
|
+
},
|
|
20
|
+
description: DEFAULT_DESCRIPTION,
|
|
21
|
+
input: findVersionByIDInputSchema,
|
|
22
|
+
}).handler(async ({ slug, authorizedMCP, input, req }) => {
|
|
23
|
+
const payload = req.payload
|
|
24
|
+
const logger = getLogger({ payload })
|
|
25
|
+
const { id, depth, draft, fallbackLocale, locale, populate, select, trash } = input
|
|
26
|
+
|
|
27
|
+
logger.info(`Finding version in collection: ${slug} with ID: ${id}`)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await payload.findVersionByID({
|
|
31
|
+
id: String(id),
|
|
32
|
+
collection: slug,
|
|
33
|
+
depth,
|
|
34
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
35
|
+
req,
|
|
36
|
+
...(draft !== undefined ? { draft } : {}),
|
|
37
|
+
...(fallbackLocale !== undefined ? { fallbackLocale } : {}),
|
|
38
|
+
...(locale ? { locale } : {}),
|
|
39
|
+
...(populate ? { populate } : {}),
|
|
40
|
+
...(select ? { select } : {}),
|
|
41
|
+
...(trash !== undefined ? { trash } : {}),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: `Version "${id}" from collection "${slug}":\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
doc: result as unknown as Record<string, unknown>,
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
55
|
+
logger.error(`Error finding version ${id} in ${slug}: ${errorMessage}`)
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: 'text',
|
|
60
|
+
text: `❌ **Error finding version "${id}" in collection "${slug}":** ${errorMessage}`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { findVersionsInputSchema } from 'payload'
|
|
2
|
+
|
|
3
|
+
import { defaultAccess } from '../../../defaultAccess.js'
|
|
4
|
+
import { defineCollectionTool } from '../../../defineTool.js'
|
|
5
|
+
import { getLogger } from '../../../utils/getLogger.js'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DESCRIPTION =
|
|
8
|
+
'Find document versions in any version-enabled collection by passing the collection slug and optional where clause.'
|
|
9
|
+
|
|
10
|
+
export const findVersionsTool = defineCollectionTool({
|
|
11
|
+
access: (args) =>
|
|
12
|
+
defaultAccess(args) && Boolean(args.permissions?.collections?.[args.slug]?.readVersions),
|
|
13
|
+
annotations: {
|
|
14
|
+
destructiveHint: false,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
readOnlyHint: true,
|
|
18
|
+
title: 'Find Versions',
|
|
19
|
+
},
|
|
20
|
+
description: DEFAULT_DESCRIPTION,
|
|
21
|
+
input: findVersionsInputSchema,
|
|
22
|
+
}).handler(async ({ slug, authorizedMCP, input, req }) => {
|
|
23
|
+
const payload = req.payload
|
|
24
|
+
const logger = getLogger({ payload })
|
|
25
|
+
const {
|
|
26
|
+
depth,
|
|
27
|
+
draft,
|
|
28
|
+
fallbackLocale,
|
|
29
|
+
limit,
|
|
30
|
+
locale,
|
|
31
|
+
page,
|
|
32
|
+
pagination,
|
|
33
|
+
populate,
|
|
34
|
+
select,
|
|
35
|
+
sort,
|
|
36
|
+
trash,
|
|
37
|
+
where,
|
|
38
|
+
} = input
|
|
39
|
+
|
|
40
|
+
logger.info(`Finding versions in collection: ${slug}, limit: ${limit}, page: ${page}`)
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const result = await payload.findVersions({
|
|
44
|
+
collection: slug,
|
|
45
|
+
depth,
|
|
46
|
+
limit,
|
|
47
|
+
overrideAccess: authorizedMCP.overrideAccess,
|
|
48
|
+
page,
|
|
49
|
+
req,
|
|
50
|
+
...(draft !== undefined ? { draft } : {}),
|
|
51
|
+
...(fallbackLocale !== undefined ? { fallbackLocale } : {}),
|
|
52
|
+
...(locale ? { locale } : {}),
|
|
53
|
+
...(pagination !== undefined ? { pagination } : {}),
|
|
54
|
+
...(populate ? { populate } : {}),
|
|
55
|
+
...(select ? { select } : {}),
|
|
56
|
+
...(sort ? { sort } : {}),
|
|
57
|
+
...(trash !== undefined ? { trash } : {}),
|
|
58
|
+
...(where ? { where } : {}),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: 'text',
|
|
65
|
+
text: `Versions for collection "${slug}":\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
doc: result as unknown as Record<string, unknown>,
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
|
72
|
+
logger.error(`Error finding versions in ${slug}: ${errorMessage}`)
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: `❌ **Error finding versions in collection "${slug}":** ${errorMessage}`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
isError: true,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|