fix(plugin-mcp)!: respect schema access and use req.user consistently#17159
fix(plugin-mcp)!: respect schema access and use req.user consistently#17159AlessioGr wants to merge 9 commits into
Conversation
| const inputSchema = getCollectionInputSchema({ | ||
| collectionSlug, | ||
| req, | ||
| ...(permissions ? { permissions } : {}), |
There was a problem hiding this comment.
Passing permissions (optional arg) now makes getCollectionInputSchema strip access-denied fields from the schema
| * }) | ||
| * ``` | ||
| */ | ||
| export const localAPIDefaults = ( |
There was a problem hiding this comment.
@DanRibbens this being removed should make you happy!
| * to this tool for the collection. The shared tool is not advertised when no collections allow | ||
| * it, but can remain advertised when it is available for another collection. This is skipped | ||
| * when `overrideAccess` is enabled. | ||
| */ |
There was a problem hiding this comment.
The shared tool is not advertised when no collections allow
it, but can remain advertised when it is available for another collection.
This needed clarification in the jsdocs, as it was not clearly evident whether this stops advertising the tool or not, and when
📦 esbuild Bundle Analysis for payloadThis analysis was generated by esbuild-bundle-analyzer. 🤖
Largest pathsThese visualization shows top 20 largest paths in the bundle.Meta file: packages/next/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_index.json, Out file: esbuild/index.js
Meta file: packages/payload/meta_shared.json, Out file: esbuild/exports/shared.js
Meta file: packages/richtext-lexical/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_client.json, Out file: esbuild/exports/client_optimized/index.js
Meta file: packages/ui/meta_shared.json, Out file: esbuild/exports/shared_optimized/index.js
DetailsNext to the size is how much the size has increased or decreased compared with the base branch of this PR.
|
| @@ -0,0 +1,515 @@ | |||
| import type { | |||
There was a problem hiding this comment.
This is a new, internal plugin for our test suite that adds rbac for every field, collection and global.
It allows us to greatly simplify access checks in test. We no longer have to adjust the schema or add new fields to test access. Instead, we can define access control within the test itself and assert accordingly.
In short: it allows us to define access control as data on the user object, instead of as schema in the payload config
Example:
const apiKey = await getApiKey({
fields: {
// Internally this creates a new user doc with this added to the rbac json field in that user doc
'site-settings.contactEmail': {
update: false,
},
},
})
const client = await mcp.connect(apiKey)
const schemaResponse = await client.callTool({
arguments: { globalSlug: 'site-settings' },
name: 'getGlobalSchema',
})
const schema = getToolDoc<JsonSchemaType>(schemaResponse)
expect(schema.properties?.siteName).toBeDefined()
expect(schema.properties?.contactEmail).toBeUndefined()Previously, we would have needed to bloat our config with a new contactEmailWithUpdateAccessFalse field to test this. Now, we can define on the user doc that this user should not have update access to this specific field.
| return apiKey | ||
| } | ||
|
|
||
| export async function getLimitedApiKey(): Promise<string> { |
There was a problem hiding this comment.
Thanks to the rbac helper plugin, we no longer need these pre-seeded users and pre-created access control functions that return false for specific users.
Access-aware schemas
getCollectionSchemaandgetGlobalSchemanow return only the schema that the current MCP user can write to. A collection schema requires create or update access, while a global schema requires update access. Fields, including nested fields and blocks, are included when the user can write them through at least one permitted operation.overrideAccess: truecontinues to return the complete schema.For example, this collection contains a field that cannot be created or updated:
Previously,
getCollectionSchema({ collectionSlug: 'posts' })still advertised both fields:{ "type": "object", "properties": { "title": { "type": "string" }, "internalNotes": { "type": "string" } } }It now omits
internalNotesfor that user:{ "type": "object", "properties": { "title": { "type": "string" } } }This avoids exposing schema details the user cannot use and makes it less likely that an LLM will send data that Payload rejects.
This filtering is limited to MCP schema generation and happens before we call our core schema generation function.
Authentication cleanup
This change also removes the duplicate user value from
AuthorizedMCP. Payload authentication already stores the user onreq.user, so keeping a second copy could allow access checks and local API calls to use different identities, as there may be drift between req.user and AuthorizedMCP['user']. MCP now usesreq.useras the single source of truth. CustomoverrideGetAuthorizedMCPimplementations must set it to the resolved Payload user, or tonullfor an anonymous request.