225 lines
7.2 KiB
Markdown
225 lines
7.2 KiB
Markdown
---
|
|
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 <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
|
|
```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 <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`
|
|
|
|
## 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]]
|