Connecting Payload CMS to the Claude desktop and web apps with OAuth

by Ric Wood | Jun 5, 2026 | AI, Web Development

If you've added the official @payloadcms/plugin-mcp to a Payload project, you've probably noticed it works beautifully with Claude Code and not at all with the Claude desktop or web apps. The plugin is solid. The problem is how those two clients expect to connect.

This is the post we wish we'd found when we hit that wall. It explains why the gap exists, and it points at the small plugin we wrote to close it.

The actual problem

The official MCP plugin authenticates with an API key. You generate a key, you hand it to the client, the client sends it on every request. Claude Code is happy with that, because you configure it locally and you control the file the key sits in.

The Claude desktop and web apps don't work that way. When you add a custom connector there, the app expects to log in to your server: discover where the auth lives, register itself, send you off to approve the connection, and come back holding a token. That's OAuth, and the API-key plugin doesn't speak it. So the connector either refuses to set up or sits there unauthenticated.

There's no flag to flip. The two clients want genuinely different things, and the official plugin only ships one of them.

What we wanted

The same Payload server we already query from Claude Code, available as a proper custom connector in the desktop and web apps, with a real login flow rather than a key copied into a settings box. Crucially, without breaking the API-key path that was already working.

The plugin

So we built the missing half and published it: @brainwebuk/payload-plugin-mcp-oauth.

It's purely additive. It wraps the MCP endpoint handler and adds the OAuth machinery the desktop and web apps need, while leaving the original API-key route completely untouched. Anything already connected to your server keeps working exactly as it did. The handler simply checks the bearer token: a pmoauth_ token takes the OAuth path, anything else falls through to the original API-key handler unchanged.

What it adds:

  • OAuth 2.1 authorization-code flow with PKCE (S256 only). The modern standard, done properly.
  • Dynamic Client Registration (RFC 7591), so Claude.ai registers itself with no manual client setup at your end.
  • Discovery documents (RFC 8414 and RFC 9728) at the .well-known paths, so the client finds the auth server on its own.
  • Tokens hashed at rest with HMAC-SHA-256, plus refresh and revocation.
  • Admin views for the tokens you've issued and the clients that have registered.

How it goes together

Five steps, none of them long, but two have a sharp edge worth flagging.

1. Install and register it after mcpPlugin(). Order matters here; the plugin throws on boot if it's registered before the MCP plugin.

2. Pass the same options object to both plugins. This is the one that'll catch you out. The plugin installs its token-validation hook by mutating the MCP options object. If you pass a fresh object or a spread copy to either call, OAuth tokens silently fail to authenticate, and because the API-key path keeps working, it's easy to miss. Assign the options to one const and reuse that exact reference in both calls.

const mcpOptions: MCPPluginConfig = {
  collections: {
    users: { enabled: { find: true, update: true } },
    media: { enabled: { find: true, create: true } },
  },
}

export default buildConfig({
  plugins: [
    mcpPlugin(mcpOptions),
    payloadMcpOAuth({
      issuer: process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000',
      mcpPluginOptions: mcpOptions, // the same object, not a copy
    }),
  ],
})

3. Wire up the proxy or middleware. Discovery and bare-host connector URLs need two host-level rewrites that a Payload plugin can't register on its own, so the package ships a ready-made handler. On Next.js 16 it goes in proxy.ts; on 14 and 15 it's middleware.ts with the same body. The catch: declare the config matcher as a local literal in that file. Don't re-export config from the package. As of Next 16, re-exporting it hard-errors and 500s every route in your app, which is a memorable afternoon.

4. Set your environment variables. A public server URL for the issuer, and PMOAUTH_TOKEN_PEPPER (32+ characters) for hashing tokens. In production the plugin throws on boot if the pepper is missing, which is the correct place to find out.

5. Regenerate the admin import map and run your migrations. The plugin adds two admin views and three collections, so a generate:importmap and your usual schema step finish the job.

Then in Claude: Settings, Connectors, Add custom connector, enter your server URL. The app handles discovery, registers itself, runs the handshake, sends you to your Payload admin login and a consent screen, and you're connected.

What we'd tell anyone hitting the same wall

If the only thing standing between you and a working desktop connector is the auth flow, you don't need to fork the official plugin or rebuild the MCP layer. The endpoint is fine. You just need the OAuth half bolted on alongside it, additively, so the key-based path you already trust stays exactly where it is.

That's the whole design philosophy of the thing, and it's the same one that runs through everything the studio builds: solve the actual problem, change as little as possible, and leave the working parts working.

npm install @brainwebuk/payload-plugin-mcp-oauth

The package is on npm, the source is on GitHub under an MIT licence, and there's an INSTALL_FOR_AGENTS.md in the package if you'd rather point a coding agent at it than follow the steps by hand.

Do you want a planet-friendly website?

Ready to make your website more sustainable? We can help you create a website that is efficient, user-friendly, and environmentally friendly. So don't wait any longer - contact us today and take the first step towards a more sustainable future!