obsidian/wiki/payloadcms/plugin-mcp.md
2026-05-15 16:19:06 +01:00

7.2 KiB

title aliases tags sources created updated
MCP Plugin — Model Context Protocol for Payload CMS
payload-mcp
plugin-mcp
payload-mcp-server
payloadcms
mcp
plugin
ai
model-context-protocol
api-keys
raw/plugins__mcp.md
2026-05-15 2026-05-15

Overview

@payloadcms/plugin-mcp exposes your Payload CMS as an MCP server. AI clients (Claude Code, Cursor, VS Code Copilot, etc.) can then perform CRUD operations on your collections and globals using Bearer-token API keys.

Installation

pnpm add @payloadcms/plugin-mcp
import { buildConfig } from 'payload'
import { mcpPlugin } from '@payloadcms/plugin-mcp'

export default buildConfig({
  plugins: [
    mcpPlugin({
      collections: {
        posts: { enabled: true },
      },
    }),
  ],
})

Access Control — Two-Step Pattern

Enabling in config alone is NOT enough. Both steps are required.

Step 1 — Enable in plugin config:

mcpPlugin({
  collections: { posts: { enabled: true } },
  globals: { 'site-settings': { enabled: { find: true, update: true } } },
})

Step 2 — Allow in the API Key:
Admin panel → MCP → API Keys → create key → toggle individual capabilities.

All MCP requests require Authorization: Bearer <key>. Requests without a valid key are rejected immediately.

Key Options Reference

Option Type Description
collections[slug].enabled boolean | { find, create, update, delete } Granular CRUD permissions
globals[slug].enabled boolean | { find, update } Globals are singletons — no create/delete
collections[slug].overrideResponse function Intercept/sanitize response before the model sees it
userCollection CollectionSlug Which collection holds users for API key linking
overrideApiKeyCollection function Extend the auto-generated API Keys collection
overrideAuth function Replace Bearer-token auth with custom strategy
disabled boolean Disable plugin while keeping DB schema consistent
mcp.tools array Custom tools (Zod schema + handler)
mcp.prompts array Custom prompts with argsSchema
mcp.resources array Static or dynamic resources (URI templates)
mcp.handlerOptions.onEvent function Audit/analytics callback for every MCP event
mcp.handlerOptions.maxDuration number Request timeout in seconds (default: 60)

Connecting MCP Clients

Claude Code

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

Cursor / VS Code (via mcp-remote)

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

Direct HTTP

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

Custom Tools / Prompts / Resources

Tool — custom business logic

mcp: {
  tools: [{
    name: 'getPostScores',
    description: 'Get scores about posts since a given date',
    handler: async (args, req) => {
      const stats = await req.payload.find({
        collection: 'posts',
        where: { createdAt: { greater_than: args.since } },
        req,
        overrideAccess: false,  // respect API key owner's access
        user: req.user,
      })
      return { content: [{ type: 'text', text: `${stats.totalDocs} posts` }] }
    },
    parameters: z.object({ since: z.string() }).shape,
  }]
}

Prompt

prompts: [{
  name: 'reviewContent',
  argsSchema: { content: z.string(), criteria: z.array(z.string()) },
  handler: ({ content, criteria }, req) => ({
    messages: [{ role: 'user', content: { type: 'text', text: `...` } }]
  }),
}]

Resource (static + dynamic)

resources: [
  { name: 'guidelines', uri: 'guidelines://company', mimeType: 'text/markdown', handler: (uri, req) => ({ contents: [...] }) },
  { name: 'userProfile', uri: new ResourceTemplate('users://profile/{userId}', { list: undefined }), ... }
]

Globals

Globals get two MCP tools per global: findSiteSettings and updateSiteSettings.
No create or delete — singletons managed by Payload.

Hooks Integration

Detect MCP context in collection hooks via req.payloadAPI === 'MCP':

hooks: {
  beforeRead: [
    ({ doc, req }) => {
      if (req.payloadAPI === 'MCP') {
        doc.title = `${doc.title} (MCP Override)`
      }
      return doc
    },
  ],
}

Token Efficiency Tips

  • select parameter — pass { select: '{"title": true, "slug": true}' } to limit fields returned; without it the full document is returned every time
  • overrideResponse — strip sensitive/irrelevant fields server-side before the model sees them
  • Enable only needed ops — each extra operation (create/update/delete) adds tools to the model's context budget
  • Strong descriptions — precise collection descriptions help the model pick the right tool on the first try
// Redact sensitive fields
overrideResponse: (response, doc, req) => {
  response.content = response.content.map((item) => ({
    ...item,
    text: item.text.replace(/"hash":\s*"[^"]*"/g, '"hash": "[redacted]"'),
  }))
  return response
}

Localization

When localization is enabled, all collection/global tools automatically expose locale and fallbackLocale parameters — no extra config needed.

Testing

# MCP Inspector (interactive)
npx @modelcontextprotocol/inspector
# → set URL: http://127.0.0.1:3000/api/mcp
# → add header: Authorization: Bearer MCP-USER-API-KEY

# curl test
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":{}}'

Key Takeaways

  • Two-step access control: plugin config AND API Key admin toggle — both required
  • All requests need Authorization: Bearer <key>; existing Payload access rules still apply
  • Use select to avoid sending full documents to models — major token savings on rich-text collections
  • Use overrideResponse to sanitize sensitive fields (passwords, hashes, PII) before LLM sees them
  • req.payloadAPI === 'MCP' flag lets hooks behave differently for MCP vs REST/GraphQL traffic
  • Only enable operations the model actually needs — unnecessary tools waste context budget
  • Virtual fields are excluded from create/update schemas but appear in find responses
  • Custom Tools receive req.payload for full Payload Local API access; always pass overrideAccess: false