Skip to content

Commit c979fb3

Browse files
feat(plugin-mcp): add depth parameter support to all MCP find resource tools (#14931)
### What Adds `depth` parameter support to MCP resource tools (find and create), allowing MCP clients to control relationship population depth when querying or creating documents. ### Why MCP clients currently have no way to control how deeply relationships are populated when retrieving or creating documents. When collections have relationship fields, agents need the ability to either: - Get just the IDs (depth=0) for lightweight responses - Get fully populated relationship data (depth=1+) for richer context This can significantly reduce the token count when reading documents. ### How - Added `depth` parameter to `findResources` and `createResource` schemas in schemas.ts - Updated `find.ts` to pass `depth` to both `payload.find()` and `payload.findByID()` calls - Updated `create.ts` to pass `depth` to `payload.create()` call - Parameter is an optional integer (0-10) with default of 0 - Added integration tests --------- Co-authored-by: Kendell Joseph <kjoseph@figma.com>
1 parent 1a3aeb8 commit c979fb3

6 files changed

Lines changed: 825 additions & 728 deletions

File tree

packages/plugin-mcp/src/mcp/getMcpHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const getMCPHandler = (
4444
req: PayloadRequest,
4545
) => {
4646
const { payload } = req
47-
const configSchema = configToJSONSchema(payload.config)
47+
const configSchema = configToJSONSchema(payload.config, payload.db.defaultIDType, req.i18n)
4848

4949
// Handler wrapper that injects req before the _extra argument
5050
const wrapHandler = (handler: (...args: any[]) => any) => {

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const createResourceTool = (
2020
) => {
2121
const tool = async (
2222
data: string,
23+
depth: number = 0,
2324
draft: boolean,
2425
locale?: string,
2526
fallbackLocale?: string,
@@ -58,6 +59,7 @@ export const createResourceTool = (
5859
const result = await payload.create({
5960
collection: collectionSlug,
6061
data: parsedData,
62+
depth,
6163
draft,
6264
overrideAccess: false,
6365
req,
@@ -122,6 +124,14 @@ ${JSON.stringify(result, null, 2)}
122124
// Create a new schema that combines the converted fields with create-specific parameters
123125
const createResourceSchema = z.object({
124126
...convertedFields.shape,
127+
depth: z
128+
.number()
129+
.int()
130+
.min(0)
131+
.max(10)
132+
.optional()
133+
.default(0)
134+
.describe('How many levels deep to populate relationships in response'),
125135
draft: z
126136
.boolean()
127137
.optional()
@@ -144,10 +154,11 @@ ${JSON.stringify(result, null, 2)}
144154
`${collections?.[collectionSlug]?.description || toolSchemas.createResource.description.trim()}`,
145155
createResourceSchema.shape,
146156
async (params: Record<string, unknown>) => {
147-
const { draft, fallbackLocale, locale, ...fieldData } = params
157+
const { depth, draft, fallbackLocale, locale, ...fieldData } = params
148158
const data = JSON.stringify(fieldData)
149159
return await tool(
150160
data,
161+
depth as number,
151162
draft as boolean,
152163
locale as string | undefined,
153164
fallbackLocale as string | undefined,

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export const findResourceTool = (
2020
page: number = 1,
2121
sort?: string,
2222
where?: string,
23+
depth: number = 0,
2324
locale?: string,
2425
fallbackLocale?: string,
2526
draft?: boolean,
@@ -67,6 +68,7 @@ export const findResourceTool = (
6768
const doc = await payload.findByID({
6869
id,
6970
collection: collectionSlug,
71+
depth,
7072
overrideAccess: false,
7173
req,
7274
user,
@@ -121,6 +123,7 @@ ${JSON.stringify(doc, null, 2)}`,
121123
// Otherwise, use find to get multiple documents
122124
const findOptions: Parameters<typeof payload.find>[0] = {
123125
collection: collectionSlug,
126+
depth,
124127
limit,
125128
overrideAccess: false,
126129
page,
@@ -199,8 +202,8 @@ Page: ${result.page} of ${result.totalPages}
199202
`find${collectionSlug.charAt(0).toUpperCase() + toCamelCase(collectionSlug).slice(1)}`,
200203
`${collections?.[collectionSlug]?.description || toolSchemas.findResources.description.trim()}`,
201204
toolSchemas.findResources.parameters.shape,
202-
async ({ id, draft, fallbackLocale, limit, locale, page, sort, where }) => {
203-
return await tool(id, limit, page, sort, where, locale, fallbackLocale, draft)
205+
async ({ id, depth, draft, fallbackLocale, limit, locale, page, sort, where }) => {
206+
return await tool(id, limit, page, sort, where, depth, locale, fallbackLocale, draft)
204207
},
205208
)
206209
}

packages/plugin-mcp/src/mcp/tools/schemas.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ export const toolSchemas = {
3434
.describe(
3535
'Optional: specific document ID to retrieve. If not provided, returns all documents',
3636
),
37+
depth: z
38+
.number()
39+
.int()
40+
.min(0)
41+
.max(10)
42+
.optional()
43+
.default(0)
44+
.describe('How many levels deep to populate relationships (default: 0)'),
3745
draft: z
3846
.boolean()
3947
.optional()
@@ -82,6 +90,14 @@ export const toolSchemas = {
8290
description: 'Create a document in a collection.',
8391
parameters: z.object({
8492
data: z.string().describe('JSON string containing the data for the new document'),
93+
depth: z
94+
.number()
95+
.int()
96+
.min(0)
97+
.max(10)
98+
.optional()
99+
.default(0)
100+
.describe('How many levels deep to populate relationships in response (default: 0)'),
85101
draft: z
86102
.boolean()
87103
.optional()

packages/plugin-mcp/src/utils/convertCollectionSchemaToZod.ts

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,70 @@ import { jsonSchemaToZod } from 'json-schema-to-zod'
44
import * as ts from 'typescript'
55
import { z } from 'zod'
66

7+
/**
8+
* Recursively processes JSON schema properties to simplify relationship fields.
9+
* For create/update validation we only need to accept IDs (string/number),
10+
* not populated objects. This removes the $ref option from oneOf unions
11+
* that represent relationship fields, leaving only the ID shape.
12+
*
13+
* NOTE: This function must operate on a cloned schema to avoid mutating
14+
* the original JSON schema used for tool listing.
15+
*/
16+
function simplifyRelationshipFields(schema: JSONSchema4): JSONSchema4 {
17+
if (!schema || typeof schema !== 'object') {
18+
return schema
19+
}
20+
21+
const processed = { ...schema }
22+
23+
if (Array.isArray(processed.oneOf)) {
24+
const hasRef = processed.oneOf.some(
25+
(option) => option && typeof option === 'object' && '$ref' in option,
26+
)
27+
28+
processed.oneOf = processed.oneOf.map((option) => {
29+
if (option && typeof option === 'object' && '$ref' in option) {
30+
// Replace unresolved $ref with a permissive object schema to keep the union shape
31+
return { type: 'object', additionalProperties: true }
32+
}
33+
return simplifyRelationshipFields(option)
34+
})
35+
}
36+
37+
if (processed.properties && typeof processed.properties === 'object') {
38+
processed.properties = Object.fromEntries(
39+
Object.entries(processed.properties).map(([key, value]) => [
40+
key,
41+
simplifyRelationshipFields(value),
42+
]),
43+
)
44+
}
45+
46+
if (processed.items && typeof processed.items === 'object' && !Array.isArray(processed.items)) {
47+
processed.items = simplifyRelationshipFields(processed.items)
48+
}
49+
50+
return processed
51+
}
52+
753
export const convertCollectionSchemaToZod = (schema: JSONSchema4) => {
54+
// Clone to avoid mutating the original schema (used elsewhere for tool listing)
55+
const schemaClone = JSON.parse(JSON.stringify(schema)) as JSONSchema4
56+
857
// Remove properties that should not be included in the Zod schema
9-
delete schema?.properties?.createdAt
10-
delete schema?.properties?.updatedAt
58+
delete schemaClone?.properties?.id
59+
delete schemaClone?.properties?.createdAt
60+
delete schemaClone?.properties?.updatedAt
61+
if (Array.isArray(schemaClone.required)) {
62+
schemaClone.required = schemaClone.required.filter((field) => field !== 'id')
63+
if (schemaClone.required.length === 0) {
64+
delete schemaClone.required
65+
}
66+
}
67+
68+
const simplifiedSchema = simplifyRelationshipFields(schemaClone)
1169

12-
const zodSchemaAsString = jsonSchemaToZod(schema)
70+
const zodSchemaAsString = jsonSchemaToZod(simplifiedSchema)
1371

1472
// Transpile TypeScript to JavaScript
1573
const transpileResult = ts.transpileModule(zodSchemaAsString, {

0 commit comments

Comments
 (0)