> ## 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.

# Building Your Own MCP Server

> A one-stop guide for developers building their own remote MCP server to run behind MCP Manager: the two things any server must do (expose Streamable HTTP and authenticate one of three supported ways), a decision tree for choosing an auth mode, a framework comparison across FastMCP, the TypeScript SDK, Cloudflare Workers, Spring AI, and Go, the universal gotchas that bite multi-instance deployments, and a troubleshooting catalog that maps each symptom you see in MCP Manager back to the server-side misconfiguration that actually causes it.

This section is for developers **building their own MCP server** to run behind **MCP Manager** — whether you're at the start, choosing a framework and deciding how to wire up authentication, or you already have a server and it won't connect cleanly. MCP Manager connects to any standards-compliant remote MCP server; this guide is about making *your* server one of them, and fixing it when it isn't.

<Note>
  Connecting a **third-party** server (Atlassian, Slack, HubSpot)? That's a different job — see [Find & Connect MCP
  Servers](/mcp-server-guides/overview) and the [per-server guides](/mcp-server-guides/overview#connection-guides). This section is about a server
  **you build and operate**.
</Note>

## Two requirements, and everything else is detail

Strip away the framework choices and a server that works behind MCP Manager does exactly two things:

1. **It's reachable over HTTPS as a Streamable HTTP MCP server.** [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports) is the current MCP remote transport — a single endpoint (commonly `/mcp`) that accepts a JSON-RPC `POST` and answers with either a JSON body or a `text/event-stream` body. **MCP Manager connects over Streamable HTTP only.** It does *not* fall back to the older, separate HTTP+SSE transport (the 2024-11-05 design with distinct `/sse` and `/messages` endpoints); a server that exposes only that legacy transport is rejected as **unsupported**. Streaming your *responses* as `text/event-stream` from the single Streamable HTTP endpoint is fine — what's unsupported is the two-endpoint legacy transport. Build on Streamable HTTP.
2. **It authenticates in one of the three ways MCP Manager supports** — standard OAuth with dynamic client registration, OAuth with client pre-registration, or a token in a custom header.

When you add a server by URL, MCP Manager runs a discovery call and connects automatically if it can. Every framework decision below maps onto these two axes.

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Lato, sans-serif','lineColor':'#6a6b76','primaryColor':'#e0e2e8','primaryTextColor':'#12141d','primaryBorderColor':'#6a6b76','edgeLabelBackground':'#ffffff','textColor':'#12141d'}}}%%
flowchart LR
  Dev["🧑‍💻<br/>Your MCP server"] --> R1{"Reachable over HTTPS<br/>as Streamable HTTP?"}
  R1 -->|"no"| F1["Expose a remote endpoint<br/>(or run it as a workstation server)"]
  R1 -->|"yes"| R2{"Authenticates a<br/>supported way?"}
  R2 -->|"OAuth + DCR"| A1["Fully automatic —<br/>paste URL, approve"]
  R2 -->|"OAuth, no DCR"| A2["Pre-registration —<br/>you issue a Client ID + Secret"]
  R2 -->|"API key / token"| A3["Token in a header"]
  A1 --> OK["✅ Governed by MCP Manager"]
  A2 --> OK
  A3 --> OK
  classDef trust fill:#2fedb4,color:#062b4c,stroke:#059669,stroke-width:1.5px;
  classDef step fill:#aed8ff,color:#062b4c,stroke:#0b4880,stroke-width:1.5px;
  class OK trust;
  class A1,A2,A3,F1 step;
```

## Choosing an authentication mode

The biggest decision is how your server authenticates, because it determines how much you build and how seamless the connect experience is. MCP Manager supports three modes (covered in depth under [Authentication & Identity](/security/authentication-and-identity#how-mcp-manager-authenticates-to-a-server)); here's how to choose.

```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
  Q1{"Do you need per-user identity<br/>(each user acts as themselves)?"}
  Q1 -->|"no — one shared service credential is fine"| T["Token in a custom header<br/>Simplest. An API key your framework validates."]
  Q1 -->|"yes"| Q2{"Can your server be an OAuth<br/>authorization server with<br/>dynamic registration?"}
  Q2 -->|"yes"| DCR["Standard OAuth + DCR<br/>Most seamless. MCP Manager self-registers; users just approve."]
  Q2 -->|"no, or you proxy an upstream IdP"| PRE["OAuth pre-registration<br/>You issue MCP Manager a Client ID + Secret once."]
  classDef step fill:#aed8ff,color:#062b4c,stroke:#0b4880,stroke-width:1.5px;
  classDef best fill:#2fedb4,color:#062b4c,stroke:#059669,stroke-width:1.5px;
  class DCR best;
  class T,PRE step;
```

<CardGroup cols={3}>
  <Card title="Standard OAuth + DCR" icon="wand-magic-sparkles">
    The most seamless. Your server is an OAuth 2.1 authorization server that supports [dynamic client registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) and publishes [well-known metadata](https://datatracker.ietf.org/doc/html/rfc8414). MCP Manager registers itself and users just approve a consent screen. **Most build effort, best UX.**
  </Card>

  <Card title="OAuth pre-registration" icon="id-card">
    Your server speaks OAuth but doesn't register clients on the fly (or proxies an upstream IdP that doesn't). You issue MCP Manager a **Client ID and
    Client Secret** once and allow its callback URL. **Less to build, still per-user.**
  </Card>

  <Card title="Token in a header" icon="key">
    No OAuth at all — your server checks an API key or bearer token. You hand MCP Manager the header name and value. **Least to build; one shared identity unless you scope tokens per user.**
  </Card>
</CardGroup>

<Tip>
  If you only need a single shared service identity and want the least to build, a **token in a header** is the fastest path to a governed server.
  Reach for **OAuth + DCR** when you want every user to authenticate as themselves with no setup on their part — that's where MCP Manager's per-user
  identity model shines. See [per-user versus shared identity](/security/authentication-and-identity#per-user-identity-versus-shared-identity).
</Tip>

## Pick your framework

The framework you choose mostly decides **how much of the OAuth work is done for you**. The key fact: only some frameworks can be an OAuth authorization server with dynamic client registration out of the box. The rest are resource servers — they verify a token but expect a separate authorization server — which maps to the pre-registration or token modes above. All of them can serve Streamable HTTP; just don't ship a server that speaks *only* the legacy HTTP+SSE transport, which MCP Manager won't connect to.

| Framework                     | Language   | OAuth + DCR built in?                             | Registered-client storage                                         | Read the cookbook                                     |
| ----------------------------- | ---------- | ------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------- |
| **FastMCP**                   | Python     | Yes — `OAuthProvider`, `OAuthProxy`, `OIDCProxy`  | In-memory on Linux by default — **must** configure a shared store | [FastMCP →](/build-your-own-mcp-server/fastmcp)       |
| **MCP TypeScript SDK**        | TypeScript | Yes — `mcpAuthRouter`, `ProxyOAuthServerProvider` | Only an in-memory demo store ships — **bring your own**           | [TypeScript →](/build-your-own-mcp-server/typescript) |
| **Cloudflare Workers**        | TypeScript | Yes — `workers-oauth-provider`                    | Workers KV — durable across instances (but eventually consistent) | [Cloudflare →](/build-your-own-mcp-server/cloudflare) |
| **Spring AI**                 | Java       | Yes — `mcp-authorization-server`                  | In-memory by default — switch to JDBC for production              | [Spring AI →](/build-your-own-mcp-server/java-spring) |
| **Go** (official SDK, mcp-go) | Go         | No — resource-server / token only                 | N/A — DCR lives in an external authorization server               | [Go →](/build-your-own-mcp-server/go)                 |

<CardGroup cols={2}>
  <Card title="FastMCP (Python)" icon="python" href="/build-your-own-mcp-server/fastmcp">
    The most popular Python framework, with full OAuth and DCR. The shared-storage gotcha and the proxy patterns, explained.
  </Card>

  <Card title="TypeScript SDK" icon="js" href="/build-your-own-mcp-server/typescript">
    The official SDK's full authorization server, plus the Vercel `mcp-handler` resource-server pattern for Next.js.
  </Card>

  <Card title="Cloudflare Workers" icon="cloudflare" href="/build-your-own-mcp-server/cloudflare">
    The closest thing to a turnkey DCR server — KV-backed storage means none of the ephemeral-client pain.
  </Card>

  <Card title="Spring AI (Java)" icon="java" href="/build-your-own-mcp-server/java-spring">
    The one JVM path to a DCR-capable server, and why you must move off the in-memory client repository.
  </Card>

  <Card title="Go (SDK + mcp-go)" icon="golang" href="/build-your-own-mcp-server/go">
    Resource-server frameworks: how to connect with pre-registration or a token when the server can't do DCR itself.
  </Card>

  <Card title="Debug Self-Hosted OAuth" icon="bug" href="/build-your-own-mcp-server/debugging-self-hosted-oauth">
    The deep dive on the single most common failure — the OAuth identity step that works, then doesn't.
  </Card>
</CardGroup>

## Five rules that prevent most problems

These cut across every framework. Get them right and the troubleshooting section below stays unread.

<Steps>
  <Step title="Serve Streamable HTTP — not the legacy SSE transport">
    Expose the current Streamable HTTP transport at a fixed `https://` path and point MCP Manager at exactly that path. Do **not** ship a server that speaks only the older HTTP+SSE transport (separate `/sse` and `/messages` endpoints) — MCP Manager won't connect to it and marks it unsupported. Watch the **trailing slash**, too: `/mcp` and `/mcp/` are different URLs, and a framework that 301-redirects between them can drop the `Authorization` header on the way.
  </Step>

  <Step title="If you do OAuth, publish complete well-known metadata">
    Serve [`/.well-known/oauth-authorization-server`](https://datatracker.ietf.org/doc/html/rfc8414) and [`/.well-known/oauth-protected-resource`](https://datatracker.ietf.org/doc/html/rfc9728), and make every URL inside them — `issuer`, `authorization_endpoint`, `token_endpoint`, `registration_endpoint`, `resource` — your **public** address, not an internal host or `localhost`. The `issuer` must exactly match the URL the metadata is served from.
  </Step>

  <Step title="Never keep DCR clients (or sessions) in process memory">
    On any host that runs more than one instance — Cloud Run, Lambda, Kubernetes replicas, anything load-balanced — a per-instance in-memory store loses the client MCP Manager just registered. Use a shared, network-accessible store and a stable encryption/signing key shared across instances. This is the number-one cause of "it worked, then it didn't." See [Debug Self-Hosted OAuth](/build-your-own-mcp-server/debugging-self-hosted-oauth).
  </Step>

  <Step title="Allow MCP Manager's callback URL">
    If your OAuth layer enforces a redirect-URI allowlist, add MCP Manager's fixed callback:

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

  <Step title="Bind tokens to your canonical public URL">
    Make the `resource`/audience your server validates equal to its canonical public URL (no trailing slash). A token minted for `https://mcp.example.com/` won't validate against `https://mcp.example.com` — a single slash is a real, common failure.
  </Step>
</Steps>

<Tip>
  If your server is private, allow MCP Manager's **single static IP** (shown at [Security → IP
  addresses](https://app.mcpmanager.ai/settings/security/ip-addresses)) through your firewall, so the backend discovery and registration calls aren't
  silently dropped.
</Tip>

## Troubleshooting: it works in a quick test, but fails in MCP Manager

If you've landed here from a search, you're probably seeing an error while connecting your own server and you suspect MCP Manager. Usually the symptom surfaces *in* MCP Manager but the cause lives *in your server's* OAuth or transport configuration — MCP Manager implements the standard flow by the book. Each entry below maps what you see to the most likely server-side cause and fix.

<Warning>
  Not every one of these is your server's fault. A few are bugs in MCP **clients** (the AI apps) that present as server problems — we flag those so
  you don't "fix" a server that's already correct.
</Warning>

<AccordionGroup>
  <Accordion title="'Client Not Registered' / client ID not found — works briefly, then fails" icon="id-card-clip">
    **You see:** an error page (often rendered by your own server) saying the client ID wasn't found in its registry, during the identity/authorize step. It's intermittent — a fresh setup works, then everyone hits it.

    **Most likely cause:** your OAuth layer stores dynamically-registered clients **in process memory**, and on a multi-instance host the authorize request lands on an instance that never saw the registration. This is structural, not flaky networking.

    **Fix:** a shared, network-accessible client store plus a stable encryption key across instances. The full diagnosis and per-framework fix is in [Debug Self-Hosted OAuth](/build-your-own-mcp-server/debugging-self-hosted-oauth).
  </Accordion>

  <Accordion title="MCP Manager can't detect authentication, or drops you to manual Client ID + Secret entry" icon="file-magnifying-glass">
    **You see:** discovery doesn't find OAuth, so MCP Manager falls back to asking for a Client ID and Secret (pre-registration) when you expected automatic OAuth.

    **Most likely cause:** your server doesn't publish complete OAuth metadata — `/.well-known/oauth-authorization-server` is missing or omits `registration_endpoint`, `/.well-known/oauth-protected-resource` (RFC 9728) is absent, or your `401` doesn't carry the challenge header that points at the metadata. A wrong `issuer` (not byte-matching the URL it's served from), or metadata advertising internal or `localhost` URLs, causes the same outcome.

    **Fix:** serve both well-known documents at your public URL, include `registration_endpoint` if you support DCR, and make `issuer` match exactly. Verify by fetching the well-known path and reading the JSON.
  </Accordion>

  <Accordion title="MCP Manager can't connect or initialize at all" icon="plug-circle-xmark">
    **You see:** the connection fails before any identity step — a transport error, a `404`/`405`, a timeout, or the server is marked **unsupported**.

    **Most likely cause:** your server speaks only the **legacy HTTP+SSE transport** (separate `/sse` and `/messages` endpoints). MCP Manager connects over **Streamable HTTP only** and does not fall back, so an SSE-only server is rejected on a content-type mismatch. Other causes: a **trailing-slash redirect** (`/mcp` ↔ `/mcp/`) that strips the request, or **Origin/CORS** validation rejecting the call. (Streaming *responses* as `text/event-stream` from your single Streamable HTTP endpoint is fully supported — only the two-endpoint legacy transport is not.)

    **Fix:** serve Streamable HTTP at the exact path with no redirect; if you validate `Origin`, allow MCP Manager rather than disabling the check.
  </Accordion>

  <Accordion title="The authorize step fails with a redirect error" icon="arrow-right-arrow-left">
    **You see:** the browser hop to authorize is rejected — a `redirect_uri` mismatch or "not registered for client."

    **Most likely cause:** your auth server enforces its own redirect-URI allowlist and MCP Manager's callback isn't on it. Some servers ignore the `redirect_uris` sent during DCR and enforce a separate list.

    **Fix:** add `https://app.mcpmanager.ai/api/v1/mcpm/inbound/oauth/callback` to your server's allowed redirect URIs.
  </Accordion>

  <Accordion title="Every call returns 401 right after connecting" icon="lock">
    **You see:** the identity is created, but every tool call comes back `401`.

    **Most likely cause:** an **audience/resource mismatch**. Your server validates the token against an audience or issuer that doesn't match what was minted — frequently a trailing-slash difference between the `resource` your metadata advertises and your server's canonical URL ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).

    **Fix:** make the advertised `resource`/audience the canonical no-trailing-slash public URL, and confirm your authorization server issues tokens with that exact `aud`.
  </Accordion>

  <Accordion title="It connects, then breaks intermittently — or works for one user but not another" icon="arrows-rotate">
    **You see:** connections or identities succeed sometimes and fail other times with no clear pattern; "session not found," or repeated re-auth prompts.

    **Most likely cause:** per-instance in-memory state on a multi-instance deployment — either DCR clients (see the first entry) or **Streamable HTTP sessions** held in one instance's memory without sticky routing. Eventually-consistent stores (for example Cloudflare KV) cause a related, self-clearing version: a token or client written on one node isn't visible on another for a short window.

    **Fix:** run the server **stateless**, enable sticky sessions, or externalize session and client state to a shared store. For eventually-consistent stores, tolerate a brief propagation delay with retries.
  </Accordion>

  <Accordion title="Connected, but no tools appear" icon="wrench">
    **You see:** the server shows connected, but the tool list is empty.

    **Most likely cause:** the token's **scopes are too narrow** to expose the scope-gated tools, the server returns an empty list instead of a `401` when auth quietly failed, or the `tools` capability wasn't advertised at `initialize`. (Client-side: some clients cache the tool list at startup — a restart can be needed.)

    **Fix:** grant the scopes the tools require, return `401` on auth failure so re-auth is triggered, and confirm your server advertises the `tools` capability.
  </Accordion>

  <Accordion title="Users have to re-authenticate constantly" icon="clock-rotate-left">
    **You see:** identities stop working after a while and users must reconnect.

    **Most likely cause:** your authorization server never issues a **refresh token**, so MCP Manager can't refresh silently when the access token expires. MCP Manager refreshes automatically when a refresh token exists.

    **Fix:** enable refresh tokens on your authorization server (include `refresh_token` in the client's `grant_types`; advertise `offline_access` if your stack uses it).
  </Accordion>
</AccordionGroup>

<Note>
  When MCP Manager hits an OAuth failure it records an **alert** with the provider's exact error and a deep link, and marks the server **Not
  connected** so you can fix the server and use the **reconnect** flow. See [Alerts](/features/alerts) and the failure-behavior notes in
  [Authentication & Identity](/security/authentication-and-identity#edge-and-failure-behavior).
</Note>

## Further reading

<CardGroup cols={2}>
  <Card title="Debug Self-Hosted OAuth" icon="bug" href="/build-your-own-mcp-server/debugging-self-hosted-oauth">
    The deep dive on the dynamic-client-registration failure that trips up multi-instance deployments.
  </Card>

  <Card title="Authentication & Identity" icon="fingerprint" href="/security/authentication-and-identity">
    The three authentication methods, per-user versus shared identity, and how credentials are stored.
  </Card>

  <Card title="Remote MCP Servers" icon="cloud" href="/mcp-gateway-concepts/mcp-servers/remote">
    How MCP Manager reaches a remote server and how to choose an authentication method.
  </Card>

  <Card title="Find & Connect MCP Servers" icon="compass" href="/mcp-server-guides/overview">
    How MCP Manager detects a server's authentication type when you add it by URL.
  </Card>
</CardGroup>

## External sources

<CardGroup cols={2}>
  <Card title="MCP transports" icon="network-wired" href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports">
    The Streamable HTTP transport every remote server should implement.
  </Card>

  <Card title="MCP authorization spec" icon="shield" href="https://modelcontextprotocol.io/specification/draft/basic/authorization">
    The OAuth 2.1-based authorization model for MCP, including discovery and DCR.
  </Card>
</CardGroup>
