Skip to content

Commit 268b33a

Browse files
feat(plugin-mcp): adds select API with CRUD tools (#15301)
## Goals - Helps reduce token usage. - Better sync with the existing Payload API. MCP Tools effected: - Find, Create, and Update tools for Globals and Collections. ### Discussion: #14921
1 parent e77f9b6 commit 268b33a

File tree

7 files changed

+430
-13
lines changed

7 files changed

+430
-13
lines changed

packages/plugin-mcp/src/mcp/tools/global/find.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2-
import type { PayloadRequest, TypedUser } from 'payload'
2+
import type { PayloadRequest, SelectType, TypedUser } from 'payload'
33

44
import type { PluginMCPServerConfig } from '../../../types.js'
55

@@ -18,6 +18,7 @@ export const findGlobalTool = (
1818
depth: number = 0,
1919
locale?: string,
2020
fallbackLocale?: string,
21+
select?: string,
2122
): Promise<{
2223
content: Array<{
2324
text: string
@@ -39,13 +40,34 @@ export const findGlobalTool = (
3940
user,
4041
}
4142

43+
let selectClause: SelectType | undefined
44+
if (select) {
45+
try {
46+
selectClause = JSON.parse(select) as SelectType
47+
} catch (_parseError) {
48+
payload.logger.warn(`[payload-mcp] Invalid select clause JSON for global: ${select}`)
49+
const response = {
50+
content: [{ type: 'text' as const, text: 'Error: Invalid JSON in select clause' }],
51+
}
52+
return (globals?.[globalSlug]?.overrideResponse?.(response, {}, req) || response) as {
53+
content: Array<{
54+
text: string
55+
type: 'text'
56+
}>
57+
}
58+
}
59+
}
60+
4261
// Add locale parameters if provided
4362
if (locale) {
4463
findOptions.locale = locale
4564
}
4665
if (fallbackLocale) {
4766
findOptions.fallbackLocale = fallbackLocale
4867
}
68+
if (selectClause) {
69+
findOptions.select = selectClause
70+
}
4971

5072
const result = await payload.findGlobal(findOptions)
5173

@@ -96,8 +118,8 @@ ${JSON.stringify(result, null, 2)}
96118
`find${globalSlug.charAt(0).toUpperCase() + toCamelCase(globalSlug).slice(1)}`,
97119
`${toolSchemas.findGlobal.description.trim()}\n\n${globals?.[globalSlug]?.description || ''}`,
98120
toolSchemas.findGlobal.parameters.shape,
99-
async ({ depth, fallbackLocale, locale }) => {
100-
return await tool(depth, locale, fallbackLocale)
121+
async ({ depth, fallbackLocale, locale, select }) => {
122+
return await tool(depth, locale, fallbackLocale, select)
101123
},
102124
)
103125
}

packages/plugin-mcp/src/mcp/tools/global/update.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
import type { JSONSchema4 } from 'json-schema'
3-
import type { PayloadRequest, TypedUser } from 'payload'
3+
import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

@@ -25,6 +25,7 @@ export const updateGlobalTool = (
2525
depth: number = 0,
2626
locale?: string,
2727
fallbackLocale?: string,
28+
select?: string,
2829
): Promise<{
2930
content: Array<{
3031
text: string
@@ -62,6 +63,24 @@ export const updateGlobalTool = (
6263
}
6364
}
6465

66+
let selectClause: SelectType | undefined
67+
if (select) {
68+
try {
69+
selectClause = JSON.parse(select) as SelectType
70+
} catch (_parseError) {
71+
payload.logger.warn(`[payload-mcp] Invalid select clause JSON for global: ${select}`)
72+
const response = {
73+
content: [{ type: 'text' as const, text: 'Error: Invalid JSON in select clause' }],
74+
}
75+
return (globals?.[globalSlug]?.overrideResponse?.(response, {}, req) || response) as {
76+
content: Array<{
77+
text: string
78+
type: 'text'
79+
}>
80+
}
81+
}
82+
}
83+
6584
const updateOptions: Parameters<typeof payload.updateGlobal>[0] = {
6685
slug: globalSlug,
6786
data: parsedData,
@@ -77,6 +96,9 @@ export const updateGlobalTool = (
7796
if (fallbackLocale) {
7897
updateOptions.fallbackLocale = fallbackLocale
7998
}
99+
if (selectClause) {
100+
updateOptions.select = selectClause
101+
}
80102

81103
const result = await payload.updateGlobal(updateOptions)
82104

@@ -146,21 +168,28 @@ ${JSON.stringify(result, null, 2)}
146168
.describe(
147169
'Optional: locale code to update data in (e.g., "en", "es"). Use "all" to update all locales for localized fields',
148170
),
171+
select: z
172+
.string()
173+
.optional()
174+
.describe(
175+
'Optional: define exactly which fields you\'d like to return in the response (JSON), e.g., \'{"siteName": "My Site"}\'',
176+
),
149177
})
150178

151179
server.tool(
152180
`update${globalSlug.charAt(0).toUpperCase() + toCamelCase(globalSlug).slice(1)}`,
153181
`${toolSchemas.updateGlobal.description.trim()}\n\n${globals?.[globalSlug]?.description || ''}`,
154182
updateGlobalSchema.shape,
155183
async (params: Record<string, unknown>) => {
156-
const { depth, draft, fallbackLocale, locale, ...rest } = params
184+
const { depth, draft, fallbackLocale, locale, select, ...rest } = params
157185
const data = JSON.stringify(rest)
158186
return await tool(
159187
data,
160188
draft as boolean,
161189
depth as number,
162190
locale as string,
163191
fallbackLocale as string,
192+
select as string | undefined,
164193
)
165194
},
166195
)

packages/plugin-mcp/src/mcp/tools/resource/create.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
import type { JSONSchema4 } from 'json-schema'
3-
import type { PayloadRequest, TypedUser } from 'payload'
3+
import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

@@ -24,6 +24,7 @@ export const createResourceTool = (
2424
draft: boolean,
2525
locale?: string,
2626
fallbackLocale?: string,
27+
select?: string,
2728
): Promise<{
2829
content: Array<{
2930
text: string
@@ -55,6 +56,25 @@ export const createResourceTool = (
5556
}
5657
}
5758

59+
let selectClause: SelectType | undefined
60+
if (select) {
61+
try {
62+
selectClause = JSON.parse(select) as SelectType
63+
} catch (_parseError) {
64+
payload.logger.warn(`[payload-mcp] Invalid select clause JSON: ${select}`)
65+
const response = {
66+
content: [{ type: 'text' as const, text: 'Error: Invalid JSON in select clause' }],
67+
}
68+
return (collections?.[collectionSlug]?.overrideResponse?.(response, {}, req) ||
69+
response) as {
70+
content: Array<{
71+
text: string
72+
type: 'text'
73+
}>
74+
}
75+
}
76+
}
77+
5878
// Create the resource
5979
const result = await payload.create({
6080
collection: collectionSlug,
@@ -66,6 +86,7 @@ export const createResourceTool = (
6686
user,
6787
...(locale && { locale }),
6888
...(fallbackLocale && { fallbackLocale }),
89+
...(selectClause && { select: selectClause }),
6990
})
7091

7192
if (verboseLogs) {
@@ -147,21 +168,28 @@ ${JSON.stringify(result, null, 2)}
147168
.describe(
148169
'Optional: locale code to create the document in (e.g., "en", "es"). Defaults to the default locale',
149170
),
171+
select: z
172+
.string()
173+
.optional()
174+
.describe(
175+
'Optional: define exactly which fields you\'d like to create (JSON), e.g., \'{"title": "My Post"}\'',
176+
),
150177
})
151178

152179
server.tool(
153180
`create${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
154181
`${collections?.[collectionSlug]?.description || toolSchemas.createResource.description.trim()}`,
155182
createResourceSchema.shape,
156183
async (params: Record<string, unknown>) => {
157-
const { depth, draft, fallbackLocale, locale, ...fieldData } = params
184+
const { depth, draft, fallbackLocale, locale, select, ...fieldData } = params
158185
const data = JSON.stringify(fieldData)
159186
return await tool(
160187
data,
161188
depth as number,
162189
draft as boolean,
163190
locale as string | undefined,
164191
fallbackLocale as string | undefined,
192+
select as string | undefined,
165193
)
166194
},
167195
)

packages/plugin-mcp/src/mcp/tools/resource/find.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2-
import type { PayloadRequest, TypedUser } from 'payload'
2+
import type { PayloadRequest, SelectType, TypedUser } from 'payload'
33

44
import type { PluginMCPServerConfig } from '../../../types.js'
55

@@ -20,6 +20,7 @@ export const findResourceTool = (
2020
page: number = 1,
2121
sort?: string,
2222
where?: string,
23+
select?: string,
2324
depth: number = 0,
2425
locale?: string,
2526
fallbackLocale?: string,
@@ -62,13 +63,34 @@ export const findResourceTool = (
6263
}
6364
}
6465

66+
// Parse select clause if provided
67+
let selectClause: SelectType | undefined
68+
if (select) {
69+
try {
70+
selectClause = JSON.parse(select) as SelectType
71+
} catch (_parseError) {
72+
payload.logger.warn(`[payload-mcp] Invalid select clause JSON: ${select}`)
73+
const response = {
74+
content: [{ type: 'text' as const, text: 'Error: Invalid JSON in select clause' }],
75+
}
76+
return (collections?.[collectionSlug]?.overrideResponse?.(response, {}, req) ||
77+
response) as {
78+
content: Array<{
79+
text: string
80+
type: 'text'
81+
}>
82+
}
83+
}
84+
}
85+
6586
// If ID is provided, use findByID
6687
if (id) {
6788
try {
6889
const doc = await payload.findByID({
6990
id,
7091
collection: collectionSlug,
7192
depth,
93+
...(selectClause && { select: selectClause }),
7294
overrideAccess: false,
7395
req,
7496
user,
@@ -129,6 +151,7 @@ ${JSON.stringify(doc, null, 2)}`,
129151
page,
130152
req,
131153
user,
154+
...(selectClause && { select: selectClause }),
132155
...(locale && { locale }),
133156
...(fallbackLocale && { fallbackLocale }),
134157
...(draft !== undefined && { draft }),
@@ -202,8 +225,19 @@ Page: ${result.page} of ${result.totalPages}
202225
`find${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
203226
`${collections?.[collectionSlug]?.description || toolSchemas.findResources.description.trim()}`,
204227
toolSchemas.findResources.parameters.shape,
205-
async ({ id, depth, draft, fallbackLocale, limit, locale, page, sort, where }) => {
206-
return await tool(id, limit, page, sort, where, depth, locale, fallbackLocale, draft)
228+
async ({ id, depth, draft, fallbackLocale, limit, locale, page, select, sort, where }) => {
229+
return await tool(
230+
id,
231+
limit,
232+
page,
233+
sort,
234+
where,
235+
select,
236+
depth,
237+
locale,
238+
fallbackLocale,
239+
draft,
240+
)
207241
},
208242
)
209243
}

packages/plugin-mcp/src/mcp/tools/resource/update.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
import type { JSONSchema4 } from 'json-schema'
3-
import type { PayloadRequest, TypedUser } from 'payload'
3+
import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

@@ -29,6 +29,7 @@ export const updateResourceTool = (
2929
overwriteExistingFiles: boolean = false,
3030
locale?: string,
3131
fallbackLocale?: string,
32+
select?: string,
3233
): Promise<{
3334
content: Array<{
3435
text: string
@@ -107,6 +108,25 @@ export const updateResourceTool = (
107108
}
108109
}
109110

111+
let selectClause: SelectType | undefined
112+
if (select) {
113+
try {
114+
selectClause = JSON.parse(select) as SelectType
115+
} catch (_parseError) {
116+
payload.logger.warn(`[payload-mcp] Invalid select clause JSON: ${select}`)
117+
const response = {
118+
content: [{ type: 'text' as const, text: 'Error: Invalid JSON in select clause' }],
119+
}
120+
return (collections?.[collectionSlug]?.overrideResponse?.(response, {}, req) ||
121+
response) as {
122+
content: Array<{
123+
text: string
124+
type: 'text'
125+
}>
126+
}
127+
}
128+
}
129+
110130
// Update by ID or where clause
111131
if (id) {
112132
// Single document update
@@ -124,6 +144,7 @@ export const updateResourceTool = (
124144
...(overwriteExistingFiles && { overwriteExistingFiles }),
125145
...(locale && { locale }),
126146
...(fallbackLocale && { fallbackLocale }),
147+
...(selectClause && { select: selectClause }),
127148
}
128149

129150
if (verboseLogs) {
@@ -174,6 +195,7 @@ ${JSON.stringify(result, null, 2)}
174195
...(overwriteExistingFiles && { overwriteExistingFiles }),
175196
...(locale && { locale }),
176197
...(fallbackLocale && { fallbackLocale }),
198+
...(selectClause && { select: selectClause }),
177199
}
178200

179201
if (verboseLogs) {
@@ -296,6 +318,12 @@ ${JSON.stringify(errors, null, 2)}
296318
.optional()
297319
.default(false)
298320
.describe('Whether to overwrite existing files'),
321+
select: z
322+
.string()
323+
.optional()
324+
.describe(
325+
'Optional: define exactly which fields you\'d like to return in the response (JSON), e.g., \'{"title": "My Post"}\'',
326+
),
299327
where: z
300328
.string()
301329
.optional()
@@ -316,6 +344,7 @@ ${JSON.stringify(errors, null, 2)}
316344
locale,
317345
overrideLock,
318346
overwriteExistingFiles,
347+
select,
319348
where,
320349
...fieldData
321350
} = params
@@ -332,6 +361,7 @@ ${JSON.stringify(errors, null, 2)}
332361
overwriteExistingFiles as boolean,
333362
locale as string | undefined,
334363
fallbackLocale as string | undefined,
364+
select as string | undefined,
335365
)
336366
},
337367
)

0 commit comments

Comments
 (0)