@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.
Files changed (92) hide show
  1. package/bin.js +0 -7
  2. package/dist/endpoint/access.d.ts.map +1 -1
  3. package/dist/endpoint/access.js +15 -3
  4. package/dist/endpoint/access.js.map +1 -1
  5. package/dist/endpoint/index.d.ts.map +1 -1
  6. package/dist/endpoint/index.js +46 -4
  7. package/dist/endpoint/index.js.map +1 -1
  8. package/dist/endpoint/oauth.d.ts +39 -0
  9. package/dist/endpoint/oauth.d.ts.map +1 -0
  10. package/dist/endpoint/oauth.js +92 -0
  11. package/dist/endpoint/oauth.js.map +1 -0
  12. package/dist/endpoint/oauthLoginToken.d.ts +20 -0
  13. package/dist/endpoint/oauthLoginToken.d.ts.map +1 -0
  14. package/dist/endpoint/oauthLoginToken.js +158 -0
  15. package/dist/endpoint/oauthLoginToken.js.map +1 -0
  16. package/dist/endpoint/oauthServer.d.ts +12 -0
  17. package/dist/endpoint/oauthServer.d.ts.map +1 -0
  18. package/dist/endpoint/oauthServer.js +377 -0
  19. package/dist/endpoint/oauthServer.js.map +1 -0
  20. package/dist/index.d.ts +10 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/mcp/buildMcpServer.d.ts +1 -1
  25. package/dist/mcp/buildMcpServer.d.ts.map +1 -1
  26. package/dist/mcp/buildMcpServer.js +3 -3
  27. package/dist/mcp/buildMcpServer.js.map +1 -1
  28. package/dist/mcp/builtin/collections/authTools.d.ts.map +1 -1
  29. package/dist/mcp/builtin/collections/authTools.js +4 -3
  30. package/dist/mcp/builtin/collections/authTools.js.map +1 -1
  31. package/dist/mcp/builtin/collections/createTool.d.ts.map +1 -1
  32. package/dist/mcp/builtin/collections/createTool.js +12 -6
  33. package/dist/mcp/builtin/collections/createTool.js.map +1 -1
  34. package/dist/mcp/builtin/collections/deleteTool.js +6 -2
  35. package/dist/mcp/builtin/collections/deleteTool.js.map +1 -1
  36. package/dist/mcp/builtin/collections/duplicateTool.d.ts.map +1 -1
  37. package/dist/mcp/builtin/collections/duplicateTool.js +6 -2
  38. package/dist/mcp/builtin/collections/duplicateTool.js.map +1 -1
  39. package/dist/mcp/builtin/collections/fileInput.d.ts.map +1 -1
  40. package/dist/mcp/builtin/collections/fileInput.js +19 -54
  41. package/dist/mcp/builtin/collections/fileInput.js.map +1 -1
  42. package/dist/mcp/builtin/collections/findTool.js +8 -4
  43. package/dist/mcp/builtin/collections/findTool.js.map +1 -1
  44. package/dist/mcp/builtin/collections/getCollectionSchemaTool.d.ts.map +1 -1
  45. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js +19 -3
  46. package/dist/mcp/builtin/collections/getCollectionSchemaTool.js.map +1 -1
  47. package/dist/mcp/builtin/collections/restoreVersionTool.js.map +1 -1
  48. package/dist/mcp/builtin/collections/updateTool.d.ts.map +1 -1
  49. package/dist/mcp/builtin/collections/updateTool.js +21 -8
  50. package/dist/mcp/builtin/collections/updateTool.js.map +1 -1
  51. package/dist/mcp/builtin/collections/uploadInstructionsTool.js.map +1 -1
  52. package/dist/mcp/builtin/globals/findTool.js.map +1 -1
  53. package/dist/mcp/builtin/globals/getGlobalSchemaTool.d.ts.map +1 -1
  54. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js +19 -3
  55. package/dist/mcp/builtin/globals/getGlobalSchemaTool.js.map +1 -1
  56. package/dist/mcp/builtin/globals/updateTool.js.map +1 -1
  57. package/dist/mcp/sanitizeMCPConfig.d.ts.map +1 -1
  58. package/dist/mcp/sanitizeMCPConfig.js +3 -0
  59. package/dist/mcp/sanitizeMCPConfig.js.map +1 -1
  60. package/dist/stdio.js +4 -3
  61. package/dist/stdio.js.map +1 -1
  62. package/dist/types.d.ts +91 -3
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js.map +1 -1
  65. package/dist/utils/toStandardSchema.js.map +1 -1
  66. package/package.json +23 -26
  67. package/src/endpoint/access.ts +12 -3
  68. package/src/endpoint/index.ts +41 -3
  69. package/src/endpoint/oauth.ts +91 -0
  70. package/src/endpoint/oauthLoginToken.ts +210 -0
  71. package/src/endpoint/oauthServer.ts +428 -0
  72. package/src/index.ts +40 -5
  73. package/src/mcp/buildMcpServer.ts +5 -5
  74. package/src/mcp/builtin/collections/authTools.ts +7 -4
  75. package/src/mcp/builtin/collections/createTool.ts +26 -6
  76. package/src/mcp/builtin/collections/deleteTool.ts +3 -3
  77. package/src/mcp/builtin/collections/duplicateTool.ts +3 -2
  78. package/src/mcp/builtin/collections/fileInput.ts +17 -59
  79. package/src/mcp/builtin/collections/findTool.ts +4 -4
  80. package/src/mcp/builtin/collections/getCollectionSchemaTool.ts +12 -1
  81. package/src/mcp/builtin/collections/restoreVersionTool.ts +1 -1
  82. package/src/mcp/builtin/collections/updateTool.ts +10 -6
  83. package/src/mcp/builtin/collections/uploadInstructionsTool.ts +1 -1
  84. package/src/mcp/builtin/globals/findTool.ts +1 -1
  85. package/src/mcp/builtin/globals/getGlobalSchemaTool.ts +11 -1
  86. package/src/mcp/builtin/globals/updateTool.ts +1 -1
  87. package/src/mcp/sanitizeMCPConfig.ts +1 -0
  88. package/src/stdio.ts +2 -2
  89. package/src/types.ts +93 -3
  90. package/src/utils/toStandardSchema.ts +1 -1
  91. package/dist/mcp/builtin/collections/fileInput.spec.js +0 -86
  92. 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 { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig } from './types.js'
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 { AuthorizedMCP, MCPPluginConfig, SanitizedMCPPluginConfig }
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 as unknown as typeof registered.options
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
- { handler: mcpEndpoint, method: 'post', path: '/mcp' },
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 `createLocalReq`.
28
+ * synthesized one built via `createPayloadRequest`.
31
29
  */
32
30
  export const buildMcpServer = ({
33
31
  authorizedMCP,
@@ -38,6 +36,8 @@ export const buildMcpServer = ({
38
36
  pluginConfig: SanitizedMCPPluginConfig
39
37
  req: PayloadRequest
40
38
  }): McpServer => {
39
+ z.config(z.locales.en())
40
+
41
41
  const serverOptions = pluginConfig.mcp?.serverOptions || {}
42
42
  const server = new McpServer(
43
43
  { name: 'Payload MCP Server', version: '1.0.0', ...serverOptions.serverInfo },
@@ -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 }) as unknown as ToolInputSchema
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: schema as JsonSchemaType })
264
+ return addSlugToJSONSchema({ description, schema })
265
265
  }
266
266
 
267
267
  return addSlugToJSONSchema({ description, schema: input })
@@ -1,4 +1,4 @@
1
- import { createLocalReq, strictObject, z } from 'payload'
1
+ import { createPayloadRequest, strictObject, z } from 'payload'
2
2
 
3
3
  import type { MCPToolResponse } from '../../../types.js'
4
4
 
@@ -53,7 +53,10 @@ export const authCollectionTool = defineCollectionTool({
53
53
  if (input.headers) {
54
54
  authHeaders = new Headers(input.headers)
55
55
  }
56
- const authReq = await createLocalReq({ req: { headers: authHeaders } }, req.payload)
56
+ const authReq = await createPayloadRequest({
57
+ payload: req.payload,
58
+ req: { headers: authHeaders },
59
+ })
57
60
  const result = await req.payload.auth({ headers: authHeaders, req: authReq })
58
61
 
59
62
  if (result.user) {
@@ -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 } as Record<string, unknown>,
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 } as Record<string, unknown>,
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 { depth, documents, draft, fallbackLocale, locale, populate, publishAllLocales, select } =
34
- input
35
+ const {
36
+ depth,
37
+ documents,
38
+ draft,
39
+ fallbackLocale,
40
+ locale,
41
+ populate,
42
+ publishAllLocales,
43
+ returning,
44
+ select,
45
+ } = input
46
+ const shouldUsePartialSchema =
47
+ draft === true && collectionConfig !== undefined && !hasDraftValidationEnabled(collectionConfig)
35
48
 
36
49
  logger.info(`Creating ${documents.length} documents in collection: ${slug}`)
37
50
 
38
51
  try {
39
52
  const virtualFieldNames = getCollectionVirtualFieldNames(payload.config, slug)
40
- const docs: Array<{ doc: Record<string, unknown>; index: number }> = []
53
+ const docs: Array<
54
+ { doc: Record<string, unknown>; index: number } | { id: number | string; index: number }
55
+ > = []
41
56
  const errors: Array<{ index: number; message: string }> = []
42
57
  let validationSchema: Record<string, unknown> | undefined
43
58
 
44
59
  for (const [index, document] of documents.entries()) {
45
60
  try {
46
61
  const inputData = stripVirtualFields(document.data, virtualFieldNames)
47
- validateCollectionData({ slug, data: inputData, req })
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
- ...(select ? { select } : {}),
83
+ select: returning ? select : { id: true },
64
84
  })
65
85
 
66
- docs.push({ doc: result as Record<string, unknown>, index })
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 as Record<string, unknown>,
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 as Record<string, unknown>,
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, FileData, PayloadRequest } from 'payload'
1
+ import type { CollectionSlug, File, PayloadRequest } from 'payload'
2
2
 
3
3
  import { APIError, z } from 'payload'
4
- import { getExternalFile, getFileFromUploadInstructions, isURLAllowed } from 'payload/internal'
4
+ import {
5
+ externalURLInputSchema,
6
+ getFileFromUploadInstructions,
7
+ resolveURLUploadInput,
8
+ } from 'payload/internal'
5
9
  import { sanitizeFilename } from 'payload/shared'
6
10
 
7
11
  const mimeTypeSchema = z
@@ -30,11 +34,7 @@ export const fileInputSchema = z
30
34
  mimeType: mimeTypeSchema.check(z.describe('The file MIME type, for example image/png.')),
31
35
  source: z.literal('base64'),
32
36
  }),
33
- z.strictObject({
34
- name: z.optional(z.string().check(z.minLength(1))).check(z.describe('File name override.')),
35
- source: z.literal('externalURL'),
36
- url: z.url().check(z.describe('The http or https URL to download.')),
37
- }),
37
+ externalURLInputSchema,
38
38
  z.strictObject({
39
39
  file: uploadFileSchema.check(z.describe('getUploadInstructions file field post-upload.')),
40
40
  source: z.literal('uploadReference'),
@@ -75,6 +75,10 @@ export async function resolveFile({
75
75
  }
76
76
  }
77
77
 
78
+ if (input.source === 'externalURL') {
79
+ return resolveURLUploadInput({ slug, input, req })
80
+ }
81
+
78
82
  const uploadConfig = req.payload.collections[slug]?.config.upload
79
83
 
80
84
  if (!uploadConfig) {
@@ -82,48 +86,12 @@ export async function resolveFile({
82
86
  }
83
87
 
84
88
  const maxFileSize = req.payload.config.upload.limits?.fileSize
85
- let file: File
86
-
87
- if (input.source === 'base64') {
88
- const data = decodeBase64({ maxFileSize, value: input.data })
89
-
90
- file = {
91
- name: sanitizeFilename(input.name),
92
- data,
93
- mimetype: input.mimeType,
94
- size: data.length,
95
- }
96
- } else {
97
- if (uploadConfig.pasteURL === false) {
98
- throw new APIError(`Uploading files from URLs is disabled for collection "${slug}".`, 400)
99
- }
100
-
101
- const url = new URL(input.url)
102
-
103
- if (!['http:', 'https:'].includes(url.protocol)) {
104
- throw new APIError('File URLs must use http or https.', 400)
105
- }
106
-
107
- if (
108
- typeof uploadConfig.pasteURL === 'object' &&
109
- !isURLAllowed(input.url, uploadConfig.pasteURL.allowList)
110
- ) {
111
- throw new APIError('The provided file URL is not allowed.', 400)
112
- }
113
-
114
- file = await getExternalFile({
115
- data: {
116
- filename: sanitizeFilename(input.name || getURLFilename(url)),
117
- url: input.url,
118
- } as FileData,
119
- req,
120
- uploadConfig: {
121
- ...uploadConfig,
122
- externalFileHeaderFilter: uploadConfig.externalFileHeaderFilter ?? (() => ({})),
123
- },
124
- })
125
- file.mimetype = file.mimetype?.split(';')[0] || 'application/octet-stream'
126
- file.size = file.data.length
89
+ const data = decodeBase64({ maxFileSize, value: input.data })
90
+ const file: File = {
91
+ name: sanitizeFilename(input.name),
92
+ data,
93
+ mimetype: input.mimeType,
94
+ size: data.length,
127
95
  }
128
96
 
129
97
  if (maxFileSize !== undefined && Number.isFinite(maxFileSize) && file.size > maxFileSize) {
@@ -157,13 +125,3 @@ function decodeBase64({ maxFileSize, value }: { maxFileSize?: number; value: str
157
125
 
158
126
  return data
159
127
  }
160
-
161
- function getURLFilename(url: URL): string {
162
- const pathSegment = url.pathname.split('/').pop() || 'upload'
163
-
164
- try {
165
- return decodeURIComponent(pathSegment)
166
- } catch {
167
- return pathSegment
168
- }
169
- }
@@ -1,4 +1,4 @@
1
- import { findDocumentsInputSchema } from 'payload'
1
+ import { findDocumentsInputSchema, parseDocumentID } from 'payload'
2
2
 
3
3
  import { defaultAccess } from '../../../defaultAccess.js'
4
4
  import { defineCollectionTool } from '../../../defineTool.js'
@@ -45,10 +45,10 @@ export const findDocumentsTool = defineCollectionTool({
45
45
  )
46
46
 
47
47
  try {
48
- if (id) {
48
+ if (id !== undefined) {
49
49
  try {
50
50
  const doc = await payload.findByID({
51
- id,
51
+ id: parseDocumentID({ id, collectionSlug: slug, payload }),
52
52
  collection: slug,
53
53
  depth,
54
54
  overrideAccess: authorizedMCP.overrideAccess,
@@ -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: doc as Record<string, unknown>,
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: 'Get the input schema for creating or updating documents in a collection.',
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 as Record<string, unknown>,
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}"!\nUpdated document:\n\`\`\`json\n${JSON.stringify(result)}\n\`\`\``,
103
+ text: `Document updated successfully in collection "${slug}"!\nResult:\n\`\`\`json\n${JSON.stringify(responseResult)}\n\`\`\``,
100
104
  },
101
105
  ],
102
- doc: result as Record<string, unknown>,
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
- args.permissions?.collections?.[args.slug]?.update,
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 as Record<string, unknown>,
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 for updating a global.',
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 as Record<string, unknown>,
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 { createLocalReq, getPayload } from 'payload'
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 createLocalReq({ req: { headers } }, payload)
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