Skip to content

Commit a3f490b

Browse files
stevenceuppensclaudekendelljoseph
authored
feat(plugin-mcp): add localization support to MCP resource operations (#14334)
### What? Adds full localization support to the MCP plugin's resource operations (create, update, find, delete). ### Why? The MCP plugin did not expose Payload's localization capabilities. Users with multilingual content could not create or manage translations through MCP, limiting the plugin's usefulness for international projects. This brings MCP feature parity with Payload's REST API. ### How? - Add `locale` and `fallbackLocale` parameters to all resource tools (create, update, find, delete) - Pass locale parameters through to underlying Payload operations - Add comprehensive integration tests for localization features (6 new tests, all passing) - Update documentation with localization usage examples and MCP client configuration - Follow Payload REST API localization pattern for consistency **Testing:** ```bash pnpm test:int plugin-mcp # All 15 tests passing (9 existing + 6 new localization tests) ``` **Example Usage:** ```json // Create content in English { "name": "createPosts", "arguments": { "title": "Hello", "locale": "en" }} // Add Spanish translation { "name": "updatePosts", "arguments": { "id": "123", "title": "Hola", "locale": "es" }} // Retrieve all translations { "name": "findPosts", "arguments": { "id": "123", "locale": "all" }} ``` Fixes # --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Kendell Joseph <kjoseph@figma.com>
1 parent e1168a0 commit a3f490b

File tree

9 files changed

+710
-65
lines changed

9 files changed

+710
-65
lines changed

docs/plugins/mcp.mdx

Lines changed: 96 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,94 @@ export default config
112112
| `mcp.serverOptions.serverInfo.name` | `string` | The name of the MCP server (default: 'Payload MCP Server'). |
113113
| `mcp.serverOptions.serverInfo.version` | `string` | The version of the MCP server (default: '1.0.0'). |
114114

115+
## Connecting to MCP Clients
116+
117+
After installing and configuring the plugin, you can connect apps with MCP client capabilities to Payload.
118+
119+
### Step 1: Create an API Key
120+
121+
1. Start your Payload server
122+
2. Navigate to your admin panel at `http://localhost:3000/admin`
123+
3. Go to the **MCP → API Keys** collection
124+
4. Click **Create New**
125+
5. Allow or Disallow MCP traffic permissions for each collection (enable find, create, update, delete as needed)
126+
6. Click **Create** and copy the uniquely generated API key
127+
128+
### Step 2: Configure Your MCP Client
129+
130+
MCP Clients can be configured to interact with your MCP server.
131+
These clients require some JSON configuration, or platform configuration in order to know how to reach your MCP server.
132+
133+
<Banner type="warning">
134+
Caution: the format of these JSON files may change over time. Please check the
135+
client website for updates.
136+
</Banner>
137+
138+
Our recommended approach to make your server available for most MCP clients is to use the [mcp-remote](https://www.npmjs.com/package/mcp-remote) package via `npx`.
139+
140+
Below are configuration examples for popular MCP clients.
141+
142+
#### [VSCode](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
143+
144+
```json
145+
{
146+
"mcp.servers": {
147+
"Payload": {
148+
"command": "npx",
149+
"args": [
150+
"-y",
151+
"mcp-remote",
152+
"http://127.0.0.1:3000/api/mcp",
153+
"--header",
154+
"Authorization: Bearer API-KEY-HERE"
155+
]
156+
}
157+
}
158+
}
159+
```
160+
161+
#### [Cursor](https://cursor.com/docs/context/mcp)
162+
163+
```json
164+
{
165+
"mcpServers": {
166+
"Payload": {
167+
"command": "npx",
168+
"args": [
169+
"-y",
170+
"mcp-remote",
171+
"http://localhost:3000/api/mcp",
172+
"--header",
173+
"Authorization: Bearer API-KEY-HERE"
174+
]
175+
}
176+
}
177+
}
178+
```
179+
180+
#### Other MCP Clients
181+
182+
For connections without using `mcp-remote` you can use this configuration format:
183+
184+
```json
185+
{
186+
"mcpServers": {
187+
"Payload": {
188+
"type": "http",
189+
"url": "http://localhost:3000/api/mcp",
190+
"headers": {
191+
"Authorization": "Bearer API-KEY-HERE"
192+
}
193+
}
194+
}
195+
}
196+
```
197+
198+
## Customizations
199+
200+
The plugin supports fully custom `prompts`, `tools` and `resources` that can be called or retrieved by MCP clients.
201+
After defining a custom method you can allow / disallow the feature from the admin panel by adjusting the `API Key` MCP Options checklist.
202+
115203
## Prompts
116204

117205
Prompts allow models to generate structured messages for specific tasks. Each prompt defines a schema for arguments and returns formatted messages:
@@ -126,7 +214,7 @@ prompts: [
126214
content: z.string().describe('The content to review'),
127215
criteria: z.array(z.string()).describe('Review criteria'),
128216
},
129-
handler: ({ content, criteria }) => ({
217+
handler: ({ content, criteria }, req) => ({
130218
messages: [
131219
{
132220
content: {
@@ -154,6 +242,7 @@ resources: [
154242
description: 'Company content creation guidelines',
155243
uri: 'guidelines://company',
156244
mimeType: 'text/markdown',
245+
handler: (uri, req) => ({
157246
handler: (uri, req) => ({
158247
contents: [
159248
{
@@ -198,7 +287,6 @@ tools: [
198287
description: 'Get useful scores about content in posts',
199288
handler: async (args, req) => {
200289
const { payload } = req
201-
202290
const stats = await payload.find({
203291
collection: 'posts',
204292
where: {
@@ -249,6 +337,12 @@ mcpPlugin({
249337
{ label: 'Marketing', value: 'marketing' },
250338
],
251339
})
340+
341+
// You can also add hooks
342+
collection.hooks?.beforeRead?.push(({ doc, req }) => {
343+
req.payload.logger.info('Before Read MCP hook!')
344+
return doc
345+
})
252346
return collection
253347
},
254348
// ... other options
@@ -363,43 +457,3 @@ const config = buildConfig({
363457
],
364458
})
365459
```
366-
367-
## MCP Clients
368-
369-
MCP Clients can be configured to interact with your MCP server. These clients require some JSON configuration, or platform configuration in order to know how to reach your MCP server.
370-
371-
> Caution: the format of these JSON files may change over time. Please check the client website for updates.
372-
373-
[VSCode](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
374-
375-
```json
376-
{
377-
"mcp.servers": {
378-
"Payload": {
379-
"url": "http://localhost:3000/api/mcp",
380-
"headers": {
381-
"Authorization": "Bearer API-KEY-HERE"
382-
}
383-
}
384-
}
385-
}
386-
```
387-
388-
[Cursor](https://cursor.com/docs/context/mcp)
389-
390-
```json
391-
{
392-
"mcpServers": {
393-
"Payload": {
394-
"command": "npx",
395-
"args": [
396-
"-y",
397-
"mcp-remote",
398-
"http://localhost:3000/api/mcp",
399-
"--header",
400-
"Authorization: Bearer API-KEY-HERE"
401-
]
402-
}
403-
}
404-
}
405-
```

packages/plugin-mcp/src/mcp/tools/resource/create.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
import type { JSONSchema4 } from 'json-schema'
33
import type { PayloadRequest, TypedUser } from 'payload'
44

5+
import { z } from 'zod'
6+
57
import type { PluginMCPServerConfig } from '../../../types.js'
68

79
import { toCamelCase } from '../../../utils/camelCase.js'
@@ -18,6 +20,9 @@ export const createResourceTool = (
1820
) => {
1921
const tool = async (
2022
data: string,
23+
draft: boolean,
24+
locale?: string,
25+
fallbackLocale?: string,
2126
): Promise<{
2227
content: Array<{
2328
text: string
@@ -27,7 +32,9 @@ export const createResourceTool = (
2732
const payload = req.payload
2833

2934
if (verboseLogs) {
30-
payload.logger.info(`[payload-mcp] Creating resource in collection: ${collectionSlug}`)
35+
payload.logger.info(
36+
`[payload-mcp] Creating resource in collection: ${collectionSlug}${locale ? ` with locale: ${locale}` : ''}`,
37+
)
3138
}
3239

3340
try {
@@ -51,9 +58,12 @@ export const createResourceTool = (
5158
const result = await payload.create({
5259
collection: collectionSlug,
5360
data: parsedData,
61+
draft,
5462
overrideAccess: false,
5563
req,
5664
user,
65+
...(locale && { locale }),
66+
...(fallbackLocale && { fallbackLocale }),
5767
})
5868

5969
if (verboseLogs) {
@@ -109,13 +119,39 @@ ${JSON.stringify(result, null, 2)}
109119
if (collections?.[collectionSlug]?.enabled) {
110120
const convertedFields = convertCollectionSchemaToZod(schema)
111121

122+
// Create a new schema that combines the converted fields with create-specific parameters
123+
const createResourceSchema = z.object({
124+
...convertedFields.shape,
125+
draft: z
126+
.boolean()
127+
.optional()
128+
.default(false)
129+
.describe('Whether to create the document as a draft'),
130+
fallbackLocale: z
131+
.string()
132+
.optional()
133+
.describe('Optional: fallback locale code to use when requested locale is not available'),
134+
locale: z
135+
.string()
136+
.optional()
137+
.describe(
138+
'Optional: locale code to create the document in (e.g., "en", "es"). Defaults to the default locale',
139+
),
140+
})
141+
112142
server.tool(
113143
`create${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
114144
`${collections?.[collectionSlug]?.description || toolSchemas.createResource.description.trim()}`,
115-
convertedFields.shape,
145+
createResourceSchema.shape,
116146
async (params: Record<string, unknown>) => {
117-
const data = JSON.stringify(params)
118-
return await tool(data)
147+
const { draft, fallbackLocale, locale, ...fieldData } = params
148+
const data = JSON.stringify(fieldData)
149+
return await tool(
150+
data,
151+
draft as boolean,
152+
locale as string | undefined,
153+
fallbackLocale as string | undefined,
154+
)
119155
},
120156
)
121157
}

packages/plugin-mcp/src/mcp/tools/resource/delete.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ export const deleteResourceTool = (
1515
collections: PluginMCPServerConfig['collections'],
1616
) => {
1717
const tool = async (
18-
id?: string,
18+
id?: number | string,
1919
where?: string,
2020
depth: number = 0,
21+
locale?: string,
22+
fallbackLocale?: string,
2123
): Promise<{
2224
content: Array<{
2325
text: string
@@ -28,7 +30,7 @@ export const deleteResourceTool = (
2830

2931
if (verboseLogs) {
3032
payload.logger.info(
31-
`[payload-mcp] Deleting resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}`,
33+
`[payload-mcp] Deleting resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ' with where clause'}${locale ? `, locale: ${locale}` : ''}`,
3234
)
3335
}
3436

@@ -80,6 +82,8 @@ export const deleteResourceTool = (
8082
overrideAccess: false,
8183
req,
8284
user,
85+
...(locale && { locale }),
86+
...(fallbackLocale && { fallbackLocale }),
8387
}
8488

8589
// Delete by ID or where clause
@@ -204,8 +208,8 @@ ${JSON.stringify(errors, null, 2)}
204208
`delete${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
205209
`${collections?.[collectionSlug]?.description || toolSchemas.deleteResource.description.trim()}`,
206210
toolSchemas.deleteResource.parameters.shape,
207-
async ({ id, depth, where }) => {
208-
return await tool(id, where, depth)
211+
async ({ id, depth, fallbackLocale, locale, where }) => {
212+
return await tool(id, where, depth, locale, fallbackLocale)
209213
},
210214
)
211215
}

packages/plugin-mcp/src/mcp/tools/resource/find.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ export const findResourceTool = (
1515
collections: PluginMCPServerConfig['collections'],
1616
) => {
1717
const tool = async (
18-
id?: string,
18+
id?: number | string,
1919
limit: number = 10,
2020
page: number = 1,
2121
sort?: string,
2222
where?: string,
23+
locale?: string,
24+
fallbackLocale?: string,
2325
): Promise<{
2426
content: Array<{
2527
text: string
@@ -30,7 +32,7 @@ export const findResourceTool = (
3032

3133
if (verboseLogs) {
3234
payload.logger.info(
33-
`[payload-mcp] Reading resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ''}, limit: ${limit}, page: ${page}`,
35+
`[payload-mcp] Reading resource from collection: ${collectionSlug}${id ? ` with ID: ${id}` : ''}, limit: ${limit}, page: ${page}${locale ? `, locale: ${locale}` : ''}`,
3436
)
3537
}
3638

@@ -67,6 +69,8 @@ export const findResourceTool = (
6769
overrideAccess: false,
6870
req,
6971
user,
72+
...(locale && { locale }),
73+
...(fallbackLocale && { fallbackLocale }),
7074
})
7175

7276
if (verboseLogs) {
@@ -120,6 +124,8 @@ ${JSON.stringify(doc, null, 2)}`,
120124
page,
121125
req,
122126
user,
127+
...(locale && { locale }),
128+
...(fallbackLocale && { fallbackLocale }),
123129
}
124130

125131
if (sort) {
@@ -190,8 +196,8 @@ Page: ${result.page} of ${result.totalPages}
190196
`find${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
191197
`${collections?.[collectionSlug]?.description || toolSchemas.findResources.description.trim()}`,
192198
toolSchemas.findResources.parameters.shape,
193-
async ({ id, limit, page, sort, where }) => {
194-
return await tool(id, limit, page, sort, where)
199+
async ({ id, fallbackLocale, limit, locale, page, sort, where }) => {
200+
return await tool(id, limit, page, sort, where, locale, fallbackLocale)
195201
},
196202
)
197203
}

0 commit comments

Comments
 (0)