Back to Payload

MCP Plugin

docs/plugins/mcp.mdx

3.84.127.5 KB
Original Source

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>

Core features

  • Adds a collection to your config where:
    • You can allow / disallow find, create, update, and delete operations for each collection
    • You can allow / disallow find and update operations for each global
    • You can allow / disallow capabilities in real time
    • You can define your own Prompts, Tools and Resources available over MCP

Installation

Install the plugin using any JavaScript package manager like pnpm, npm, or Yarn:

bash
  pnpm add @payloadcms/plugin-mcp

Basic Usage

In the plugins array of your Payload Config, call the plugin with options:

ts
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

Options

OptionTypeDescription
collectionsobjectAn object of collection slugs to use for MCP capabilities.
collections[slug]objectAn object of collection slugs to use for MCP capabilities.
collections[slug].descriptionstringA description for the collection.
collections[slug].overrideResponsefunctionA function that allows you to override the response from the operation tool call.
collections[slug].enabledobject or booleanDetermines whether the model can find, create, update, and delete documents in the collection.
collections[slug].enabled.findbooleanWhether to allow the model to find documents in the collection.
collections[slug].enabled.createbooleanWhether to allow the model to create documents in the collection.
collections[slug].enabled.updatebooleanWhether to allow the model to update documents in the collection.
collections[slug].enabled.deletebooleanWhether to allow the model to delete documents in the collection.
globalsobjectAn object of global slugs to expose via MCP. Globals only support find and update.
globals[slug].descriptionstringA description for the global shown to models.
globals[slug].overrideResponsefunctionA function that allows you to override the response from the operation tool call.
globals[slug].enabledobject or booleanDetermines whether the model can find or update the global.
globals[slug].enabled.findbooleanWhether to allow the model to read the global.
globals[slug].enabled.updatebooleanWhether to allow the model to update the global.
disabledbooleanDisable the MCP plugin while keeping database schema consistent.
userCollectionCollectionSlugThe users collection that API keys are associated with. Defaults to config.admin.user.
overrideApiKeyCollectionfunctionA function that allows you to override the automatically generated API Keys collection.
overrideAuthfunctionReplace the default Bearer-token / API-key auth with a completely custom access strategy.
mcpobjectMCP options that allow you to customize the MCP server.
mcp.toolsarrayAn array of tools to add to the MCP server.
mcp.tools.namestringThe name of the tool.
mcp.tools.descriptionstringThe description of the tool.
mcp.tools.handlerfunctionThe handler function for the tool.
mcp.tools.parametersobjectThe parameters for the tool (Zod schema).
mcp.promptsarrayAn array of prompts to add to the MCP server.
mcp.prompts.namestringThe name of the prompt.
mcp.prompts.titlestringThe title of the prompt (used by models to determine when to use it).
mcp.prompts.descriptionstringThe description of the prompt.
mcp.prompts.handlerfunctionThe handler function for the prompt.
mcp.prompts.argsSchemaobjectThe arguments schema for the prompt (Zod schema).
mcp.resourcesarrayAn array of resources to add to the MCP server.
mcp.resources.namestringThe name of the resource.
mcp.resources.titlestringThe title of the resource (used by models to determine when to use it).
mcp.resources.descriptionstringThe description of the resource.
mcp.resources.handlerfunctionThe handler function for the resource.
mcp.resources.uristring or objectThe URI of the resource (can be a string or ResourceTemplate for dynamic URIs).
mcp.resources.mimeTypestringThe MIME type of the resource.
mcp.handlerOptionsobjectThe handler options for the MCP server.
mcp.handlerOptions.verboseLogsbooleanWhether to log verbose logs to the console (default: false).
mcp.handlerOptions.maxDurationnumberThe maximum duration for the MCP server requests in seconds (default: 60).
mcp.handlerOptions.onEventfunctionCallback invoked for every MCP event. Useful for analytics and audit logging.
mcp.serverOptionsobjectThe server options for the MCP server.
mcp.serverOptions.serverInfoobjectThe server info for the MCP server.
mcp.serverOptions.serverInfo.namestringThe name of the MCP server (default: 'Payload MCP Server').
mcp.serverOptions.serverInfo.versionstringThe version of the MCP server (default: '1.0.0').

How Access Control Works with MCP

<Banner type="warning"> Enabling a collection or global in the plugin config does **not** automatically make it accessible to MCP clients. There are two separate steps. </Banner>

Step 1 — Enable in your config

Add the collection or global to the plugin with enabled: true (or a capabilities object):

ts
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:

bash
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.

Connecting to MCP Clients

After installing and configuring the plugin, you can connect apps with MCP client capabilities to Payload.

Step 1: Create an API Key

  1. Start your Payload server
  2. Navigate to your admin panel at http://localhost:3000/admin
  3. Go to the MCP → API Keys collection
  4. Click Create New
  5. Allow or Disallow MCP traffic permissions for each collection (enable find, create, update, delete as needed)
  6. Click Create and copy the uniquely generated API key

Step 2: Configure Your MCP Client

MCP 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.

VSCode

json
{
  "mcp.servers": {
    "Payload": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://127.0.0.1:3000/api/mcp",
        "--header",
        "Authorization: Bearer MCP-USER-API-KEY"
      ]
    }
  }
}

Cursor

json
{
  "mcpServers": {
    "Payload": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://localhost:3000/api/mcp",
        "--header",
        "Authorization: Bearer MCP-USER-API-KEY"
      ]
    }
  }
}

Claude Code

bash
claude mcp add --transport http Payload http://127.0.0.1:3000/api/mcp \
  --header "Authorization: Bearer MCP-USER-API-KEY"

Other MCP Clients

For connections without using mcp-remote you can use this configuration format:

json
{
  "mcpServers": {
    "Payload": {
      "type": "http",
      "url": "http://localhost:3000/api/mcp",
      "headers": {
        "Authorization": "Bearer MCP-USER-API-KEY"
      }
    }
  }
}

Testing Your MCP Endpoint

The MCP Inspector is the recommended way to explore and test your MCP server interactively:

bash
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:

bash
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":{}}'

Customizations

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

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.

ts
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

Prompts allow models to generate structured messages for specific tasks. Each prompt defines a schema for arguments and returns formatted messages:

ts
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

Resources provide access to data or content that models can read. They can be static or dynamic with parameterized URIs:

ts
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

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:

PropertyDescription
req.payloadThe initialised Payload instance — use it to call payload.find, payload.create, payload.update, etc.
req.userThe authenticated user making the request (the MCP API key owner).
req.localeThe active locale, if localisation is enabled.
req.headersIncoming 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.

ts
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,
  },
]

Reducing Token Usage with select

All 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:

ts
// In a tool call, pass select as a JSON string:
// arguments: { select: '{"title": true, "slug": true}' }

For example, to list only post titles:

bash
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.

Modifying Responses

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.

ts
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
      },
    },
  },
})
<Banner type="warning"> Models receive the full document by default. Use `overrideResponse` or `select` to remove sensitive fields before they reach the model. </Banner>

API Key Access to MCP

Payload adds an API key collection that allows admins to manage MCP capabilities. Admins can:

  • Create user associated API keys for MCP clients
  • Allow or disallow endpoint traffic in real-time
  • Allow or disallow tools, resources, and prompts

You can customize the API Key collection using the overrideApiKeyCollection option:

ts
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:

ts
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.

Hooks

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.

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
      },
    ],
  },
}

Localization

When your Payload config has localization enabled, all collection and global tools automatically include locale and fallbackLocale parameters — no extra plugin configuration is required.

ParameterDescription
localeRetrieve or write data in a specific locale (e.g. "en", "es"). Pass "all" to return all locales at once.
fallbackLocaleThe locale to use when the requested locale has no translation for a field.

Example — find posts in Spanish with English as fallback:

bash
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"
      }
    }
  }'

Tracking MCP Events

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.

ts
mcpPlugin({
  mcp: {
    handlerOptions: {
      onEvent: (event) => {
        // Forward to your analytics pipeline, audit log, etc.
        console.log('[MCP event]', event)
      },
    },
  },
})

Virtual Fields

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.

Performance

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.

Write strong descriptions

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.

ts
// 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.',
    },
  },
})

Use select to return only the fields you need

By 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:

bash
# 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.

Sanitize responses with overrideResponse

If 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.

Only enable the operations you need

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.

ts
mcpPlugin({
  collections: {
    posts: {
      enabled: { find: true }, // create / update / delete are off
    },
  },
})