MCP Infrastructure

OAuth 2.0 for Remote MCP Servers: Connecting Your SaaS to ChatGPT and Claude

What it actually takes to let ChatGPT or Claude connect to your product as a remote MCP server: dynamic client registration, PKCE, identity-provider-backed flows, discovery metadata, and mapping an outside user to the right tenant.

12 min read

A local MCP server is a script on your own machine, trusted implicitly. A remote MCP server is a different animal: it lives on the public internet, and the client connecting to it (ChatGPT, Claude, or another host) is software you don't control, acting for a user you have to authenticate from scratch. The convenience is enormous. Your users can reach your product's capabilities without leaving the assistant they already use. But the entry price is a correct, complete OAuth 2.0 implementation, built to the shape a remote MCP client expects.

This paper walks through that shape end to end, drawing on a remote MCP server we built to bring a multi-agent SaaS product inside ChatGPT: dynamic client registration, PKCE, an identity-provider-backed authorization flow, the discovery endpoints the client probes for, and, the part that's easy to underestimate, safely mapping an authenticated outside user to the right tenant in your own system. The full path was covered by a 108-test suite before it shipped.

The remote MCP OAuth flow

ChatGPT / Claude

  • Third-party client

Discovery + DCR

  • Metadata endpoints
  • Self-registration

PKCE + IdP login

  • Authorize, token
  • Hosted login

Bearer middleware

  • User to tenant
  • Refuse if unmapped

HMAC bridge

  • Signed call
  • Backend agents
The client discovers and registers, the user logs in through the identity provider, and every authenticated tool call is mapped to a tenant and bridged to the backend.

The problem: a stranger wants your tools

When a user adds your server to ChatGPT, the assistant needs to do three things: discover how to authenticate, get the user to log in and consent, and obtain a token it can present on every subsequent tool call. None of this can rely on a shared secret configured ahead of time, because the client registered itself moments ago and you have never seen it before.

That rules out the comfortable path of issuing API keys by hand. The remote MCP model assumes the client is dynamic and anonymous until it proves otherwise, and it standardizes the handshake so that any compliant client can connect to any compliant server. Your job is to implement the server half of that standard precisely. The client will not bend to a bespoke flow.

What a remote MCP client expects

Before a single tool is called, the client goes looking for metadata. It expects to find, at well-known URLs, a description of your authorization server and the protected resource: which authorization and token endpoints to use, which grant types and challenge methods you support, and what scopes exist. Concretely, that means serving protected-resource metadata, authorization-server metadata, and OpenID Connect discovery documents that are internally consistent and point at endpoints that actually behave as advertised.

Getting this metadata right is half the battle. If the discovery documents disagree with the endpoints (a token URL that 404s, a challenge method you advertise but don't accept), the client fails early and opaquely, often with no more than a generic connection error. The discipline is to treat the metadata as a contract and to make the endpoints conform to it exactly, then to verify each one in isolation.

Dynamic client registration

Because the client has no pre-existing credentials, it registers itself. Your server exposes a registration endpoint that accepts the client's details, its name and redirect URIs, and issues it a client identifier (and, where appropriate, a secret). This is Dynamic Client Registration, and it is what allows an assistant you've never integrated with to become a known client on demand.

The registration store can start simple, but it is real state: you now have a record of every client that has connected, and you can reason about it. Registration also pins the redirect URIs, which matters, because those URIs are the addresses to which authorization codes will be sent. A registration flow that accepts arbitrary redirect URIs later, at authorize time, is a redirect vulnerability waiting to happen. Register once, then honor only what was registered.

PKCE and the authorization flow

With a client identifier in hand, the assistant starts an authorization-code flow secured by PKCE (Proof Key for Code Exchange). PKCE exists precisely for clients that cannot keep a secret: instead of proving identity with a static secret, the client generates a random verifier, sends a hash of it (the challenge) when it requests authorization, and later presents the original verifier when it exchanges the code for a token. The server holds the challenge alongside the issued code and refuses the exchange if the verifier doesn't hash to it.

The sequence, in order: the client sends the user to your authorize endpoint with its client id, registered redirect URI, requested scope, and code challenge. You authenticate the user (see below), then issue a short-lived authorization code bound to that client, redirect URI, scope, and challenge. The client posts the code and its verifier to your token endpoint; you validate all four bindings, confirm the verifier matches the challenge, and only then issue an access token. Each of these bindings is a check that must be enforced server-side; skipping any one of them turns the flow into a hole.

Backing it with a real identity provider

You could implement the user-authentication step yourself, but you almost certainly shouldn't. In the ChatGPT integration, the authorize and callback steps delegate to a managed identity provider: the user is redirected to the IdP's hosted login, authenticates there, and returns to your callback with a verified identity. Your server's role is to orchestrate this, to shepherd the user out to the IdP and catch them on the way back, not to store passwords.

This delegation buys you a great deal: real login UX, multi-factor, account recovery, and the security posture of a dedicated identity system, none of which you want to reinvent inside an MCP server. Your OAuth layer becomes a broker. It speaks the remote-MCP OAuth dialect to the client on one side and the IdP's dialect on the other, translating between them and holding the short-lived state (the pending authorization sessions keyed by state parameter) that ties a given login attempt back to the client request that started it.

Mapping an outside user to a tenant

Here is the step that pure-OAuth tutorials skip and that determines whether the integration is actually safe: once you have a verified external identity, which account in your system is this? An authenticated user from the IdP is not yet a customer of your product. Bearer-auth middleware sits in front of every tool call, resolves the token to the external identity, and then maps that identity to a tenant account in your own data, refusing the call if no mapping exists.

This matters for two reasons. First, correctness: your tools operate on tenant-scoped data, and calling them without a resolved tenant is meaningless at best and a cross-tenant leak at worst. Second, isolation: the mapping is the seam where one user's ChatGPT session is bound to exactly one account's data and no other. Every tool call flows through this resolution, so the binding is enforced uniformly rather than remembered case by case. Logs at this layer redact user identifiers, because the whole point is to handle identity carefully.

Bridging tools to your backend safely

The MCP tools you expose are usually thin. They don't reimplement your product; they call it. In this integration each tool call, once authenticated and mapped to a tenant, is bridged to the existing backend over an HMAC-signed request (so the backend can verify the call genuinely came from the MCP server and wasn't forged), then the server polls the resulting agent run asynchronously and returns a formatted result to the client.

Two things make this robust. The HMAC signature means the trust boundary between the MCP server and your core backend is explicit and verifiable, not implied by network topology. And the asynchronous polling acknowledges reality: agent work takes time, MCP clients expect a timely response, and the server has to mediate between the two, starting the run, waiting within a bounded window, and returning either the result or a clear "still working" signal rather than hanging.

Testing the whole chain

An OAuth flow is a chain, and a chain is only as good as its weakest link, which is invisible until you exercise the whole thing. The integration above was covered end to end: client registration, IdP login, PKCE token exchange, bearer authentication, the tenant mapping, the HMAC bridge to the backend, agent polling, and result formatting, 108 tests in total, including the identifier-redaction helpers added for safe logging.

That investment is not gold-plating. Remote MCP integrations fail in ways that are hard to reproduce by clicking through once: a discovery document that drifts from an endpoint, a redirect URI that isn't pinned, a verifier check that's accidentally permissive, a tenant mapping that silently succeeds for the wrong account. Each of those is a single test that either passes or fails deterministically. Writing them is how you ship an integration that connects to ChatGPT or Claude and stays connected, and how you sleep at night knowing a stranger's assistant is talking to your production tools exactly as much as you allowed, and no more.