Skip to content

plugin-mcp: revalidatePath/revalidateTag called in hooks during MCP tool calls are silently dropped #18490

Open

Description

@LukasHechenberger

Describe the Bug

Changes made through the MCP plugin's tools are saved, but revalidatePath/revalidateTag calls from collection or global hooks (e.g. afterChange) have no effect, so statically rendered pages keep showing the old content. The same change saved in the admin panel revalidates as expected. Nothing throws or logs.

Cause: the /api/mcp endpoint (built on mcp-handler) returns its text/event-stream response as soon as the stream opens and runs the tool while the body streams. mcp-handler's response adapter resolves the Response once the headers are written, before the tool has finished. Next.js applies a route handler's pending revalidations when the handler returns, so revalidations called during the tool call come too late and are dropped. Locally, the handler returned about 120 ms before revalidatePath ran.

Workaround: wrap the plugin and let the POST /mcp handler read the whole response before returning, so the tool finishes inside the handler:

const awaitResponseBody =
  (handler: PayloadHandler): PayloadHandler =>
  async (request) => {
    const response = await handler(request);
    return new Response(await response.arrayBuffer(), response);
  };

Possible fix in the plugin: buffer the response in the /mcp endpoint like above (Payload's tools only return a single result). Alternatively, mcp-handler could expose the SDK transport's enableJsonResponse option, but it currently only passes sessionIdGenerator.

Longer write-up: https://lukashechenberger.com/blog/your-payload-mcp-edits-never-show-up-on-the-site

Link to the code that reproduces this issue

https://github.com/ls-age/landing-page/blob/b5f19afba95ac54cb156758bb5458468456c5db2/apps/web/src/payload/mcp.ts

(A public site that contains the workaround. Using @payloadcms/plugin-mcp directly instead of this wrapper in payload.config.ts reproduces the bug.)

Reproduction Steps

  1. Add an afterChange hook that calls revalidatePath('/', 'layout') to a collection that a statically rendered page reads.
  2. Update a document through an MCP update tool (POST /api/mcp).
  3. The document is updated, but the page still shows the old content. Saving the same document in the admin panel refreshes it.

Which area(s) are affected?

plugin: mcp

Environment Info

payload: 3.90.2
@payloadcms/plugin-mcp: 3.90.2
mcp-handler: 1.1.0
next: 16.3.6
Node.js: 24

Activity

  1. aayushbaluni commented on Oct 6, 2026

    @aayushbaluni

    Your diagnosis matches the code on the branch you are running, and it does not
    match main — which I think makes this a "which branch gets the fix" question
    rather than a "what is the cause" one.

    On 3.x (packages/plugin-mcp/package.json): version 3.90.2, exactly
    what you report, depending on mcp-handler: ^1.0.7 — consistent with the
    mcp-handler: 1.1.0 in your environment block. So the mechanism you describe,
    the response resolving once the stream's headers are written and the tool
    finishing afterwards, applies to the code you are actually running.

    On main: plugin-mcp is 4.0.0-canary.37 and mcp-handler is gone. The
    endpoint now uses @modelcontextprotocol/server@2.3.1 via createMcpHandler
    with

    maxSubscriptions: 0,   // "SDK subscriptions always use SSE, so disable them
    responseMode: 'json',  //  to keep every response JSON-only."

    and the legacy path passes enableJsonResponse: true. That migration landed in
    d947584 (2026-07-06), before this report. A JSON-mode response has to be
    buffered to be emitted at all, so await handler.fetch(...) should not resolve
    until the tool has returned — which is the thing that was going wrong for you.

    So the possibilities look like:

    1. the bug is specific to the mcp-handler era and is already moot on 4.x, in
      which case this is a backport decision for 3.x rather than a fix on main; or
    2. it survives the migration for some reason I cannot see from reading, in which
      case the cause in the report needs revisiting against the new transport.

    I would rather not guess which. If a maintainer says 1 and wants 3.x fixed, I
    am happy to open a PR against that branch using the shape from your wrapper —
    reading the response body to completion inside the handler so the tool finishes
    before Next.js flushes pending revalidations — with the afterChange +
    revalidatePath case as a test. If the answer is "upgrade to 4.x", that is worth
    saying on the issue so the next person finds it.

    Caveat on my side: this is verification of the code and the dependency graph on
    both branches, not a reproduction. I have not run either version, so if you can
    reproduce on a 4.x canary that settles it against option 2 immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions