Are you the author? Sign in to claim
OAuth / SSO bridge for remote MCP servers when your IdP does not support DCR.
OAuth in your MCP server, not an API key in your client's config.
Quickstart · Machine-to-machine · API-key gateway · Security · Alternatives · Status · Threat model · Live verification
Remote MCP servers need auth. The default is a static API key pasted into every
client config — no expiry, no per-user identity, no revocation short of rotating
the one shared secret. It's what leaks in a git add . or a support screenshot.
The MCP spec's answer is OAuth 2.1. But here's the catch: the clean self-onboarding — paste a URL and the client connects — depends on Dynamic Client Registration (DCR): the client registers itself at the identity provider automatically, no manual setup. The identity providers you actually have at work — Entra ID, Cloudflare Access, and most enterprise SSO — don't expose a DCR endpoint your client can call. So the paste-a-URL experience should just work… and against enterprise IdPs, it doesn't. You're left hand-rolling OAuth glue per deployment, or settling for manual registration that breaks the self-onboarding.
mcp-sso is the bridge. It speaks DCR, PKCE, and consent to the MCP client; your IdP stays the identity source of truth; it mints its own audience-bound tokens (each valid only for your server). Upstream IdP tokens never pass through.
sequenceDiagram
participant C as MCP client
participant B as mcp-sso bridge
participant I as Your IdP (Entra / CF Access / OIDC)
C->>B: register (DCR) + authorize (PKCE)
B->>I: user signs in at the IdP
I-->>B: verified identity (id_token / signed assertion)
B-->>C: consent screen, then a bridge-minted token (audience-bound)
C->>B: /mcp calls with the bridge token
Note over B,I: upstream IdP tokens never pass through —<br/>the bridge mints its own
The fastest start needs no IdP and no keys to generate:
node examples/fastify-sqlite/index.ts
# → prints a one-time code to the console (console-pairing identity).
claude mcp add --transport http my-bridge http://localhost:3000/mcp
# → a browser opens to the consent page; approve; the tool is callable.
For a real identity provider (Cloudflare Access or Entra), set the env vars and
point the client at a public https URL — see
examples/fastify-sqlite/ and
docs/live-verification.md for the exact setup
(including the named Cloudflare tunnel the public URL needs).
node:sqlite (recommended, zero-ops), mysql2, in-memory — one
shared conformance suite.client_credentials (M2M).jose only.client_credentials)For headless callers — CI jobs, service agents, schedulers. Implements the
official MCP extension io.modelcontextprotocol/oauth-client-credentials.
Machine clients are provisioned out-of-band — there's no HTTP endpoint for
it; you run provisionMachineClient against the same ClientStore the bridge
uses. ClientStore is a two-method port (save/find) you implement against
your own database — the shipped /store/sqlite and /store/mysql adapters are
StorePort-only (codes, refresh tokens, consent JTIs), not ClientStore. The
secret is returned once and stored only as a SHA-256 hash.
import { provisionMachineClient, noopAudit } from "mcp-sso";
const { clientId, clientSecret } = await provisionMachineClient(
{ store: clientStore, catalog: config.scopeCatalog, clock: { nowMs: () => Date.now() }, audit: noopAudit },
{ name: "nightly-sync", allowedScopes: ["mcp:read"] }, // per-client scope ceiling, fixed at provisioning
);
// clientSecret (mcs_…) is returned ONCE — put it in your secret manager now; it cannot be retrieved again.
curl -s https://auth.example.com/oauth/token -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials -d scope=mcp:read
# → { "access_token": "…", "token_type": "Bearer", "expires_in": …, "scope": "mcp:read" }
Requires stored-DCR mode (dcr: { mode: "stored", store }) and
clientCredentials: { enabled: true } in createBridgeConfig. No refresh
token (the client already holds a durable credential). The mcc_… subject
prefix and a gty: "client_credentials" marker jointly identify machine tokens
— enforced at three points, detailed in docs/contracts.md
§17.2. Rotate with rotateMachineClientSecret.
The common production shape: an internal MCP server that only accepts a static
API key. Put mcp-sso in front — users authenticate through your real IdP, the
gateway verifies its own short-lived tokens on /mcp, and the static key is
injected server-side only. It never reaches an MCP client, a laptop, or a
config file. Worked example: examples/api-key-gateway/;
full pattern, topology, and Kubernetes notes in
docs/gateway-deployment.md.
jose is the only runtime dependency; every pin is ≥15 days old before we
accept it; npm publishes run only through GitHub Actions with Sigstore
provenance, never from a local machine.exp (no access-token
introspection or revocation in v0.2; threat-model row 1).
Separate signing keys for consent vs. access; timing-safe PKCE; redirect URIs
matched against an explicit allowlist.docs/threat-model.md,
docs/authorization.md.Does your identity provider already speak DCR/OAuth 2.1? Then you don't need a
bridge — use mcp-auth (compatibility list).
If it doesn't (Entra ID, Cloudflare Access, most enterprise SSO), that's exactly
what mcp-sso is for.
| Project | Choose it if… |
|---|---|
| mcp-sso (this repo) | Your IdP doesn't speak DCR — Entra ID, Cloudflare Access, most enterprise SSO. |
mcp-auth | Your IdP already speaks DCR/OAuth 2.1; you just need resource-server wiring. |
mcp-oauth-server | You need device flow today (mcp-sso has client_credentials; device flow is v0.3). |
workers-oauth-provider | Your MCP server is a Cloudflare Worker. |
mcp-sso is live-verified against real MCP clients — Claude Code, Codex CLI,
claude.ai, ChatGPT, and the official MCP SDK on a real Cloudflare Access tenant;
Entra ID (redirect flow) with Claude Code and Claude Desktop in a real
enterprise deployment; and Google sign-in with Claude Code and the official MCP
SDK. The full provider × client matrix lives in
docs/live-verification.md.
GitHub identity preset · device authorization flow (RFC 8628).
CIMD (Client ID Metadata Documents) is implemented on main behind the S6a/S6b
work and ships in v0.3; it is not yet live-verified against a real CIMD-first
client.
MIT
Run Claude Code as an MCP server so any agent can delegate coding tasks to it
Browser automation using accessibility snapshots instead of screenshots
Google's universal MCP server supporting PostgreSQL, MySQL, MongoDB, Redis, and 10+ databases
Official GitHub integration for repos, issues, PRs, and CI/CD workflows