> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mcpmanager.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Debugging Your Self-Hosted Server's OAuth

> How to debug a self-hosted remote MCP server whose OAuth identity step fails even though the connection works: why a 'Client Not Registered' / client-ID-not-found error during dynamic client registration is almost always an ephemeral, per-instance client store on autoscaling hosts like Cloud Run, the shared-storage-plus-stable-key fix (with the FastMCP OIDCProxy example), the other common culprits — missing well-known metadata, redirect-URI allowlists, and well-known-client allowlists — a diagnostic decision flow, and the pre-registration and token fallbacks when you can't change the server.

This page is for teams running **their own remote MCP server** who hit a wall at the **identity step**: the server connects fine, but creating an identity — the OAuth handshake — fails. Almost every self-hosted OAuth problem lives on the server, not in **MCP Manager**, and they cluster into a handful of recognizable patterns. MCP Manager implements the standard OAuth flow by the book; this page helps you find what your server is doing differently and fix it.

<Note>
  If you're connecting a **third-party** server (Atlassian, Slack, HubSpot) rather than one you built, start with [Find & Connect MCP
  Servers](/mcp-server-guides/overview) and the [per-server guides](/mcp-server-guides/overview#connection-guides) instead. This page is specifically
  about debugging a server **you control** — see also [Building Your Own MCP Server](/build-your-own-mcp-server/overview).
</Note>

<Tip>
  Before changing your server, reproduce the failure in an [isolated client](/advanced/validate-mcp-servers). MCPJam's **OAuth Debugger** walks each
  stage of the handshake — discovery, dynamic client registration, redirect, and token exchange — and shows exactly where it breaks, often faster than
  reading server logs.
</Tip>

## The symptom that brings most people here

The classic report sounds like this: **connecting to the server works, but creating the identity fails** — often with a message rendered by your own server that reads something like:

```text theme={null}
Client Not Registered
The client ID a1b2c3d4-… was not found in the server's client registry.
```

Two details make this diagnosable:

* **It's the *identity* step that fails, not the connection.** A plain reachability test passes; the OAuth authorize hop is what breaks.
* **It often looks intermittent.** A fresh setup works for a little while, then the same error returns — and everyone on the team hits it.

That intermittency is the tell. The error page is served by **your server**, not by MCP Manager, and it almost always means the client MCP Manager registered a moment earlier is no longer in the place your server looks for it. The rest of this page explains why, and the much shorter list of other things it can be.

## What MCP Manager actually does during the identity step

Knowing the exact sequence tells you where to look on your server. When you add a server by URL and create an identity, MCP Manager runs the standard [OAuth 2.1](https://oauth.net/2.1/) flow the [MCP authorization spec](https://modelcontextprotocol.io/specification/draft/basic/authorization) defines:

1. **Discovery.** MCP Manager fetches your server's `/.well-known/oauth-authorization-server` ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) and, where present, the protected-resource metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)) to learn your `registration_endpoint`, `authorization_endpoint`, and `token_endpoint`.
2. **Dynamic client registration (DCR).** If a `registration_endpoint` is advertised, MCP Manager POSTs to it ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)) and your server returns a fresh `client_id`. This is a **server-to-server** call from MCP Manager's backend.
3. **Authorization.** MCP Manager redirects **your browser** to the `authorization_endpoint` with that `client_id` and a [PKCE](https://oauth.net/2/pkce/) challenge. You approve, and your server redirects back to MCP Manager's fixed callback URL.
4. **Token exchange + refresh.** MCP Manager exchanges the code at the `token_endpoint`, stores the tokens encrypted, and refreshes them automatically from then on.

The callback your server must allow is always:

```text Callback URL theme={null}
https://app.mcpmanager.ai/api/v1/mcpm/inbound/oauth/callback
```

The crucial structural fact is that **steps 2 and 3 are two separate requests from two different origins** — a backend registration call, then a browser authorize redirect:

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Lato, sans-serif','actorBkg':'#aed8ff','actorBorder':'#0b4880','actorTextColor':'#062b4c','signalColor':'#6a6b76','signalTextColor':'#12141d','noteBkgColor':'#fff8e4','noteBorderColor':'#ffa535','noteTextColor':'#12141d'}}}%%
sequenceDiagram
    autonumber
    participant M as 🛡️ MCP Manager (backend)
    actor B as 🌐 Your browser
    participant S as 🖥️ Your server (one of N instances)
    M->>S: POST registration_endpoint (DCR)
    Note right of S: Instance A stores client_id → memory
    S-->>M: client_id = a1b2c3d4-…
    M->>B: Redirect to authorize with client_id
    B->>S: GET authorize?client_id=a1b2c3d4-…
    Note right of S: Lands on instance B —<br/>never saw a1b2c3d4-…
    S-->>B: ❌ Client Not Registered
```

If those two requests reach **different instances of your server**, and the registration was only stored in the first instance's memory, the second instance has no record of the `client_id` — and renders the error you're seeing.

## Cause #1 — ephemeral client storage on a multi-instance host

This is the overwhelmingly common case, and it matches the intermittent symptom exactly.

Many MCP server frameworks **default to an in-memory store** for dynamically registered clients. On a single long-lived process that's fine. But on an autoscaling or serverless host — **Google Cloud Run**, AWS Lambda/Fargate, Kubernetes with more than one replica, anything behind a load balancer — that store is **per-instance and ephemeral**: it isn't shared between instances and doesn't survive a scale event or restart. The DCR call writes the client into one instance's memory; the browser authorize request is load-balanced to another instance that never saw it.

<Tip>
  **Why it looks intermittent.** A fresh attempt can succeed when both hops happen to land on the same warm instance, then fail minutes later once
  traffic is balanced elsewhere or the instance recycles. "It works for a bit after a fresh login, then breaks for everyone" is the signature of
  ephemeral per-instance storage — not a flaky network.
</Tip>

### The fix: shared storage + a stable key

The fix is on the server, and it has **two parts** — getting only the first is a common near-miss:

1. **Give the OAuth layer a network-accessible, shared client store** so a registration written by one instance is visible to every instance. Redis (e.g. Cloud Memorystore) is the usual choice; a database, Firestore, or object storage also work.
2. **Use a stable encryption key shared across instances.** If the client store is encrypted with a per-instance ephemeral key, a second instance still can't *decrypt* what the first wrote — so you get the same failure even with shared storage. Derive the key from a secret manager so every instance loads the same one.

<Warning>
  **`min-instances` and session affinity won't reliably fix this.** Pinning to one warm instance or enabling sticky sessions only narrows the window —
  the registration and authorize requests come from **different origins** (a backend call and a browser), so they can't be guaranteed to land
  together. Shared storage plus a stable key is the real fix; instance pinning just hides the bug until the next scale event.
</Warning>

### FastMCP's OIDCProxy, specifically

[FastMCP](https://gofastmcp.com)'s built-in `OIDCProxy` is a frequent source of this exact failure on Cloud Run, because its client store defaults to in-memory and its default encryption key is ephemeral. The fix maps directly onto the two parts above:

* Set the proxy's **`client_storage`** to a shared backend so registrations are durable and visible to every instance — see FastMCP's [`client_storage` parameter reference](https://gofastmcp.com/servers/auth/oidc-proxy#param-client-storage).
* Wrap that store so it's encrypted with a **stable key** (for example, a Fernet key derived from a Secret Manager secret), rather than the default ephemeral key.

```python Illustrative example theme={null}
# Shared, durable client store + a stable encryption key,
# so every Cloud Run instance reads the same registrations.
oidc_proxy = OIDCProxy(
    # ... your existing upstream OIDC config ...
    client_storage=shared_backed_store,   # Redis / Firestore / GCS, not in-memory
)
```

FastMCP's [OIDC Proxy documentation](https://gofastmcp.com/servers/auth/oidc-proxy) covers the cloud and multi-instance deployment guidance in full and is authoritative for the exact current API. See also the [FastMCP cookbook](/build-your-own-mcp-server/fastmcp).

<Note>
  This isn't unique to FastMCP. **Any** server that keeps DCR clients in process memory — other frameworks, or a hand-rolled OAuth layer — fails the
  same way once it runs more than one instance. The two-part fix (shared store + stable key) is identical regardless of framework, and the
  [TypeScript SDK](/build-your-own-mcp-server/typescript) and [Spring AI](/build-your-own-mcp-server/java-spring) cookbooks call out the equivalent
  for those stacks. [Cloudflare Workers](/build-your-own-mcp-server/cloudflare) avoids it by storing clients in KV.
</Note>

## After you fix the server, force MCP Manager to re-register

MCP Manager persists the `client_id` **and `client_secret`** it received during dynamic client registration and reuses that stored pair on every connection — it never re-registers on its own. So after you repair the server's storage, MCP Manager keeps presenting the `client_id` it registered against the broken deployment, your repaired server doesn't recognize it, and the same "Client Not Registered" error continues. Fixing the server is necessary but not sufficient: you also have to make MCP Manager register again.

Delete the server and re-add it to force a fresh registration:

<Steps>
  <Step title="Delete the server in MCP Manager">
    Remove it on the [MCP Servers](https://app.mcpmanager.ai/settings/servers) page. MCP Manager stops using the stale `client_id` and `client_secret`
    it stored against the old deployment.
  </Step>

  <Step title="Re-add it by the same URL">
    MCP Manager treats this as a new server, re-runs discovery and DCR, receives a fresh `client_id` and `client_secret` your repaired server
    recognizes, and stores those instead.
  </Step>
</Steps>

<Warning>
  Retrying in place won't clear this. **Authenticate now**, **Add Identity**, and **Reconnect server** all reuse the `client_id` and `client_secret`
  already stored against the old deployment, so the registration error repeats. Only deleting and re-adding the server forces MCP Manager to register
  from scratch.
</Warning>

<Tip>
  Re-add the server any time its stored registration changes underneath MCP Manager — a new client store, a wiped database, or a fresh OAuth layer.
</Tip>

## The other usual suspects

If the failure is **consistent** rather than intermittent, or the storage fix doesn't resolve it, work down this list. Each is a distinct server-side cause.

<AccordionGroup>
  <Accordion title="Missing or incomplete well-known metadata" icon="file-magnifying-glass">
    If your server doesn't publish `/.well-known/oauth-authorization-server` ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)), or it omits `registration_endpoint`, MCP Manager can't discover where to register and can't run automatic DCR — so it falls back to asking you for a Client ID and Secret. Verify what your server actually advertises by fetching that well-known path and reading the JSON. Confirm `registration_endpoint`, `authorization_endpoint`, and `token_endpoint` are present and point at reachable URLs. MCP Manager handles a server that legitimately has no metadata by guiding you to [pre-registration](/security/authentication-and-identity#oauth-with-client-pre-registration) — but if you *intended* DCR to work, missing metadata is why it didn't.
  </Accordion>

  <Accordion title="Redirect URI not allowed" icon="arrow-right-arrow-left">
    Some servers ignore the `redirect_uris` supplied during DCR and enforce their own allowlist at the authorize step. If MCP Manager's callback isn't on it, the authorize hop is rejected. Add the exact callback URL to your server's allowed redirect URIs:

    ```text theme={null}
    https://app.mcpmanager.ai/api/v1/mcpm/inbound/oauth/callback
    ```
  </Accordion>

  <Accordion title="An allowlist of well-known clients" icon="user-lock">
    A server may advertise a `registration_endpoint` but in practice only accept a fixed allowlist of well-known clients (Claude, Cursor, ChatGPT, and
    the like), rejecting any dynamically registered client. That's a limitation in the server's DCR implementation — open DCR means accepting clients
    you didn't pre-approve. Either widen the server to accept dynamically registered clients, or expose a pre-registration path so MCP Manager can
    connect with a Client ID and Secret you issue it.
  </Accordion>

  <Accordion title="Token endpoint auth method or scope mismatch" icon="key">
    MCP Manager negotiates a supported `token_endpoint_auth_method` (`client_secret_post`, `client_secret_basic`, or `none`) and requests the scopes your metadata advertises. If your server requires a method it doesn't advertise, or a scope it doesn't list in `scopes_supported`, registration can succeed while the token exchange fails. Make sure the methods and scopes your server *enforces* match the ones it *advertises* in its metadata.
  </Accordion>
</AccordionGroup>

## A quick diagnostic path

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Lato, sans-serif','lineColor':'#6a6b76','primaryColor':'#e0e2e8','primaryTextColor':'#12141d','primaryBorderColor':'#6a6b76','edgeLabelBackground':'#ffffff','textColor':'#12141d'}}}%%
flowchart TD
  A["Identity step fails"] --> B{"Does discovery find<br/>a registration_endpoint?"}
  B -->|"no"| M["Fix well-known metadata,<br/>or use pre-registration"]
  B -->|"yes"| C{"'Client Not Registered'<br/>and intermittent?"}
  C -->|"yes"| D["⚠️<br/>Ephemeral per-instance store —<br/>shared storage + stable key"]
  C -->|"no, fails consistently"| E{"Rejected at the<br/>authorize redirect?"}
  E -->|"yes"| F["Allow the callback URI,<br/>or the well-known-client allowlist"]
  E -->|"token exchange fails"| G["Reconcile advertised vs enforced<br/>auth method and scopes"]
  classDef warn fill:#ffd863,color:#12141d,stroke:#ffa535,stroke-width:1.5px;
  classDef step fill:#aed8ff,color:#062b4c,stroke:#0b4880,stroke-width:1.5px;
  class D warn;
  class M,F,G step;
```

## What MCP Manager does on its side

When the OAuth callback fails, MCP Manager doesn't fail silently. It records an **alert** (`error.inbound_server.oauth_callback_failed`) capturing the provider's error code and description and the redirect URI involved, and deep-links you to it so you can read exactly what the upstream returned. The server's identity then shows a **Not connected** state. Because MCP Manager reuses the `client_id` and `client_secret` it already stored, retrying in place won't pick up a server-side fix that changed the client registration — delete and re-add the server to force a fresh registration, as in the section above. Retrying in place resolves the failure only when the stored registration is still valid, and it also lets you switch the server to pre-registration or token auth if you decide not to rely on DCR. See [Alerts](/features/alerts) and [Authentication & Identity](/security/authentication-and-identity#edge-and-failure-behavior).

<Tip>
  If your server is private, remember MCP Manager's discovery and registration calls come from a **single static IP**, shown at [Security → IP
  addresses](https://app.mcpmanager.ai/settings/security/ip-addresses). Allowlist it so those backend hops aren't silently dropped by a firewall. See
  [Remote MCP Servers](/mcp-gateway-concepts/mcp-servers/remote#why-use-a-remote-server).
</Tip>
