Skip to content

Commit 860bdf2

Browse files
feat(plugin-mcp): adds MCP context to req.payloadAPI (#14595)
- Adds `MCP` option to `req.payloadAPI` Users can write a conditional to detect when the Payload API context is 'MCP' related. ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', admin: { description: 'The title of the post', }, required: true, }, // ... other fields ], hooks: { beforeRead: [ ({ doc, req }) => { if (req.payloadAPI === 'MCP') { doc.title = `${doc.title} (MCP Hook Override)` } return doc }, ], }, }
1 parent 6070d8d commit 860bdf2

File tree

10 files changed

+73
-6
lines changed

10 files changed

+73
-6
lines changed

packages/payload/src/types/index.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ type PayloadRequestData = {
114114
tempFilePath?: string
115115
}
116116
}
117-
export type PayloadRequest = CustomPayloadRequestProperties &
118-
Partial<Request> &
119-
PayloadRequestData &
120-
Required<Pick<Request, 'headers'>>
117+
export interface PayloadRequest
118+
extends CustomPayloadRequestProperties,
119+
Partial<Request>,
120+
PayloadRequestData {
121+
headers: Request['headers']
122+
}
121123

122124
export type { Operator }
123125

packages/payload/src/utilities/createLocalReq.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ export const createLocalReq: CreateLocalReq = async (
123123
(await getLocalI18n({ config: payload.config, language: payload.config.i18n.fallbackLanguage }))
124124

125125
if (!req.headers) {
126-
// @ts-expect-error eslint-disable-next-line no-param-reassign
127126
req.headers = new Headers()
128127
}
129128

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const initializeMCPHandler = (pluginOptions: PluginMCPServerConfig) => {
1313
const MCPHandlerOptions = MCPOptions.handlerOptions || {}
1414
const useVerboseLogs = MCPHandlerOptions.verboseLogs ?? false
1515

16+
req.payloadAPI = 'MCP' as const
17+
1618
const getDefaultMcpAccessSettings = async (overrideApiKey?: null | string) => {
1719
const apiKey =
1820
(overrideApiKey ?? req.headers.get('Authorization')?.startsWith('Bearer '))

packages/plugin-mcp/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import type { MCPAccessSettings, PluginMCPServerConfig } from './types.js'
55
import { createAPIKeysCollection } from './collections/createApiKeysCollection.js'
66
import { initializeMCPHandler } from './endpoints/mcp.js'
77

8+
declare module 'payload' {
9+
export interface PayloadRequest {
10+
payloadAPI: 'GraphQL' | 'local' | 'MCP' | 'REST'
11+
}
12+
}
13+
814
export type { MCPAccessSettings }
915
/**
1016
* The MCP Plugin for Payload. This plugin allows you to add MCP capabilities to your Payload project.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export const createResourceTool = (
5050
// Create the resource
5151
const result = await payload.create({
5252
collection: collectionSlug,
53+
req,
5354
// TODO: Move the override to a `beforeChange` hook and extend the payloadAPI context req to include MCP request info.
5455
data: collections?.[collectionSlug]?.override?.(parsedData, req) || parsedData,
5556
overrideAccess: false,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export const deleteResourceTool = (
7878
collection: collectionSlug,
7979
depth,
8080
overrideAccess: false,
81+
req,
8182
user,
8283
}
8384

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const findResourceTool = (
6565
id,
6666
collection: collectionSlug,
6767
overrideAccess: false,
68+
req,
6869
user,
6970
})
7071

@@ -117,6 +118,7 @@ ${JSON.stringify(doc, null, 2)}`,
117118
limit,
118119
overrideAccess: false,
119120
page,
121+
req,
120122
user,
121123
}
122124

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export const updateResourceTool = (
116116
draft,
117117
overrideAccess: false,
118118
overrideLock,
119+
req,
119120
user,
120121
...(filePath && { filePath }),
121122
...(overwriteExistingFiles && { overwriteExistingFiles }),
@@ -162,6 +163,7 @@ ${JSON.stringify(result, null, 2)}
162163
draft,
163164
overrideAccess: false,
164165
overrideLock,
166+
req,
165167
user,
166168
where: whereClause,
167169
...(filePath && { filePath }),

test/plugin-mcp/collections/Posts.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,14 @@ export const Posts: CollectionConfig = {
2828
},
2929
},
3030
],
31+
hooks: {
32+
beforeRead: [
33+
({ doc, req }) => {
34+
if (req.payloadAPI === 'MCP') {
35+
doc.title = `${doc.title} (MCP Hook Override)`
36+
}
37+
return doc
38+
},
39+
],
40+
},
3141
}

test/plugin-mcp/int.spec.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,50 @@ describe('@payloadcms/plugin-mcp', () => {
401401
expect(json.result.content[0].text).toContain('Total: 1 documents')
402402
expect(json.result.content[0].text).toContain('Page: 1 of 1')
403403
expect(json.result.content[0].text).toContain('```json')
404-
expect(json.result.content[0].text).toContain('"title": "Test Post for Finding"')
404+
expect(json.result.content[0].text).toContain('"content": "Content for test post."')
405405
expect(json.result.content[1].type).toBe('text')
406406
expect(json.result.content[1].text).toContain('Override MCP response for Posts!')
407407
})
408+
409+
it('should call operations with the payloadAPI context as MCP', async () => {
410+
await payload.create({
411+
collection: 'posts',
412+
data: {
413+
title: 'Test Post for Finding',
414+
content: 'Content for test post.',
415+
},
416+
})
417+
418+
const apiKey = await getApiKey()
419+
const response = await restClient.POST('/mcp', {
420+
headers: {
421+
Authorization: `Bearer ${apiKey}`,
422+
Accept: 'application/json, text/event-stream',
423+
'Content-Type': 'application/json',
424+
},
425+
body: JSON.stringify({
426+
id: 1,
427+
jsonrpc: '2.0',
428+
method: 'tools/call',
429+
params: {
430+
name: 'findPosts',
431+
arguments: {
432+
limit: 1,
433+
page: 1,
434+
where: '{"title": {"contains": "Test Post for Finding"}}',
435+
},
436+
},
437+
}),
438+
})
439+
440+
const json = await parseStreamResponse(response)
441+
442+
expect(json).toBeDefined()
443+
expect(json.result).toBeDefined()
444+
expect(json.result.content).toHaveLength(2)
445+
expect(json.result.content[0].type).toBe('text')
446+
expect(json.result.content[0].text).toContain(
447+
'"title": "Test Post for Finding (MCP Hook Override)"',
448+
)
449+
})
408450
})

0 commit comments

Comments
 (0)