Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions docs/plugins/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ This plugin adds [Model Context Protocol](https://modelcontextprotocol.io/docs/g
- You can allow / disallow `find`, `create`, `update`, and `delete` operations for each collection
- You can to allow / disallow capabilities in real time
- You can define your own Prompts, Tools and Resources available over MCP
- Full support for Payload's localization features with `locale` and `fallbackLocale` parameters
- MCP server supporting HTTP (with REDIS and STDIO under consideration)

## Installation

Expand Down Expand Up @@ -69,6 +71,75 @@ const config = buildConfig({
export default config
```

## Connecting AI Tools

After installing and configuring the plugin, you can connect AI tools that support the Model Context Protocol (MCP) to your Payload server.

### 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 **MCP → API Keys**
4. Click **Create New**
5. Configure permissions for each collection (enable find, create, update, delete as needed)
6. Click **Create** and copy the generated API key

### Step 2: Configure Your MCP Client

Add your Payload MCP server to your MCP client's configuration file (typically `.mcp.json` or similar):

```json
{
"mcpServers": {
"payload-cms": {
"type": "http",
"url": "http://localhost:3000/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY_HERE"
}
}
}
}
```

Replace `YOUR_API_KEY_HERE` with the API key you created in Step 1.

**Configuration Notes:**

- **URL:** If you're using a custom `basePath` in your MCP plugin configuration, update the URL accordingly (e.g., `http://localhost:3000/custom-path/mcp`)
- **Production:** For production deployments, use your domain instead of `localhost` (e.g., `https://yourdomain.com/api/mcp`)
- **Headers:** The `Authorization` header with Bearer token is required for authentication

### Step 3: Restart Your MCP Client

Restart your MCP client for the configuration to take effect. The Payload MCP server should now be available, and the AI tool will be able to interact with your configured collections.

Refer to your specific MCP client's documentation for additional configuration options and setup instructions.

## Connection Methods

The MCP plugin supports different connection methods for communicating with AI tools:

### HTTP (Currently Supported)

The HTTP transport is the primary and currently supported connection method. As shown in the configuration example above, clients connect via HTTP requests to your Payload server's MCP endpoint (default: `/api/mcp`).

**Configuration:**

```json
{
"mcpServers": {
"payload-cms": {
"type": "http",
"url": "http://localhost:3000/api/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY_HERE"
}
}
}
}
```

### Options

| Option | Type | Description |
Expand Down Expand Up @@ -142,6 +213,172 @@ mcpPlugin({
})
```

## Localization Support

The MCP plugin fully supports Payload's localization features, working the same way as Payload's REST API. All resource operations (create, update, find, delete) accept `locale` and `fallbackLocale` parameters, allowing you to manage multilingual content through MCP.

### Prerequisites

First, configure localization in your Payload config:

```ts
const config = buildConfig({
localization: {
defaultLocale: 'en',
fallback: true,
locales: [
{ code: 'en', label: 'English' },
{ code: 'es', label: 'Spanish' },
{ code: 'fr', label: 'French' },
],
},
collections: [
{
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
localized: true, // Enable localization for this field
},
{
name: 'content',
type: 'richText',
localized: true,
},
],
},
],
plugins: [
mcpPlugin({
collections: {
posts: {
enabled: true,
},
},
}),
],
})
```

### Creating Localized Content

Create content in a specific locale using the `locale` parameter:

```ts
// Via MCP tool call
{
"name": "createPosts",
"arguments": {
"title": "Hello World",
"content": "This is my first post in English",
"locale": "en"
}
}
```

### Adding Translations

Add translations to existing content by updating with a different locale:

```ts
// First, create in English
{
"name": "createPosts",
"arguments": {
"title": "Hello World",
"content": "English content"
}
}

// Then, add Spanish translation
{
"name": "updatePosts",
"arguments": {
"id": "document-id",
"title": "Hola Mundo",
"content": "Contenido en español",
"locale": "es"
}
}
```

### Retrieving Localized Content

Retrieve content in a specific locale:

```ts
// Get Spanish version
{
"name": "findPosts",
"arguments": {
"id": "document-id",
"locale": "es"
}
}
```

Retrieve all translations at once using `locale: 'all'`:

```ts
{
"name": "findPosts",
"arguments": {
"id": "document-id",
"locale": "all"
}
}

// Response will include all translations:
// {
// "title": {
// "en": "Hello World",
// "es": "Hola Mundo",
// "fr": "Bonjour le Monde"
// },
// "content": { ... }
// }
```

### Fallback Locales

When requesting content in a locale that doesn't have a translation, Payload will automatically fall back to the default locale:

```ts
// Request French content when only English exists
{
"name": "findPosts",
"arguments": {
"id": "document-id",
"locale": "fr"
}
}

// Returns English content (default locale) as fallback
```

You can also specify a custom fallback locale:

```ts
{
"name": "findPosts",
"arguments": {
"id": "document-id",
"locale": "fr",
"fallbackLocale": "es" // Use Spanish as fallback instead of default
}
}
```

### Locale Parameters

All resource operation tools support these parameters:

| Parameter | Type | Description |
| ---------------- | -------- | ----------------------------------------------------------------------------------------------- |
| `locale` | `string` | The locale code to use for the operation (e.g., 'en', 'es'). Use 'all' to retrieve all locales. |
| `fallbackLocale` | `string` | Optional fallback locale code to use when the requested locale is not available. |

## Prompts

Prompts allow LLMs to generate structured messages for specific tasks. Each prompt defines a schema for arguments and returns formatted messages:
Expand Down
32 changes: 28 additions & 4 deletions packages/plugin-mcp/src/mcp/tools/resource/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { JSONSchema4 } from 'json-schema'
import type { PayloadRequest, TypedUser } from 'payload'

import { z } from 'zod'

import type { PluginMCPServerConfig } from '../../../types.js'

import { toCamelCase } from '../../../utils/camelCase.js'
Expand All @@ -18,6 +20,8 @@ export const createResourceTool = (
) => {
const tool = async (
data: string,
locale?: string,
fallbackLocale?: string,
): Promise<{
content: Array<{
text: string
Expand All @@ -27,7 +31,9 @@ export const createResourceTool = (
const payload = req.payload

if (verboseLogs) {
payload.logger.info(`[payload-mcp] Creating resource in collection: ${collectionSlug}`)
payload.logger.info(
`[payload-mcp] Creating resource in collection: ${collectionSlug}${locale ? ` with locale: ${locale}` : ''}`,
)
}

try {
Expand All @@ -53,6 +59,8 @@ export const createResourceTool = (
// TODO: Move the override to a `beforeChange` hook and extend the payloadAPI context req to include MCP request info.
data: collections?.[collectionSlug]?.override?.(parsedData, req) || parsedData,
user,
...(locale && { locale }),
...(fallbackLocale && { fallbackLocale }),
})

if (verboseLogs) {
Expand Down Expand Up @@ -108,13 +116,29 @@ ${JSON.stringify(result, null, 2)}
if (collections?.[collectionSlug]?.enabled) {
const convertedFields = convertCollectionSchemaToZod(schema)

// Create a new schema that combines the converted fields with create-specific parameters
const createResourceSchema = z.object({
...convertedFields.shape,
fallbackLocale: z
.string()
.optional()
.describe('Optional: fallback locale code to use when requested locale is not available'),
locale: z
.string()
.optional()
.describe(
'Optional: locale code to create the document in (e.g., "en", "es"). Defaults to the default locale',
),
})

server.tool(
`create${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
`${toolSchemas.createResource.description.trim()}\n\n${collections?.[collectionSlug]?.description || ''}`,
convertedFields.shape,
createResourceSchema.shape,
async (params: Record<string, unknown>) => {
const data = JSON.stringify(params)
return await tool(data)
const { fallbackLocale, locale, ...fieldData } = params
const data = JSON.stringify(fieldData)
return await tool(data, locale as string | undefined, fallbackLocale as string | undefined)
},
)
}
Expand Down
23 changes: 15 additions & 8 deletions packages/plugin-mcp/src/mcp/tools/resource/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ export const deleteResourceTool = (
collections: PluginMCPServerConfig['collections'],
) => {
const tool = async (
id?: string,
id?: number | string,
where?: string,
depth: number = 0,
locale?: string,
fallbackLocale?: string,
): Promise<{
content: Array<{
text: string
Expand All @@ -26,15 +28,18 @@ export const deleteResourceTool = (
}> => {
const payload = req.payload

// Convert ID to string if it's a number (for PostgreSQL compatibility)
const idString = id !== undefined ? String(id) : undefined

if (verboseLogs) {
payload.logger.info(
`[payload-mcp] Deleting resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}`,
`[payload-mcp] Deleting resource from collection: ${collectionSlug}${idString ? ` with ID: ${idString}` : ' with where clause'}${locale ? `, locale: ${locale}` : ''}`,
)
}

try {
// Validate that either id or where is provided
if (!id && !where) {
if (!idString && !where) {
payload.logger.error('[payload-mcp] Either id or where clause must be provided')
const response = {
content: [
Expand Down Expand Up @@ -78,13 +83,15 @@ export const deleteResourceTool = (
collection: collectionSlug,
depth,
user,
...(locale && { locale }),
...(fallbackLocale && { fallbackLocale }),
}

// Delete by ID or where clause
if (id) {
deleteOptions.id = id
if (idString) {
deleteOptions.id = idString
if (verboseLogs) {
payload.logger.info(`[payload-mcp] Deleting single document with ID: ${id}`)
payload.logger.info(`[payload-mcp] Deleting single document with ID: ${idString}`)
}
} else {
deleteOptions.where = whereClause
Expand Down Expand Up @@ -202,8 +209,8 @@ ${JSON.stringify(errors, null, 2)}
`delete${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
`${toolSchemas.deleteResource.description.trim()}\n\n${collections?.[collectionSlug]?.description || ''}`,
toolSchemas.deleteResource.parameters.shape,
async ({ id, depth, where }) => {
return await tool(id, where, depth)
async ({ id, depth, fallbackLocale, locale, where }) => {
return await tool(id, where, depth, locale, fallbackLocale)
},
)
}
Expand Down
Loading