docs/plugins/mcp.mdx
This plugin adds Model Context Protocol capabilities.
<Banner type="info"> This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/3.x/packages/plugin-mcp). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%mcp&template=bug_report.md&title=plugin-mcp%3A) with as much detail as possible. </Banner>find, create, update, and delete operations for each collectionfind and update operations for each globalInstall the plugin using any JavaScript package manager like pnpm, npm, or Yarn:
pnpm add @payloadcms/plugin-mcp
In the plugins array of your Payload Config, call the plugin with options:
import { buildConfig } from 'payload'
import { mcpPlugin } from '@payloadcms/plugin-mcp'
const config = buildConfig({
collections: [
{
slug: 'posts',
fields: [],
},
],
plugins: [
mcpPlugin({
collections: {
posts: {
enabled: true,
},
},
}),
],
})
export default config
| Option | Type | Description |
|---|---|---|
collections | object | An object of collection slugs to use for MCP capabilities. |
collections[slug] | object | An object of collection slugs to use for MCP capabilities. |
collections[slug].description | string | A description for the collection. |
collections[slug].overrideResponse | function | A function that allows you to override the response from the operation tool call. |
collections[slug].enabled | object or boolean | Determines whether the model can find, create, update, and delete documents in the collection. |
collections[slug].enabled.find | boolean | Whether to allow the model to find documents in the collection. |
collections[slug].enabled.create | boolean | Whether to allow the model to create documents in the collection. |
collections[slug].enabled.update | boolean | Whether to allow the model to update documents in the collection. |
collections[slug].enabled.delete | boolean | Whether to allow the model to delete documents in the collection. |
globals | object | An object of global slugs to expose via MCP. Globals only support find and update. |
globals[slug].description | string | A description for the global shown to models. |
globals[slug].overrideResponse | function | A function that allows you to override the response from the operation tool call. |
globals[slug].enabled | object or boolean | Determines whether the model can find or update the global. |
globals[slug].enabled.find | boolean | Whether to allow the model to read the global. |
globals[slug].enabled.update | boolean | Whether to allow the model to update the global. |
disabled | boolean | Disable the MCP plugin while keeping database schema consistent. |
userCollection | CollectionSlug | The users collection that API keys are associated with. Defaults to config.admin.user. |
overrideApiKeyCollection | function | A function that allows you to override the automatically generated API Keys collection. |
overrideAuth | function | Replace the default Bearer-token / API-key auth with a completely custom access strategy. |
mcp | object | MCP options that allow you to customize the MCP server. |
mcp.tools | array | An array of tools to add to the MCP server. |
mcp.tools.name | string | The name of the tool. |
mcp.tools.description | string | The description of the tool. |
mcp.tools.handler | function | The handler function for the tool. |
mcp.tools.parameters | object | The parameters for the tool (Zod schema). |
mcp.prompts | array | An array of prompts to add to the MCP server. |
mcp.prompts.name | string | The name of the prompt. |
mcp.prompts.title | string | The title of the prompt (used by models to determine when to use it). |
mcp.prompts.description | string | The description of the prompt. |
mcp.prompts.handler | function | The handler function for the prompt. |
mcp.prompts.argsSchema | object | The arguments schema for the prompt (Zod schema). |
mcp.resources | array | An array of resources to add to the MCP server. |
mcp.resources.name | string | The name of the resource. |
mcp.resources.title | string | The title of the resource (used by models to determine when to use it). |
mcp.resources.description | string | The description of the resource. |
mcp.resources.handler | function | The handler function for the resource. |
mcp.resources.uri | string or object | The URI of the resource (can be a string or ResourceTemplate for dynamic URIs). |
mcp.resources.mimeType | string | The MIME type of the resource. |
mcp.handlerOptions | object | The handler options for the MCP server. |
mcp.handlerOptions.verboseLogs | boolean | Whether to log verbose logs to the console (default: false). |
mcp.handlerOptions.maxDuration | number | The maximum duration for the MCP server requests in seconds (default: 60). |
mcp.handlerOptions.onEvent | function | Callback invoked for every MCP event. Useful for analytics and audit logging. |
mcp.serverOptions | object | The server options for the MCP server. |
mcp.serverOptions.serverInfo | object | The server info for the MCP server. |
mcp.serverOptions.serverInfo.name | string | The name of the MCP server (default: 'Payload MCP Server'). |
mcp.serverOptions.serverInfo.version | string | The version of the MCP server (default: '1.0.0'). |
Step 1 — Enable in your config
Add the collection or global to the plugin with enabled: true (or a capabilities object):
mcpPlugin({
collections: {
posts: { enabled: true },
},
globals: {
'site-settings': { enabled: { find: true, update: true } },
},
})
Step 2 — Allow in the API Key
In your Payload admin panel, navigate to MCP → API Keys, create a new key, and toggle the individual capabilities on for each collection, global, tool, prompt, and resource you want that key to be able to use.
All MCP requests must include a valid API key as a Bearer token — requests without
one are rejected immediately. Here is a complete example of an MCP tools/list request
showing the correct headers:
curl -i 'http://localhost:3000/api/mcp' \
-X POST \
-H 'Authorization: Bearer MCP-USER-API-KEY' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}'
Access controls are also enforced at the Payload level using the user associated with the API key, so existing collection access rules, hooks, and multi-tenant restrictions all continue to apply.
After installing and configuring the plugin, you can connect apps with MCP client capabilities to Payload.
http://localhost:3000/adminMCP Clients can be configured to interact with your MCP server. These clients require some JSON configuration, or platform configuration in order to know how to reach your MCP server.
<Banner type="warning"> Caution: the format of these JSON files may change over time. Please check the client website for updates. </Banner>Our recommended approach to make your server available for most MCP clients is to use the mcp-remote package via npx.
Below are configuration examples for popular MCP clients.
{
"mcp.servers": {
"Payload": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://127.0.0.1:3000/api/mcp",
"--header",
"Authorization: Bearer MCP-USER-API-KEY"
]
}
}
}
{
"mcpServers": {
"Payload": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"http://localhost:3000/api/mcp",
"--header",
"Authorization: Bearer MCP-USER-API-KEY"
]
}
}
}
claude mcp add --transport http Payload http://127.0.0.1:3000/api/mcp \
--header "Authorization: Bearer MCP-USER-API-KEY"
For connections without using mcp-remote you can use this configuration format:
{
"mcpServers": {
"Payload": {
"type": "http",
"url": "http://localhost:3000/api/mcp",
"headers": {
"Authorization": "Bearer MCP-USER-API-KEY"
}
}
}
}
The MCP Inspector is the recommended way to explore and test your MCP server interactively:
npx @modelcontextprotocol/inspector
Open the inspector, set the URL to http://127.0.0.1:3000/api/mcp, and add an
Authorization: Bearer MCP-USER-API-KEY header.
You can also test directly with curl:
curl -i 'http://localhost:3000/api/mcp' \
-X POST \
-H 'Authorization: Bearer MCP-USER-API-KEY' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}'
The plugin supports fully custom prompts, tools and resources that can be called or retrieved by MCP clients.
After defining a custom method you can allow / disallow the feature from the admin panel by adjusting the API Key MCP Options checklist.
Globals are singleton configuration objects (e.g. site settings, navigation). The plugin
exposes them as find and update tools — one tool per global per operation.
mcpPlugin({
globals: {
'site-settings': {
enabled: {
find: true,
update: true,
},
description:
'Site-wide configuration settings including name, description, and maintenance mode.',
},
},
})
This produces two MCP tools: findSiteSettings and updateSiteSettings. Globals do not
support create or delete because they are singletons managed by Payload.
Prompts allow models to generate structured messages for specific tasks. Each prompt defines a schema for arguments and returns formatted messages:
prompts: [
{
name: 'reviewContent',
title: 'Content Review Prompt',
description: 'Creates a prompt for reviewing content quality',
argsSchema: {
content: z.string().describe('The content to review'),
criteria: z.array(z.string()).describe('Review criteria'),
},
handler: ({ content, criteria }, req) => ({
messages: [
{
content: {
type: 'text',
text: `Please review this content based on the following criteria: ${criteria.join(', ')}\n\nContent: ${content}`,
},
role: 'user',
},
],
}),
},
]
Resources provide access to data or content that models can read. They can be static or dynamic with parameterized URIs:
resources: [
// Static resource
{
name: 'guidelines',
title: 'Content Guidelines',
description: 'Company content creation guidelines',
uri: 'guidelines://company',
mimeType: 'text/markdown',
handler: (uri, req) => ({
contents: [
{
uri: uri.href,
text: '# Content Guidelines\n\n1. Keep it concise\n2. Use clear language',
},
],
}),
},
// Dynamic resource with template
{
name: 'userProfile',
title: 'User Profile',
description: 'Access user profile information',
uri: new ResourceTemplate('users://profile/{userId}', { list: undefined }),
mimeType: 'application/json',
handler: async (uri, { userId }, req) => {
// Fetch user data from your system
const userData = await getUserById(userId)
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(userData, null, 2),
},
],
}
},
},
]
Tools allow you to extend MCP capabilities beyond basic CRUD operations. Use them when you need to perform complex queries, aggregations, or business logic that isn't covered by the standard collection operations.
Every tool handler receives (args, req) where req is the full Payload
PayloadRequest object. From it you have access to:
| Property | Description |
|---|---|
req.payload | The initialised Payload instance — use it to call payload.find, payload.create, payload.update, etc. |
req.user | The authenticated user making the request (the MCP API key owner). |
req.locale | The active locale, if localisation is enabled. |
req.headers | Incoming request headers. |
Always pass req through to Payload operations and set overrideAccess: false so the
request inherits the API key owner's access control rules.
tools: [
{
name: 'getPostScores',
description: 'Get useful scores about content in posts',
handler: async (args, req) => {
const { payload } = req
const stats = await payload.find({
collection: 'posts',
where: {
createdAt: {
greater_than: args.since,
},
},
req,
overrideAccess: false,
user: req.user,
})
return {
content: [
{
type: 'text',
text: `Found ${stats.totalDocs} posts created since ${args.since}`,
},
],
}
},
parameters: z.object({
since: z.string().describe('ISO date string for filtering posts'),
}).shape,
},
]
selectAll collection and global tools accept an optional select parameter. It limits which
fields are returned in the response, which can significantly reduce token consumption
when working with large documents.
select is a JSON string that follows Payload's Select API
syntax — set a field to true to include it:
// In a tool call, pass select as a JSON string:
// arguments: { select: '{"title": true, "slug": true}' }
For example, to list only post titles:
curl -i 'http://localhost:3000/api/mcp' \
-X POST \
-H 'Authorization: Bearer MCP-USER-API-KEY' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "findPosts",
"arguments": {
"select": "{\"title\": true, \"slug\": true}"
}
}
}'
Without select, the full document is returned for every result. On collections with
many fields or rich text, this can exhaust a model's context budget quickly.
Use overrideResponse on a collection or global to intercept what is sent back to the
model after any operation. This is the primary place to sanitize sensitive data.
mcpPlugin({
collections: {
posts: {
enabled: true,
overrideResponse: (response, doc, req) => {
req.payload.logger.info('[MCP] Post response intercepted')
// Append additional context
response.content.push({
type: 'text',
text: `Document last modified by: ${doc.updatedBy ?? 'unknown'}`,
})
return response
},
},
users: {
enabled: { find: true },
overrideResponse: (response, doc, req) => {
// Redact sensitive fields before the model sees the document
response.content = response.content.map((item) => ({
...item,
text: item.text
.replace(/"hash":\s*"[^"]*"/g, '"hash": "[redacted]"')
.replace(/"salt":\s*"[^"]*"/g, '"salt": "[redacted]"'),
}))
return response
},
},
},
})
Payload adds an API key collection that allows admins to manage MCP capabilities. Admins can:
Allow or disallow endpoint traffic in real-timeAllow or disallow tools, resources, and promptsYou can customize the API Key collection using the overrideApiKeyCollection option:
mcpPlugin({
overrideApiKeyCollection: (collection) => {
// Add fields to the API Keys collection
collection.fields.push({
name: 'department',
type: 'select',
options: [
{ label: 'Development', value: 'dev' },
{ label: 'Marketing', value: 'marketing' },
],
})
// You can also add hooks
collection.hooks?.beforeRead?.push(({ doc, req }) => {
req.payload.logger.info('Before Read MCP hook!')
return doc
})
return collection
},
// ... other options
})
You can create an MCP access strategy using the overrideAuth option:
import { type MCPAccessSettings, mcpPlugin } from '@payloadcms/plugin-mcp'
// ... other config
mcpPlugin({
overrideAuth: (req, getDefaultMcpAccessSettings) => {
const { payload } = req
// This will return the default MCPAccessSettings
// getDefaultMcpAccessSettings()
payload.logger.info('Custom access Settings for all MCP traffic')
return {
posts: {
find: true,
},
products: {
find: true,
},
} as MCPAccessSettings
},
// ... other options
})
If you want the default MCPAccessSettings, you can use the additional argument getDefaultMcpAccessSettings.
This will use the Bearer token found in the headers on the req to return the MCPAccessSettings related to the user assigned to the API key.
To understand or modify data returned by models at runtime use a collection Hook. Within a hook you can look up the API context. If the context is MCP that collection was triggered by the MCP Plugin. This does not apply to custom tools or resources that have their own context, and can make unrelated database calls.
In this example, Post titles are modified to include '(MCP Hook Override)' when they are read using MCP.
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
},
],
},
}
When your Payload config has localization enabled, all collection and global tools
automatically include locale and fallbackLocale parameters — no extra plugin
configuration is required.
| Parameter | Description |
|---|---|
locale | Retrieve or write data in a specific locale (e.g. "en", "es"). Pass "all" to return all locales at once. |
fallbackLocale | The locale to use when the requested locale has no translation for a field. |
Example — find posts in Spanish with English as fallback:
curl -i 'http://localhost:3000/api/mcp' \
-X POST \
-H 'Authorization: Bearer MCP-USER-API-KEY' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/call",
"params": {
"name": "findPosts",
"arguments": {
"locale": "es",
"fallbackLocale": "en"
}
}
}'
Use the onEvent callback to receive a notification for every MCP request processed
by your server. This is useful for analytics, audit logging, or debugging.
mcpPlugin({
mcp: {
handlerOptions: {
onEvent: (event) => {
// Forward to your analytics pipeline, audit log, etc.
console.log('[MCP event]', event)
},
},
},
})
Virtual fields (computed, read-only fields) are automatically excluded from the create
and update tool schemas. They cannot be set by a model, so including them in the schema
would be misleading and add noise. They are still present in find responses.
There are several levers for reducing the number of tokens consumed per MCP request. Token efficiency matters because large responses can exhaust a model's context budget, slow down responses, and increase cost.
The description you provide for a collection or global is the primary signal a model uses to decide which tool to call. A vague description leads to missed or incorrect tool calls; a precise description gets the right tool called on the first try.
// Weak — a model has no idea what kind of posts these are or when to use this collection
mcpPlugin({
collections: {
posts: {
enabled: true,
description: 'My posts',
},
},
})
// Strong — the model understands the content and its purpose
mcpPlugin({
collections: {
posts: {
enabled: true,
description:
'Published articles covering science and nature topics, with title, body, tags, and publication date.',
},
},
})
select to return only the fields you needBy default, the full document is returned for every operation. On collections with
many fields, rich text, or deeply nested relationships this can be very large. Pass
a select JSON string to limit what comes back:
# Return only title and slug instead of the full document
-d '{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"findPosts","arguments":{"select":"{\"title\": true, \"slug\": true}"}}}'
See Reducing Token Usage with select for more detail.
overrideResponseIf a collection has fields that are irrelevant or sensitive for the model's task, strip them before the response leaves the server. Fewer fields returned means fewer tokens consumed on every call.
See Modifying Responses for examples.
If a model only needs to read data, enable only find. Every additional operation
(create, update, delete) adds more tools to the model's context, which costs
tokens and increases the chance of an unintended action.
mcpPlugin({
collections: {
posts: {
enabled: { find: true }, // create / update / delete are off
},
},
})