Skip to content

Commit fc2becf

Browse files
fix(plugin-mcp): correctly uses local access controls (#14457)
Fix: - Local requests correctly honor user access controls on CRUD tools Feat: - Allows config of a custom user collection with API Keys - adds allow/disallow capability for custom `resources` and `prompts` - tool descriptions are now optional -- we use a default description if one isn't in the config
1 parent 5bf3bd4 commit fc2becf

File tree

12 files changed

+276
-103
lines changed

12 files changed

+276
-103
lines changed

packages/plugin-mcp/src/collections/createApiKeysCollection.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ const addEnabledCollectionTools = (collections: PluginMCPServerConfig['collectio
106106
]
107107
: []),
108108
],
109+
label: false as const,
109110
},
110111
],
111112
label: `${enabledCollectionSlug.charAt(0).toUpperCase() + toCamelCase(enabledCollectionSlug).slice(1)}`,
@@ -116,6 +117,7 @@ export const createAPIKeysCollection = (
116117
collections: PluginMCPServerConfig['collections'],
117118
customTools: Array<{ description: string; name: string }> = [],
118119
experimentalTools: NonNullable<PluginMCPServerConfig['experimental']>['tools'] = {},
120+
pluginOptions: PluginMCPServerConfig,
119121
): CollectionConfig => {
120122
const customToolsFields = customTools.map((tool) => {
121123
const camelCasedName = toCamelCase(tool.name)
@@ -130,6 +132,40 @@ export const createAPIKeysCollection = (
130132
}
131133
})
132134

135+
const customResourceFields =
136+
pluginOptions.mcp?.resources?.map((resource) => {
137+
const camelCasedName = toCamelCase(resource.name)
138+
return {
139+
name: camelCasedName,
140+
type: 'checkbox' as const,
141+
admin: {
142+
description: resource.description,
143+
},
144+
defaultValue: true,
145+
label: camelCasedName,
146+
}
147+
}) || []
148+
149+
const customPromptFields =
150+
pluginOptions.mcp?.prompts?.map((prompt) => {
151+
const camelCasedName = toCamelCase(prompt.name)
152+
return {
153+
name: camelCasedName,
154+
type: 'checkbox' as const,
155+
admin: {
156+
description: prompt.description,
157+
},
158+
defaultValue: true,
159+
label: camelCasedName,
160+
}
161+
}) || []
162+
163+
const userCollection = pluginOptions.userCollection
164+
? typeof pluginOptions.userCollection === 'string'
165+
? pluginOptions.userCollection
166+
: pluginOptions.userCollection.slug
167+
: 'users'
168+
133169
return {
134170
slug: 'payload-mcp-api-keys',
135171
admin: {
@@ -147,7 +183,7 @@ export const createAPIKeysCollection = (
147183
admin: {
148184
description: 'The user that the API key is associated with.',
149185
},
150-
relationTo: 'users',
186+
relationTo: userCollection,
151187
required: true,
152188
},
153189
{
@@ -176,12 +212,53 @@ export const createAPIKeysCollection = (
176212
},
177213
fields: [
178214
{
179-
name: 'custom',
215+
name: 'payload-mcp-tool',
180216
type: 'group' as const,
181217
fields: customToolsFields,
218+
label: false as const,
219+
},
220+
],
221+
label: 'Tools',
222+
},
223+
]
224+
: []),
225+
226+
...(pluginOptions.mcp?.resources && pluginOptions.mcp?.resources.length > 0
227+
? [
228+
{
229+
type: 'collapsible' as const,
230+
admin: {
231+
position: 'sidebar' as const,
232+
},
233+
fields: [
234+
{
235+
name: 'payload-mcp-resource',
236+
type: 'group' as const,
237+
fields: customResourceFields,
238+
label: false as const,
239+
},
240+
],
241+
label: 'Resources',
242+
},
243+
]
244+
: []),
245+
246+
...(pluginOptions.mcp?.prompts && pluginOptions.mcp?.prompts.length > 0
247+
? [
248+
{
249+
type: 'collapsible' as const,
250+
admin: {
251+
position: 'sidebar' as const,
252+
},
253+
fields: [
254+
{
255+
name: 'payload-mcp-prompt',
256+
type: 'group' as const,
257+
fields: customPromptFields,
258+
label: false as const,
182259
},
183260
],
184-
label: 'Custom Tools',
261+
label: 'Prompts',
185262
},
186263
]
187264
: []),

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import crypto from 'crypto'
22
import { APIError, type PayloadHandler, type Where } from 'payload'
33

4-
import type { PluginMCPServerConfig, ToolSettings } from '../types.js'
4+
import type { MCPAccessSettings, PluginMCPServerConfig } from '../types.js'
55

66
import { createRequestFromPayloadRequest } from '../mcp/createRequest.js'
77
import { getMCPHandler } from '../mcp/getMcpHandler.js'
@@ -46,13 +46,13 @@ export const initializeMCPHandler = (pluginOptions: PluginMCPServerConfig) => {
4646
throw new APIError('API Key is invalid', 401)
4747
}
4848

49-
const toolSettings = docs[0] as ToolSettings
49+
const mcpAccessSettings = docs[0] as MCPAccessSettings
5050

5151
if (useVerboseLogs) {
5252
payload.logger.info('[payload-mcp] API Key is valid')
5353
}
5454

55-
const handler = getMCPHandler(pluginOptions, toolSettings, req)
55+
const handler = getMCPHandler(pluginOptions, mcpAccessSettings, req)
5656
const request = createRequestFromPayloadRequest(req)
5757
return await handler(request)
5858
}

packages/plugin-mcp/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ export const mcpPlugin =
4242
* - If a custom tool has gone haywire, admins can disallow that tool.
4343
*
4444
*/
45-
const apiKeyCollection = createAPIKeysCollection(collections, customTools, experimentalTools)
45+
const apiKeyCollection = createAPIKeysCollection(
46+
collections,
47+
customTools,
48+
experimentalTools,
49+
pluginOptions,
50+
)
4651
if (pluginOptions.overrideApiKeyCollection) {
4752
config.collections.push(pluginOptions.overrideApiKeyCollection(apiKeyCollection))
4853
} else {

0 commit comments

Comments
 (0)