Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

MCP Plugin

https://www.npmjs.com/package/@payloadcms/plugin-mcp

This plugin adds Model Context Protocol capabilities to Payload.

Core features

  • Streamable HTTP transport at /api/mcp for local and remote clients, using Payload auth when provided
  • Stable discovery and CRUD tools for configured collections and globals (getConfigInfo, getCollectionSchema, findDocuments, createDocuments, updateDocument, deleteDocuments, getUploadInstructions, getGlobalSchema, findGlobal, updateGlobal) - filtered by Payload access and configurable with simple keys like tools: { delete: false }
  • Auth-enabled collections expose opt-in login, auth, forgotPassword, resetPassword, unlock, verify tools
  • Define your own tools, prompts, and resources with full TypeScript inference via defineTool / defineCollectionTool / defineGlobalTool / definePrompt
  • Gate tools, prompts, and resources with code-level access callbacks while Payload access control still protects the underlying documents
  • Observe or transform every completed tool response with hooks.afterToolCall
  • Custom auth strategies via overrideGetAuthorizedMCP

Installation

1
pnpm add @payloadcms/plugin-mcp

Quick Start

1. Add mcpPlugin to your Payload config:

1
import { buildConfig } from 'payload'
2
import { mcpPlugin } from '@payloadcms/plugin-mcp'
3
4
export default buildConfig({
5
// your collections, globals, etc.
6
plugins: [mcpPlugin({})],
7
})

2. Start Payload and point your MCP client at the HTTP endpoint:

1
{
2
"mcpServers": {
3
"Payload": {
4
"type": "http",
5
"url": "http://127.0.0.1:3000/api/mcp?overrideAccess=true"
6
}
7
}
8
}

This development URL skips access control. Outside development, remove overrideAccess=true and send Payload authorization. If your client does not support HTTP natively, see Connecting MCP clients.

That's it - every collection and global is now available through the generic MCP tools. For example, call getConfigInfo to see available slugs, inspect a collection with getCollectionSchema, then create one or more documents with createDocuments.

Uploading files

Upload collections include a getUploadInstructions tool. It keeps the file itself out of MCP tool arguments:

1
MCP client
2
-> calls getUploadInstructions with the collection and file metadata
3
4
Payload
5
-> returns an HTTP request or named provider instruction, plus a file value
6
7
MCP client
8
-> sends the original file using the HTTP request
9
or calls the MCP tool named by the provider instruction
10
-> calls createDocuments or updateDocument with { source: "uploadReference", file: <returned file> }
11
12
Payload
13
-> runs normal file validation, image processing, hooks, and document creation

When supported by the storage adapter, the HTTP request uploads directly to the storage provider and bypasses Payload. Otherwise, it uploads to temporary storage through Payload. Named provider instructions tell the client to call an MCP tool with the returned name; if that tool is not installed, the upload is unsupported.

The program running the MCP client must be able to read the local file and send its bytes. A remote Payload server cannot read a path on the caller's computer, and the file should not be converted to base64 for createDocuments or updateDocument.

Configuring per-collection tools

To configure a collection - disable specific built-in tools, tighten a description, add a custom tool - use the collections map:

1
mcpPlugin({
2
collections: {
3
posts: {
4
description:
5
'Published blog articles. Use `findDocuments` to list or fetch one by ID.',
6
tools: {
7
// Built-in opt-out: keep find/create/update, block delete
8
delete: false,
9
10
// Built-in override: keep findDocuments enabled, just refine its description
11
find: {
12
description:
13
'Find blog posts. Pass an `id` to fetch one; omit it to list with pagination.',
14
},
15
},
16
},
17
media: {
18
// Disable document creation and deletion; updates remain available
19
tools: {
20
create: false,
21
delete: false,
22
},
23
},
24
},
25
})

Auth-collection tools

Auth-enabled collections additionally support auth, login, forgotPassword, resetPassword, unlock, and verify tools. These are opt-in - set them to true (or to an override object) to expose them:

1
mcpPlugin({
2
collections: {
3
users: {
4
description: 'User accounts.',
5
tools: {
6
auth: true,
7
login: true,
8
forgotPassword: true,
9
resetPassword: true,
10
unlock: true,
11
verify: true,
12
},
13
},
14
},
15
})

Like every MCP item, auth tools use default MCP access unless you provide an access callback. If you want an opt-in auth tool to be callable without an authenticated user, provide an override object with access: () => true.

Options

Option

Type

Description

collections

object

Map keyed by collection slug. See Per-collection options.

globals

object

Map keyed by global slug. See Per-global options.

tools

object

Cross-cutting custom tools, keyed by tool name. Values are Tool objects (use defineTool).

prompts

object

Custom prompts, keyed by name. Values are Prompt objects (use definePrompt).

resources

object

Custom resources, keyed by name. Values are Resource objects.

disabled

boolean

Skip MCP endpoint registration.

hooks.afterToolCall

function[]

Run cross-cutting hooks after tool handlers return. See MCP plugin hooks.

overrideGetAuthorizedMCP

function

Replace the default MCP authorization resolver. See Custom auth.

mcp.maxRequestBodySize

number

Largest HTTP request body in bytes. Larger requests get a 413 response. Base64 file uploads count toward it. Default 4194304 (4 MiB).

mcp.serverOptions.serverInfo.name

string

The MCP server name advertised to clients. Default 'Payload MCP Server'.

mcp.serverOptions.serverInfo.version

string

The MCP server version. Default '1.0.0'.

mcp.serverOptions.options

object

Raw options passed through to the underlying McpServer from @modelcontextprotocol/server.

mcp.verboseLogs

boolean

When true, info/debug logs from the plugin surface in Payload's logger. Warnings and errors are always logged. Default false.

Per-collection options

collections[slug] accepts:

Option

Type

Description

description

string

Description shown for this collection in discovery responses.

tools

object

Map of tool name → configuration. See Configuring tools and Defining custom tools.

overrideResponse

function

Intercept the response of any built-in tool for this collection ((response, doc, req) => response). See Modifying responses.

The tools map keys can be:

  • A built-in key (getCollectionSchema, getUploadInstructions on upload collections, find, create, update, delete, plus auth tool names on auth collections): set to false to disable, or to an override object { access?, description?, overrideResponse? }. Auth tools also accept true to enable with no overrides.
  • A custom name: a value produced by defineCollectionTool(...).

The create key controls createDocuments, which handles both single and multi-document requests.

getConfigInfo is always registered as a top-level tool. It returns the collection and global slugs visible to the current MCP client.

Per-global options

globals[slug] accepts the same shape, but the built-in keys are limited to getGlobalSchema, find, and update (globals are singletons; create / delete don't apply).

Creating documents

createDocuments accepts a documents array with at least one item. Each item has its own data and optional file. Options such as depth, draft, locale, and fallbackLocale apply to the whole request.

Documents are created one at a time through Payload's Local API. The operation is best-effort: a failed item does not roll back successful items. Results and errors include their original zero-based input index so failed items can be retried without creating successful items again. The tool has no fixed item limit, but normal request-size and execution-time limits still apply.

createDocuments and updateDocument return affected document IDs by default. Pass returning: true when the agent needs the complete documents. You can also pass select with returning: true to limit which fields come back.

Uploading files

MCP can add or replace files in upload-enabled collections.

  • For a file that is already online, include its URL in your prompt: Add https://example.com/logo.png to the media library with the alt text "Company logo".
  • If your MCP client supports attachments, attach a small local file and ask the agent to upload it.

The agent reads the collection's upload settings before acting. MIME type and file-size restrictions still apply, URL uploads respect pasteURL and its allow list, and local files are transferred as base64.

LLM instructions

The getCollectionSchema and getGlobalSchema tools return LLM instructions in structuredContent.instructions alongside schema, and in a text content block. Read them before creating or updating content.

These instructions come from your collection or global config and can include additional instructions saved in the Admin Panel. To read them, the caller needs permission to create or update documents in that collection, or to update that global. Saved instructions also require the caller to be logged in and have read permission for the collection or global. These permission checks are skipped when overrideAccess is enabled.

HTTP transport

HTTP is the recommended transport for both local development and deployed apps. During development, config changes follow Payload's normal dev-server reload lifecycle.

Once mcpPlugin is in your plugins array, POST /api/mcp accepts JSON-RPC 2.0 MCP requests. Send Payload authorization when you want MCP to run as a user (see Authorization); without it, MCP runs access control with no user.

Quick test:

1
curl -i 'http://localhost:3000/api/mcp' \
2
-X POST \
3
-H 'Authorization: users API-Key MCP-USER-API-KEY' \
4
-H 'Content-Type: application/json' \
5
-H 'Accept: application/json, text/event-stream' \
6
-d '{"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}'

Authorization

The HTTP endpoint uses Payload authorization. A user API key uses this header:

1
txtAuthorization: <authCollectionSlug> API-Key <key>

Send this value in the Authorization header.

When authorization is sent, MCP authenticates with Payload's normal auth system. The authenticated user is used for Payload access control, and MCP only advertises tools, prompts, and resources allowed for that request. If no authorization is sent, MCP runs access control with no user.

Creating an API key

  1. Start your Payload server and open the admin panel
  2. Open a user document in an auth-enabled collection with auth.useAPIKey
  3. Enable API key auth for that user, generate a key, then save and copy it
  4. Use that key as Authorization: users API-Key <key>. Replace users with that auth collection's slug.

overrideAccess

By default, MCP runs access control. Set overrideAccess explicitly when you want MCP to behave like a Payload local API call with overrideAccess: true. When true, MCP skips item access callbacks, built-in permission checks, and Payload access control inside built-in handlers.

For HTTP in development, add overrideAccess=true or overrideAccess=false to the MCP URL:

1
txtPOST /api/mcp?overrideAccess=true

HTTP rejects the overrideAccess URL argument outside development.

Custom auth

Use overrideGetAuthorizedMCP to replace the default Payload authentication and MCP authorization flow. The custom resolver must:

  1. Authenticate the caller and assign the matching Payload user to req.user. Assign null when the caller is anonymous.
  2. Return only the MCP items that caller may use.
  3. Return the overrideAccess value that handlers should pass to Payload's local API.

req.user is the single source of truth for the caller's identity. Payload access control, MCP access callbacks, and local API calls that receive req all use it.

1
import { mcpPlugin } from '@payloadcms/plugin-mcp'
2
3
mcpPlugin({
4
overrideGetAuthorizedMCP: async ({ overrideAccess, pluginConfig, req }) => {
5
// Your application verifies the external credentials and returns the
6
// matching Payload user, or null when the caller is anonymous.
7
const user = await authenticateAndResolvePayloadUser(req)
8
req.user = user ?? null
9
10
return {
11
items: req.user ? pluginConfig.items : [],
12
overrideAccess,
13
}
14
},
15
})

This example allows every configured MCP item for any resolved user. Filter pluginConfig.items before returning when different users should see different items.

The handler receives:

  • req - the incoming PayloadRequest
  • pluginConfig - the fully sanitized plugin config, including the registered items
  • overrideAccess - the requested Payload-style access override

Connecting MCP clients

Below are configuration examples for popular MCP clients. Replace users API-Key MCP-USER-API-KEY with the authorization value from Authorization. The format of these JSON files may change over time - check the client website for current schemas. The examples include authorization because default MCP access requires a user.

HTTP

Most clients support the streamable-HTTP transport directly. If yours doesn't, the mcp-remote package via npx is a reliable adapter.

VSCode

1
{
2
"mcp.servers": {
3
"Payload": {
4
"command": "npx",
5
"args": [
6
"-y",
7
"mcp-remote",
8
"http://127.0.0.1:3000/api/mcp",
9
"--header",
10
"Authorization: users API-Key MCP-USER-API-KEY"
11
]
12
}
13
}
14
}

Cursor

1
{
2
"mcpServers": {
3
"Payload": {
4
"command": "npx",
5
"args": [
6
"-y",
7
"mcp-remote",
8
"http://localhost:3000/api/mcp",
9
"--header",
10
"Authorization: users API-Key MCP-USER-API-KEY"
11
]
12
}
13
}
14
}

Claude Code

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

Native HTTP (no mcp-remote)

1
{
2
"mcpServers": {
3
"Payload": {
4
"type": "http",
5
"url": "http://localhost:3000/api/mcp",
6
"headers": {
7
"Authorization": "users API-Key MCP-USER-API-KEY"
8
}
9
}
10
}
11
}

Testing your MCP endpoint

The MCP Inspector is the fastest way to explore and call your server interactively:

1
npx @modelcontextprotocol/inspector

Set the URL to http://127.0.0.1:3000/api/mcp and add Authorization: users API-Key MCP-USER-API-KEY as a header.

Defining custom tools, prompts, and resources

The plugin ships four helpers - defineTool, defineCollectionTool, defineGlobalTool, definePrompt - that give you full inference of the input schema in the handler's argument type. Each helper is a two-stage builder: pass the schema and metadata first, then chain .handler(fn).

Tool inputs and prompt arguments accept Standard Schema libraries such as Zod, or raw JSON Schema 2020-12.

Top-level tools

A "top-level" tool isn't scoped to a collection or global. Its wire name is exactly its key in the tools map.

1
import { defineTool, mcpPlugin } from '@payloadcms/plugin-mcp'
2
import * as z from 'zod'
3
4
mcpPlugin({
5
tools: {
6
diceRoll: defineTool({
7
description: 'Roll a virtual dice with the specified number of sides.',
8
input: z.object({
9
sides: z.number().int().min(2).max(1000).default(6),
10
}),
11
}).handler(async ({ authorizedMCP, input, req }) => {
12
const result = Math.floor(Math.random() * input.sides) + 1
13
14
await req.payload.create({
15
collection: 'rolls',
16
data: { sides: input.sides, result, user: req.user?.id },
17
overrideAccess: authorizedMCP.overrideAccess,
18
req,
19
})
20
21
return {
22
content: [{ type: 'text', text: `You rolled a ${result}.` }],
23
}
24
}),
25
},
26
})

Collection-scoped tools

A collection tool's wire name is exactly its key. Payload adds a slug string to the MCP input schema automatically; the handler receives the resolved slug. The field is intentionally not enum-restricted so newly-added collections can be used after HMR even if the MCP client cached the previous tool schema. Built-in collection tools still validate the requested slug against Payload access control on the server.

1
import { defineCollectionTool, mcpPlugin } from '@payloadcms/plugin-mcp'
2
import * as z from 'zod'
3
4
mcpPlugin({
5
collections: {
6
posts: {
7
tools: {
8
publish: defineCollectionTool({
9
description: 'Publish a draft post by ID.',
10
input: z.object({
11
id: z.string(),
12
}),
13
}).handler(async ({ slug, input, authorizedMCP, req }) => {
14
const result = await req.payload.update({
15
id: input.id,
16
collection: slug,
17
data: { _status: 'published' },
18
overrideAccess: authorizedMCP.overrideAccess,
19
req,
20
})
21
return {
22
content: [
23
{
24
type: 'text',
25
text: `Published ${input.id}: ${JSON.stringify(result)}`,
26
},
27
],
28
}
29
}),
30
},
31
},
32
},
33
})

Global-scoped tools

defineGlobalTool mirrors defineCollectionTool. Payload adds a slug string to the MCP input schema automatically; the handler receives the resolved slug. The field is intentionally not enum-restricted so newly-added globals can be used after HMR even if the MCP client cached the previous tool schema. Built-in global tools still validate the requested slug against Payload access control on the server.

1
import { defineGlobalTool, mcpPlugin } from '@payloadcms/plugin-mcp'
2
import * as z from 'zod'
3
4
mcpPlugin({
5
globals: {
6
'site-settings': {
7
tools: {
8
setMaintenanceMode: defineGlobalTool({
9
description: 'Enable or disable maintenance mode.',
10
input: z.object({
11
enabled: z.boolean(),
12
}),
13
}).handler(async ({ slug, input, authorizedMCP, req }) => {
14
const result = await req.payload.updateGlobal({
15
slug,
16
data: { maintenanceMode: input.enabled },
17
overrideAccess: authorizedMCP.overrideAccess,
18
req,
19
})
20
21
return {
22
content: [{ type: 'text', text: JSON.stringify(result) }],
23
}
24
}),
25
},
26
},
27
},
28
})

Access callbacks

Tools, prompts, and resources can include an access callback. Return false to hide that MCP primitive from the current request. Access callbacks receive req and, when Payload permissions are being enforced, permissions. Built-in collection and global tools accept the same callback in their override object. If no access callback is defined, MCP uses Payload's default behavior and requires req.user.

1
mcpPlugin({
2
collections: {
3
posts: {
4
tools: {
5
delete: {
6
access: ({ req }) => Boolean(req.user),
7
},
8
},
9
},
10
},
11
tools: {
12
adminReport: defineTool({
13
description: 'Generate an admin report.',
14
access: ({ req }) => Boolean(req.user),
15
}).handler(async () => ({
16
content: [{ type: 'text', text: 'Report generated.' }],
17
})),
18
},
19
})

Built-in collection and global tools include Payload permission checks in their default access callback. If you provide an access callback for a built-in tool, it replaces that default.

Prompts

1
import { definePrompt, mcpPlugin } from '@payloadcms/plugin-mcp'
2
import * as z from 'zod'
3
4
mcpPlugin({
5
prompts: {
6
reviewContent: definePrompt({
7
title: 'Content Review Prompt',
8
description: 'Creates a prompt for reviewing content quality.',
9
argsSchema: z.object({
10
content: z.string(),
11
criteria: z.array(z.string()),
12
}),
13
}).handler(({ input }) => ({
14
messages: [
15
{
16
role: 'user',
17
content: {
18
type: 'text',
19
text: `Review based on: ${input.criteria.join(', ')}\n\n${input.content}`,
20
},
21
},
22
],
23
})),
24
},
25
})

Resources

Resources can be static (string URI) or dynamic (ResourceTemplate for parameterized URIs):

1
import { ResourceTemplate } from '@modelcontextprotocol/server'
2
import { mcpPlugin } from '@payloadcms/plugin-mcp'
3
4
mcpPlugin({
5
resources: {
6
guidelines: {
7
title: 'Content Guidelines',
8
description: 'Company content creation guidelines.',
9
uri: 'guidelines://company',
10
mimeType: 'text/markdown',
11
handler: ({ uri }) => ({
12
contents: [
13
{
14
uri: uri.href,
15
text: '# Content Guidelines\n\n1. Keep it concise\n2. Use clear language',
16
},
17
],
18
}),
19
},
20
21
userProfile: {
22
title: 'User Profile',
23
description: 'Access user profile information.',
24
uri: new ResourceTemplate('users://profile/{userId}', {
25
list: undefined,
26
}),
27
mimeType: 'application/json',
28
handler: async ({ params, req, uri }) => {
29
const user = await req.payload.findByID({
30
id: params.userId,
31
collection: 'users',
32
overrideAccess: false,
33
req,
34
})
35
return {
36
contents: [{ uri: uri.href, text: JSON.stringify(user) }],
37
}
38
},
39
},
40
},
41
})

Request identity and AuthorizedMCP

The authenticated user is available as req.user. Tool handlers also receive an authorizedMCP object describing the MCP authorization result:

1
type AuthorizedMCP = {
2
items: MCPItem[] // the items this caller may use
3
overrideAccess: boolean // whether handlers should bypass Payload access control
4
}

When calling Payload's local API from a tool handler, pass req and authorizedMCP.overrideAccess. The local API reads the user from req.user:

1
await req.payload.create({
2
collection: 'posts',
3
data,
4
overrideAccess: authorizedMCP.overrideAccess,
5
req,
6
})

Built-in CRUD tools already use this pattern. They run as req.user unless overrideAccess is true.

Prompt and resource handlers receive req, but not authorizedMCP.

Controlling returned fields

createDocuments and updateDocument return IDs by default to keep mutation responses small. Set returning: true to return complete documents. When you only need some fields, pass select together with returning: true.

The built-in findDocuments, findGlobal, and updateGlobal tools continue to accept select directly.

select follows Payload's Select API syntax - set a field to true to include it:

1
curl -i 'http://localhost:3000/api/mcp' \
2
-X POST \
3
-H 'Authorization: users API-Key MCP-USER-API-KEY' \
4
-H 'Content-Type: application/json' \
5
-H 'Accept: application/json, text/event-stream' \
6
-d '{
7
"jsonrpc": "2.0",
8
"id": "1",
9
"method": "tools/call",
10
"params": {
11
"name": "findDocuments",
12
"arguments": {
13
"slug": "posts",
14
"select": {
15
"title": true,
16
"slug": true
17
}
18
}
19
}
20
}'

Without select, the full document is returned. On collections with rich text or deeply nested relationships this can exhaust a model's context budget quickly.

Modifying responses

Use overrideResponse to intercept what is sent back to the model after any built-in operation on a collection or global. This is the primary place to sanitize sensitive data:

1
mcpPlugin({
2
collections: {
3
posts: {
4
overrideResponse: (response, doc, req) => {
5
req.payload.logger.info('[MCP] Post response intercepted')
6
response.content.push({
7
type: 'text',
8
text: `Document last modified by: ${doc.updatedBy ?? 'unknown'}`,
9
})
10
return response
11
},
12
},
13
users: {
14
overrideResponse: (response) => {
15
response.content = response.content.map((item) => ({
16
...item,
17
text: (item as { text: string }).text
18
.replace(/"hash":\s*"[^"]*"/g, '"hash": "[redacted]"')
19
.replace(/"salt":\s*"[^"]*"/g, '"salt": "[redacted]"'),
20
}))
21
return response
22
},
23
},
24
},
25
})

For finer-grained control, you can set overrideResponse on an individual built-in tool override or on a custom tool. Resolution order: per-tool > collection/global > the built-in's default.

MCP plugin hooks

Use hooks.afterToolCall for behavior that should run after every collection, global, or top-level tool handler returns. Hooks run in array order, and each hook must return the response that the next hook receives:

1
mcpPlugin({
2
hooks: {
3
afterToolCall: [
4
({ input, req, response, toolName }) => {
5
req.payload.logger.info(
6
`[MCP] ${toolName} ${response.isError ? 'failed' : 'completed'}`,
7
)
8
9
return response
10
},
11
],
12
},
13
})

Each hook receives:

Argument

Description

input

The parsed input passed to the tool.

req

The current Payload request.

response

The response returned by the previous response override or afterToolCall hook.

toolName

The registered MCP tool name, such as createDocuments or getConfigInfo.

Payload collection hooks

To modify documents at runtime use a collection Hook. Inside the hook you can detect MCP traffic via req.payloadAPI === 'MCP':

1
import type { CollectionConfig } from 'payload'
2
3
export const Posts: CollectionConfig = {
4
slug: 'posts',
5
fields: [
6
{
7
name: 'title',
8
type: 'text',
9
required: true,
10
},
11
],
12
hooks: {
13
beforeRead: [
14
({ doc, req }) => {
15
if (req.payloadAPI === 'MCP') {
16
doc.title = `${doc.title} (MCP Hook Override)`
17
}
18
return doc
19
},
20
],
21
},
22
}

payloadAPI === 'MCP' is set on the request that the built-in collection / global tools issue. Custom tools that make their own local-API calls control their own req.payloadAPI value.

Localization

When your Payload config has localization enabled, built-in collection/global read and write tools 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:

1
curl -i 'http://localhost:3000/api/mcp' \
2
-X POST \
3
-H 'Authorization: users API-Key MCP-USER-API-KEY' \
4
-H 'Content-Type: application/json' \
5
-H 'Accept: application/json, text/event-stream' \
6
-d '{
7
"jsonrpc": "2.0",
8
"id": "1",
9
"method": "tools/call",
10
"params": {
11
"name": "findDocuments",
12
"arguments": {
13
"slug": "posts",
14
"locale": "es",
15
"fallbackLocale": "en"
16
}
17
}
18
}'

Virtual fields

Virtual fields (computed, read-only fields) are automatically excluded from getCollectionSchema and getGlobalSchema. 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 helps the model choose the right slug after inspecting the available schemas. A vague description leads to missed or incorrect tool calls; a precise description gets the right document target on the first try.

1
// Weak - the model has no idea what kind of posts these are
2
mcpPlugin({
3
collections: {
4
posts: { description: 'My posts' },
5
},
6
})
7
8
// Strong - the model understands content and purpose
9
mcpPlugin({
10
collections: {
11
posts: {
12
description:
13
'Published articles covering science and nature topics, with title, body, tags, and publication date.',
14
},
15
},
16
})

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 select to limit what comes back - see Reducing token usage with select.

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.

Only enable the operations you need

If a model only needs to read data, disable create, update, and delete.

1
mcpPlugin({
2
collections: {
3
posts: {
4
tools: {
5
create: false,
6
update: false,
7
delete: false,
8
},
9
},
10
},
11
})

stdio transport (with limitations)

For clients that can only spawn local processes, the package also ships a payload-mcp bin. It loads your Payload config, boots a separate Payload instance, and serves MCP over stdin/stdout.

1
{
2
"mcpServers": {
3
"Payload": {
4
"command": "npx",
5
"args": ["payload-mcp"],
6
"env": {
7
"PAYLOAD_MCP_AUTHORIZATION": "users API-Key YOUR_API_KEY"
8
}
9
}
10
}
11
}

PAYLOAD_MCP_AUTHORIZATION is the same value used for the HTTP Authorization header. If you omit it, MCP runs access control without a user. During development, you can instead set NODE_ENV=development and PAYLOAD_MCP_OVERRIDE_ACCESS=true to skip access control.

You do not need to add mcpPlugin to your Payload config to use the bin. Add it when you want to configure tools, descriptions, prompts, or resources. The bin uses tsx to load TypeScript configs; pass --disable-transpile when loading an already compiled JavaScript config.

Locating the config

The bin derives the project root from the installed package and resolves payload.config.ts from there. In hoisted monorepos or other unusual layouts, set PAYLOAD_CONFIG_PATH explicitly:

1
{
2
"mcpServers": {
3
"Payload": {
4
"command": "npx",
5
"args": ["payload-mcp"],
6
"env": {
7
"PAYLOAD_CONFIG_PATH": "/absolute/path/to/your/payload.config.ts"
8
}
9
}
10
}
11
}

Drawbacks

  • Payload config, MCP registrations, request context, and authorization are initialized once. Changes require restarting the server and reconnecting the client; some clients may continue showing old discovery results until the client itself is restarted.
  • Credentials and permissions are evaluated only at startup. User, role, or access changes do not take effect until restart, while HTTP reevaluates them for every request.
  • Each client starts a separate Payload process and database connection instead of reusing the running app.
  • stdout is reserved for MCP JSON-RPC messages. Config, plugin, or dependency logs written to stdout can break the connection.

Was this page helpful?

Next

Multi-Tenant Plugin