--- title: "MCP Plugin — Model Context Protocol for Payload CMS" aliases: [payload-mcp, plugin-mcp, payload-mcp-server] tags: [payloadcms, mcp, plugin, ai, model-context-protocol, api-keys] sources: [raw/plugins__mcp.md] created: 2026-05-15 updated: 2026-05-15 --- ## Overview `@payloadcms/plugin-mcp` exposes your Payload CMS as an [MCP](https://modelcontextprotocol.io) 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 ```bash pnpm add @payloadcms/plugin-mcp ``` ```ts 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:** ```ts 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 `. 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 ```bash 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) ```json { "mcpServers": { "Payload": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:3000/api/mcp", "--header", "Authorization: Bearer MCP-USER-API-KEY"] } } } ``` ### Direct HTTP ```json { "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 ```ts 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 ```ts 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) ```ts 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'`: ```ts 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 ```ts // 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 ```bash # 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 `; 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` ## Related - [[wiki/payloadcms/authentication-api-keys|Authentication — API Keys]] - [[wiki/payloadcms/plugins|Official Plugins]] - [[wiki/payloadcms/plugins-api|Plugin API]] - [[wiki/payloadcms/hooks-collections|Collection Hooks]] - [[wiki/payloadcms/local-api|Local API]] - [[wiki/payloadcms/queries|Queries]] - [[wiki/agent-sdk/_index|Claude Agent SDK]]