Skip to content

Commit 0d14b06

Browse files
feat(plugin-mcp): adds a PayloadRequest to custom tool, prompt, and resource handlers (#14644)
In order to be able to use the payload object or the same access control strategy in our custom tools without having to do additional lookups, we now included a `req` in the custom tool handlers. - Adds a `req` to the custom tool handlers. Previously handlers did not include a `req` argument: ```ts handler: async (args: Record<string, unknown>) => {} ``` Now they do! ```ts handler: async (args: Record<string, unknown>, req:PayloadRequest, _extra) => {} ```
1 parent fe8a3e8 commit 0d14b06

File tree

12 files changed

+603
-65
lines changed

12 files changed

+603
-65
lines changed

docs/plugins/mcp.mdx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ resources: [
154154
description: 'Company content creation guidelines',
155155
uri: 'guidelines://company',
156156
mimeType: 'text/markdown',
157-
handler: (uri) => ({
157+
handler: (uri, req) => ({
158158
contents: [
159159
{
160160
uri: uri.href,
@@ -171,7 +171,7 @@ resources: [
171171
description: 'Access user profile information',
172172
uri: new ResourceTemplate('users://profile/{userId}', { list: undefined }),
173173
mimeType: 'application/json',
174-
handler: async (uri, { userId }) => {
174+
handler: async (uri, { userId }, req) => {
175175
// Fetch user data from your system
176176
const userData = await getUserById(userId)
177177
return {
@@ -196,14 +196,19 @@ tools: [
196196
{
197197
name: 'getPostScores',
198198
description: 'Get useful scores about content in posts',
199-
handler: async (args, { payload }) => {
199+
handler: async (args, req) => {
200+
const { payload } = req
201+
200202
const stats = await payload.find({
201203
collection: 'posts',
202204
where: {
203205
createdAt: {
204206
greater_than: args.since,
205207
},
206208
},
209+
req,
210+
overrideAccess: false,
211+
user: req.user,
207212
})
208213

209214
return {

packages/plugin-mcp/src/endpoints/mcp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const initializeMCPHandler = (pluginOptions: PluginMCPServerConfig) => {
5757
payload.logger.info('[payload-mcp] API Key is valid')
5858
}
5959

60-
return docs[0] as MCPAccessSettings
60+
return docs[0] as unknown as MCPAccessSettings
6161
}
6262

6363
const mcpAccessSettings = pluginOptions.overrideAuth

packages/plugin-mcp/src/mcp/getMcpHandler.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,29 @@ export const getMCPHandler = (
4343
const { payload } = req
4444
const configSchema = configToJSONSchema(payload.config)
4545

46+
// Handler wrapper that injects req before the _extra argument
47+
const wrapHandler = (handler: (...args: any[]) => any) => {
48+
return async (...args: any[]) => {
49+
const _extra = args[args.length - 1]
50+
const handlerArgs = args.slice(0, -1)
51+
return await handler(...handlerArgs, req, _extra)
52+
}
53+
}
54+
55+
const payloadToolHandler = (
56+
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['tools']>[number]['handler'],
57+
) => wrapHandler(handler)
58+
59+
const payloadPromptHandler = (
60+
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['prompts']>[number]['handler'],
61+
) => wrapHandler(handler)
62+
63+
const payloadResourceHandler = (
64+
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['resources']>[number]['handler'],
65+
) => wrapHandler(handler)
66+
4667
// User
47-
const user = mcpAccessSettings.user as TypedUser
68+
const user = mcpAccessSettings.user
4869

4970
// MCP Server and Handler Options
5071
const MCPOptions = pluginOptions.mcp || {}
@@ -202,7 +223,13 @@ export const getMCPHandler = (
202223
registerTool(
203224
isToolEnabled,
204225
tool.name,
205-
() => server.tool(tool.name, tool.description, tool.parameters, tool.handler),
226+
() =>
227+
server.tool(
228+
tool.name,
229+
tool.description,
230+
tool.parameters,
231+
payloadToolHandler(tool.handler),
232+
),
206233
payload,
207234
useVerboseLogs,
208235
)
@@ -222,7 +249,7 @@ export const getMCPHandler = (
222249
description: prompt.description,
223250
title: prompt.title,
224251
},
225-
prompt.handler,
252+
payloadPromptHandler(prompt.handler),
226253
)
227254
if (useVerboseLogs) {
228255
payload.logger.info(`[payload-mcp] ✅ Prompt: ${prompt.title} Registered.`)
@@ -248,7 +275,7 @@ export const getMCPHandler = (
248275
mimeType: resource.mimeType,
249276
title: resource.title,
250277
},
251-
resource.handler,
278+
payloadResourceHandler(resource.handler),
252279
)
253280

254281
if (useVerboseLogs) {

packages/plugin-mcp/src/mcp/helpers/fileValidation.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,14 @@ function getWorkflowConfig(importedModule: Record<string, unknown>): undefined |
202202
* Validate collection configuration structure
203203
*/
204204
function validateCollectionConfig(config: CollectionConfig): ValidationResult<CollectionConfig> {
205-
if (!config || typeof config !== 'object') {
205+
if (!config) {
206206
return {
207207
error: 'Collection config is not a valid object',
208208
success: false,
209209
}
210210
}
211211

212-
if (!config.slug || typeof config.slug !== 'string') {
212+
if (!config.slug) {
213213
return {
214214
error: 'Collection config must have a valid slug property',
215215
success: false,
@@ -220,15 +220,15 @@ function validateCollectionConfig(config: CollectionConfig): ValidationResult<Co
220220
if (config.fields) {
221221
for (let i = 0; i < config.fields.length; i++) {
222222
const field = config.fields[i] as Record<string, unknown>
223-
if (!field || typeof field !== 'object') {
223+
if (!field) {
224224
return {
225225
error: `Field at index ${i} is not a valid object`,
226226
success: false,
227227
}
228228
}
229229

230230
// Check if field has type property
231-
if ('type' in field && field.type && typeof field.type !== 'string') {
231+
if ('type' in field && field.type) {
232232
return {
233233
error: `Field at index ${i} has invalid type property`,
234234
success: false,
@@ -244,29 +244,29 @@ function validateCollectionConfig(config: CollectionConfig): ValidationResult<Co
244244
* Validate task configuration structure
245245
*/
246246
function validateTaskConfig(config: TaskConfig): ValidationResult<TaskConfig> {
247-
if (!config || typeof config !== 'object') {
247+
if (!config) {
248248
return {
249249
error: 'Task config is not a valid object',
250250
success: false,
251251
}
252252
}
253253

254-
if (!config.slug || typeof config.slug !== 'string') {
254+
if (!config.slug) {
255255
return {
256256
error: 'Task config must have a valid slug property',
257257
success: false,
258258
}
259259
}
260260

261-
if (!config.handler || typeof config.handler !== 'function') {
261+
if (!config.handler) {
262262
return {
263263
error: 'Task config must have a valid handler function',
264264
success: false,
265265
}
266266
}
267267

268268
// Validate optional properties
269-
if (config.retries !== undefined && (typeof config.retries !== 'number' || config.retries < 0)) {
269+
if (config.retries !== undefined && config.retries < 0) {
270270
return {
271271
error: 'Task config retries must be a non-negative number',
272272
success: false,
@@ -277,21 +277,21 @@ function validateTaskConfig(config: TaskConfig): ValidationResult<TaskConfig> {
277277
if (config.inputSchema && Array.isArray(config.inputSchema)) {
278278
for (let i = 0; i < config.inputSchema.length; i++) {
279279
const field = config.inputSchema[i]
280-
if (!field || typeof field !== 'object') {
280+
if (!field) {
281281
return {
282282
error: `Input schema field at index ${i} is not a valid object`,
283283
success: false,
284284
}
285285
}
286286

287-
if (!field.name || typeof field.name !== 'string') {
287+
if (!field.name) {
288288
return {
289289
error: `Input schema field at index ${i} must have a valid name property`,
290290
success: false,
291291
}
292292
}
293293

294-
if (!field.type || typeof field.type !== 'string') {
294+
if (!field.type) {
295295
return {
296296
error: `Input schema field at index ${i} must have a valid type property`,
297297
success: false,
@@ -303,21 +303,21 @@ function validateTaskConfig(config: TaskConfig): ValidationResult<TaskConfig> {
303303
if (config.outputSchema && Array.isArray(config.outputSchema)) {
304304
for (let i = 0; i < config.outputSchema.length; i++) {
305305
const field = config.outputSchema[i]
306-
if (!field || typeof field !== 'object') {
306+
if (!field) {
307307
return {
308308
error: `Output schema field at index ${i} is not a valid object`,
309309
success: false,
310310
}
311311
}
312312

313-
if (!field.name || typeof field.name !== 'string') {
313+
if (!field.name) {
314314
return {
315315
error: `Output schema field at index ${i} must have a valid name property`,
316316
success: false,
317317
}
318318
}
319319

320-
if (!field.type || typeof field.type !== 'string') {
320+
if (!field.type) {
321321
return {
322322
error: `Output schema field at index ${i} must have a valid type property`,
323323
success: false,
@@ -333,36 +333,36 @@ function validateTaskConfig(config: TaskConfig): ValidationResult<TaskConfig> {
333333
* Validate workflow configuration structure
334334
*/
335335
function validateWorkflowConfig(config: WorkflowConfig): ValidationResult<WorkflowConfig> {
336-
if (!config || typeof config !== 'object') {
336+
if (!config) {
337337
return {
338338
error: 'Workflow config is not a valid object',
339339
success: false,
340340
}
341341
}
342342

343-
if (!config.slug || typeof config.slug !== 'string') {
343+
if (!config.slug) {
344344
return {
345345
error: 'Workflow config must have a valid slug property',
346346
success: false,
347347
}
348348
}
349349

350-
if (!config.handler || typeof config.handler !== 'function') {
350+
if (!config.handler) {
351351
return {
352352
error: 'Workflow config must have a valid handler function',
353353
success: false,
354354
}
355355
}
356356

357357
// Validate optional properties
358-
if (config.queue && typeof config.queue !== 'string') {
358+
if (config.queue) {
359359
return {
360360
error: 'Workflow config queue must be a string',
361361
success: false,
362362
}
363363
}
364364

365-
if (config.retries !== undefined && (typeof config.retries !== 'number' || config.retries < 0)) {
365+
if (config.retries !== undefined && config.retries < 0) {
366366
return {
367367
error: 'Workflow config retries must be a non-negative number',
368368
success: false,
@@ -373,21 +373,21 @@ function validateWorkflowConfig(config: WorkflowConfig): ValidationResult<Workfl
373373
if (config.inputSchema && Array.isArray(config.inputSchema)) {
374374
for (let i = 0; i < config.inputSchema.length; i++) {
375375
const field = config.inputSchema[i]
376-
if (!field || typeof field !== 'object') {
376+
if (!field) {
377377
return {
378378
error: `Input schema field at index ${i} is not a valid object`,
379379
success: false,
380380
}
381381
}
382382

383-
if (!field.name || typeof field.name !== 'string') {
383+
if (!field.name) {
384384
return {
385385
error: `Input schema field at index ${i} must have a valid name property`,
386386
success: false,
387387
}
388388
}
389389

390-
if (!field.type || typeof field.type !== 'string') {
390+
if (!field.type) {
391391
return {
392392
error: `Input schema field at index ${i} must have a valid type property`,
393393
success: false,

packages/plugin-mcp/src/types.ts

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CollectionConfig, CollectionSlug, PayloadRequest } from 'payload'
1+
import type { CollectionConfig, CollectionSlug, PayloadRequest, TypedUser } from 'payload'
22
import type { z } from 'zod'
33

44
import { type ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
@@ -135,7 +135,29 @@ export type PluginMCPServerConfig = {
135135
/**
136136
* Set the handler of the prompt. This is the function that will be called when the prompt is used.
137137
*/
138-
handler: (...args: any) => any
138+
handler: (
139+
args: Record<string, unknown>,
140+
req: PayloadRequest,
141+
_extra: unknown,
142+
) =>
143+
| {
144+
messages: Array<{
145+
content: {
146+
text: string
147+
type: 'text'
148+
}
149+
role: 'assistant' | 'user'
150+
}>
151+
}
152+
| Promise<{
153+
messages: Array<{
154+
content: {
155+
text: string
156+
type: 'text'
157+
}
158+
role: 'assistant' | 'user'
159+
}>
160+
}>
139161
/**
140162
* Set the function name of the prompt.
141163
*/
@@ -157,8 +179,21 @@ export type PluginMCPServerConfig = {
157179
description: string
158180
/**
159181
* Set the handler of the resource. This is the function that will be called when the resource is used.
182+
* The handler can have either 3 arguments (when no args are passed) or 4 arguments (when args are passed).
160183
*/
161-
handler: (...args: any) => any
184+
handler: (...args: any[]) =>
185+
| {
186+
contents: Array<{
187+
text: string
188+
uri: string
189+
}>
190+
}
191+
| Promise<{
192+
contents: Array<{
193+
text: string
194+
uri: string
195+
}>
196+
}>
162197
/**
163198
* Set the mime type of the resource.
164199
* example: 'text/plain'
@@ -192,12 +227,25 @@ export type PluginMCPServerConfig = {
192227
/**
193228
* Set the handler of the tool. This is the function that will be called when the tool is used.
194229
*/
195-
handler: (args: Record<string, unknown>) => Promise<{
196-
content: Array<{
197-
text: string
198-
type: 'text'
199-
}>
200-
}>
230+
handler: (
231+
args: Record<string, unknown>,
232+
req: PayloadRequest,
233+
_extra: unknown,
234+
) =>
235+
| {
236+
content: Array<{
237+
text: string
238+
type: 'text'
239+
}>
240+
role?: string
241+
}
242+
| Promise<{
243+
content: Array<{
244+
text: string
245+
type: 'text'
246+
}>
247+
role?: string
248+
}>
201249
/**
202250
* Set the name of the tool. This is the name that will be used to identify the tool. LLMs will interperate the name to determine when to use the tool.
203251
*/
@@ -309,6 +357,7 @@ export type MCPAccessSettings = {
309357
'payload-mcp-prompt'?: Record<string, boolean>
310358
'payload-mcp-resource'?: Record<string, boolean>
311359
'payload-mcp-tool'?: Record<string, boolean>
360+
user: TypedUser
312361
} & Record<string, unknown>
313362

314363
export type FieldDefinition = {

0 commit comments

Comments
 (0)