Skip to content

Commit ac4fc31

Browse files
authored
feat(plugin-mcp): allow external plugins to extend mcp plugin (#16245)
Plugins can now extend plugin-mcp (e.g. inject custom MCP tools). A plugin finds plugin-mcp by slug via `config.plugins`, accesses its options, and pushes tools directly into `options.mcp.tools`. Because plugin-mcp uses a high priority (runs last), the injected tools are available when it builds the MCP server. Also exports `MCPPluginConfig` type so external plugins can type the options object when injecting tools. Since it wasn't exported before, I took the opportunity to rename `PluginMCPServerConfig` => `MCPPluginConfig`. This is simpler and more consistent with type from our other plugins
1 parent d1837ad commit ac4fc31

13 files changed

Lines changed: 113 additions & 36 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import type { CollectionConfig, CollectionSlug } from 'payload'
22

3-
import type { PluginMCPServerConfig } from '../types.js'
3+
import type { MCPPluginConfig } from '../types.js'
44

55
import { toCamelCase } from '../utils/camelCase.js'
66
import { createApiKeyFields } from '../utils/createApiKeyFields.js'
77

88
export const createAPIKeysCollection = (
9-
collections: PluginMCPServerConfig['collections'],
10-
globals: PluginMCPServerConfig['globals'],
9+
collections: MCPPluginConfig['collections'],
10+
globals: MCPPluginConfig['globals'],
1111
customTools: Array<{ description: string; name: string }> = [],
12-
experimentalTools: NonNullable<PluginMCPServerConfig['experimental']>['tools'] = {},
13-
pluginOptions: PluginMCPServerConfig,
12+
experimentalTools: NonNullable<MCPPluginConfig['experimental']>['tools'] = {},
13+
pluginOptions: MCPPluginConfig,
1414
): CollectionConfig => {
1515
const customToolsFields = customTools.map((tool) => {
1616
const camelCasedName = toCamelCase(tool.name)

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

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

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

66
import { createRequestFromPayloadRequest } from '../mcp/createRequest.js'
77
import { getMCPHandler } from '../mcp/getMcpHandler.js'
88

9-
export const initializeMCPHandler = (pluginOptions: PluginMCPServerConfig) => {
9+
export const initializeMCPHandler = (pluginOptions: MCPPluginConfig) => {
1010
const mcpHandler: PayloadHandler = async (req) => {
1111
const { payload } = req
1212
const MCPOptions = pluginOptions.mcp || {}

packages/plugin-mcp/src/index.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { Config } from 'payload'
1+
import { definePlugin } from 'payload'
22

3-
import type { MCPAccessSettings, PluginMCPServerConfig } from './types.js'
3+
import type { MCPAccessSettings, MCPPluginConfig } from './types.js'
44

55
import { createAPIKeysCollection } from './collections/createApiKeysCollection.js'
66
import { initializeMCPHandler } from './endpoints/mcp.js'
@@ -9,19 +9,24 @@ declare module 'payload' {
99
export interface PayloadRequest {
1010
payloadAPI: 'GraphQL' | 'local' | 'MCP' | 'REST'
1111
}
12+
interface RegisteredPlugins {
13+
'@payloadcms/plugin-mcp': MCPPluginConfig
14+
}
1215
}
1316

1417
import { defaults } from './defaults.js'
1518

16-
export type { MCPAccessSettings }
19+
export type { MCPAccessSettings, MCPPluginConfig }
20+
1721
/**
1822
* The MCP Plugin for Payload. This plugin allows you to add MCP capabilities to your Payload project.
1923
*
2024
* @param pluginOptions - The options for the MCP plugin.
2125
*/
22-
export const mcpPlugin =
23-
(pluginOptions: PluginMCPServerConfig) =>
24-
(config: Config): Config => {
26+
export const mcpPlugin = definePlugin<MCPPluginConfig>({
27+
slug: '@payloadcms/plugin-mcp',
28+
order: 10,
29+
plugin: ({ config, plugins: _plugins, ...pluginOptions }) => {
2530
if (!config.collections) {
2631
config.collections = []
2732
}
@@ -104,4 +109,5 @@ export const mcpPlugin =
104109
})
105110

106111
return config
107-
}
112+
},
113+
})

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createMcpHandler } from 'mcp-handler'
44
import { join } from 'path'
55
import { APIError, configToJSONSchema, type PayloadRequest, type TypedUser } from 'payload'
66

7-
import type { MCPAccessSettings, PluginMCPServerConfig } from '../types.js'
7+
import type { MCPAccessSettings, MCPPluginConfig } from '../types.js'
88

99
import { toCamelCase } from '../utils/camelCase.js'
1010
import { getEnabledSlugs } from '../utils/getEnabledSlugs.js'
@@ -44,7 +44,7 @@ import { runJobTool } from './tools/job/run.js'
4444
import { updateJobTool } from './tools/job/update.js'
4545

4646
export const getMCPHandler = (
47-
pluginOptions: PluginMCPServerConfig,
47+
pluginOptions: MCPPluginConfig,
4848
mcpAccessSettings: MCPAccessSettings,
4949
req: PayloadRequest,
5050
) => {
@@ -63,15 +63,15 @@ export const getMCPHandler = (
6363
}
6464

6565
const payloadToolHandler = (
66-
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['tools']>[number]['handler'],
66+
handler: NonNullable<NonNullable<MCPPluginConfig['mcp']>['tools']>[number]['handler'],
6767
) => wrapHandler(handler)
6868

6969
const payloadPromptHandler = (
70-
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['prompts']>[number]['handler'],
70+
handler: NonNullable<NonNullable<MCPPluginConfig['mcp']>['prompts']>[number]['handler'],
7171
) => wrapHandler(handler)
7272

7373
const payloadResourceHandler = (
74-
handler: NonNullable<NonNullable<PluginMCPServerConfig['mcp']>['resources']>[number]['handler'],
74+
handler: NonNullable<NonNullable<MCPPluginConfig['mcp']>['resources']>[number]['handler'],
7575
) => wrapHandler(handler)
7676

7777
// User
@@ -88,7 +88,7 @@ export const getMCPHandler = (
8888

8989
// Experimental MCP Tool Requirements
9090
const isDevelopment = process.env.NODE_ENV === 'development'
91-
const experimentalTools: NonNullable<PluginMCPServerConfig['experimental']>['tools'] =
91+
const experimentalTools: NonNullable<MCPPluginConfig['experimental']>['tools'] =
9292
pluginOptions?.experimental?.tools || {}
9393
const collectionsPluginConfig = pluginOptions.collections || {}
9494
const globalsPluginConfig = pluginOptions.globals || {}

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

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

4-
import type { PluginMCPServerConfig } from '../../../types.js'
4+
import type { MCPPluginConfig } from '../../../types.js'
55

66
import { toCamelCase } from '../../../utils/camelCase.js'
77
import { toolSchemas } from '../schemas.js'
@@ -12,7 +12,7 @@ export const findGlobalTool = (
1212
user: TypedUser,
1313
verboseLogs: boolean,
1414
globalSlug: string,
15-
globals: PluginMCPServerConfig['globals'],
15+
globals: MCPPluginConfig['globals'],
1616
) => {
1717
const tool = async (
1818
depth: number = 0,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

7-
import type { PluginMCPServerConfig } from '../../../types.js'
7+
import type { MCPPluginConfig } from '../../../types.js'
88

99
import { toCamelCase } from '../../../utils/camelCase.js'
1010
import {
@@ -20,7 +20,7 @@ export const updateGlobalTool = (
2020
user: TypedUser,
2121
verboseLogs: boolean,
2222
globalSlug: string,
23-
globals: PluginMCPServerConfig['globals'],
23+
globals: MCPPluginConfig['globals'],
2424
schema: JSONSchema4,
2525
) => {
2626
const tool = async (

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

7-
import type { PluginMCPServerConfig } from '../../../types.js'
7+
import type { MCPPluginConfig } from '../../../types.js'
88

99
import { toCamelCase } from '../../../utils/camelCase.js'
1010
import {
@@ -20,7 +20,7 @@ export const createResourceTool = (
2020
user: TypedUser,
2121
verboseLogs: boolean,
2222
collectionSlug: string,
23-
collections: PluginMCPServerConfig['collections'],
23+
collections: MCPPluginConfig['collections'],
2424
schema: JSONSchema4,
2525
) => {
2626
const tool = async (

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

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

4-
import type { PluginMCPServerConfig } from '../../../types.js'
4+
import type { MCPPluginConfig } from '../../../types.js'
55

66
import { toCamelCase } from '../../../utils/camelCase.js'
77
import { toolSchemas } from '../schemas.js'
@@ -12,7 +12,7 @@ export const deleteResourceTool = (
1212
user: TypedUser,
1313
verboseLogs: boolean,
1414
collectionSlug: string,
15-
collections: PluginMCPServerConfig['collections'],
15+
collections: MCPPluginConfig['collections'],
1616
) => {
1717
const tool = async (
1818
id?: number | string,

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

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

4-
import type { PluginMCPServerConfig } from '../../../types.js'
4+
import type { MCPPluginConfig } from '../../../types.js'
55

66
import { toCamelCase } from '../../../utils/camelCase.js'
77
import { toolSchemas } from '../schemas.js'
@@ -12,7 +12,7 @@ export const findResourceTool = (
1212
user: TypedUser,
1313
verboseLogs: boolean,
1414
collectionSlug: string,
15-
collections: PluginMCPServerConfig['collections'],
15+
collections: MCPPluginConfig['collections'],
1616
) => {
1717
const tool = async (
1818
id?: number | string,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { PayloadRequest, SelectType, TypedUser } from 'payload'
44

55
import { z } from 'zod'
66

7-
import type { PluginMCPServerConfig } from '../../../types.js'
7+
import type { MCPPluginConfig } from '../../../types.js'
88

99
import { toCamelCase } from '../../../utils/camelCase.js'
1010
import {
@@ -20,7 +20,7 @@ export const updateResourceTool = (
2020
user: TypedUser,
2121
verboseLogs: boolean,
2222
collectionSlug: string,
23-
collections: PluginMCPServerConfig['collections'],
23+
collections: MCPPluginConfig['collections'],
2424
schema: JSONSchema4,
2525
) => {
2626
const tool = async (

0 commit comments

Comments
 (0)