payloadcms / payloadcms/payload

plugin-mcp: updating a localized array in one locale breaks other locales when row ids are echoed from a read

Open
#18,063 0 comments 0 reactions 1 assignee View on GitHub

@AlessioGr is already working on this.

Since Sep 11, 2026.

Bug db: mongodb db: postgres db: sqlite plugin: mcp
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

When a document with a localized array field is updated through @payloadcms/plugin-mcp, the update breaks as soon as the MCP client echoes array row ids that were read from a different locale — which is exactly what every LLM client naturally does (read the document, translate/modify the rows, write them back to another locale).

Rows of a localized array exist once per locale, keyed by their row id. The plugin's findDocuments tool (called without locale) returns the default locale's rows including their row ids, and the collection input schema declares items[].id as a settable field. So when the model then calls updateDocument with locale: "es", it sends the English rows' ids under the Spanish locale. Result:

  • Postgres / SQLite: the whole update fails with the misleading error The following field is invalid: id. The array rows live in a single table whose primary key is the row id with a _locale column, so writing the same id under a second locale is a unique constraint violation (23505), which handleUpsertError converts into that ValidationError. The model gets no hint about the real cause, retries with different shapes, and in my case ended up rewriting the array in ways that destroyed the other locale's rows and their relationships.
  • MongoDB: the update "succeeds" but silently stores the same row id under two locales — corrupt data that can never be written to a relational adapter again.

Nothing in the tool schema or descriptions tells the client that row ids are per-locale, and the plugin passes the ids straight through to payload.update, so any MCP client that follows the normal read → modify → write loop hits this.

I've opened a PR with a failing-test reproduction and a fix: the update tools now drop incoming row ids inside localized arrays/blocks that don't belong to a row of the locale being updated (ids that do belong to the target locale are kept, so partial row updates still merge correctly, and ids of non-localized arrays — which are required to keep localized subfields attached to their rows — are never touched).

Link to the code that reproduces this issue

https://github.com/tgriek/payload/blob/fix/plugin-mcp-cross-locale-row-ids/test/plugin-mcp/localizedArrays.int.spec.ts

(Integration test against the monorepo's test/plugin-mcp suite. On current main, the tests should generate new row ids when an update echoes row ids read from another locale and should strip row ids inside localized arrays when updating by where clause fail on Postgres with The following field is invalid: id, and the row-id assertion fails on MongoDB.)

Reproduction Steps

Config: a localized collection with a localized array containing a relationship:

// localization: { defaultLocale: 'en', locales: ['en', 'es', 'fr'], fallback: true }
{
  slug: 'localized-items',
  fields: [
    { name: 'title', type: 'text' },
    {
      name: 'items',
      type: 'array',
      localized: true,
      fields: [
        { name: 'label', type: 'text' },
        { name: 'rel', type: 'relationship', relationTo: 'users' },
      ],
    },
  ],
}
  1. Create a document with English content:
    await payload.create({
      collection: 'localized-items',
      data: { title: 'demo', items: [{ label: 'english label', rel: user.id }] },
      locale: 'en',
    })
    
  2. Through the MCP endpoint, call the findDocuments tool for that document without a locale (what LLM clients do). The response contains the English rows with their row ids, e.g. {"items":[{"label":"english label","rel":1,"id":"6a95360f851349b6a0032645"}]}.
  3. Call the updateDocument tool for another locale, echoing those rows back — again what LLM clients do:
    {
      "collectionSlug": "localized-items",
      "id": 1,
      "locale": "es",
      "data": {
        "items": [
          { "id": "6a95360f851349b6a0032645", "label": "spanish label", "rel": 1 }
        ]
      }
    }
    
  4. On Postgres/SQLite the tool returns Error updating document in collection "localized-items": The following field is invalid: id and the Spanish locale is never written. On MongoDB, payload.findByID({ locale: 'all' }) now shows the same row id under items.en and items.es.

Runnable version of the same steps, from the monorepo:

git fetch https://github.com/tgriek/payload fix/plugin-mcp-cross-locale-row-ids
git checkout FETCH_HEAD -- test/plugin-mcp
git stash push packages/plugin-mcp   # only needed if you also fetched the fix
pnpm run test:int:postgres plugin-mcp/localizedArrays
Which area(s) are affected?

plugin: mcp, db: postgres, db: sqlite, db: mongodb

Environment Info
Reproduced on payload main (4.0.0-canary.14), packages built from source
@payloadcms/plugin-mcp: 4.0.0-canary.14
@payloadcms/db-postgres / db-mongodb: 4.0.0-canary.14
Node.js: v25.8.0
OS: macOS (darwin 24.6.0)
Databases: PostgreSQL (ghcr.io/payloadcms/postgis-vector:latest) and MongoDB via the repo's docker compose

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.