Sso MCP Server
OAuth 2.1 and enterprise SSO for remote MCP servers without API keys in client configs.
This grade comes from an automated MCPSafe scan of the server's JavaScript, TypeScript and Python source on 2026-10-02. It read the whole repository (144 files). It checks for leaked credentials, shell or code injection from function inputs, unsafe deserialization, malware signatures, install-time scripts, and weak password handling. It does not check for path traversal, server-side request forgery, prompt injection in tool descriptions, network exposure, SQL injection, cross-site scripting, or vulnerable dependencies. It has not been manually reviewed, and the code may have changed since the scan.
Excellent Security
Trust Score 70
Trust is stricter than security: it combines reviewed security, MCP capability risk, supply-chain metadata, quality, popularity, freshness, and penalties for unknowns.
- • No reviewed severe vulnerabilities published
- • Good documentation surface
- • Solid supply-chain metadata
- • Stale or missing recent scan
- • MCP risk traits: Runs shell commands, Network/API access, Requires secrets
Security ScoreA+
Vulnerabilities Found
Quality ScoreC
Maintenance50
Popularity44
Documentation100
Available Documentation
Compatibility78
Features
Supported Transports
Vulnerabilities(0)
This grade comes from an automated scan. Individual findings from automated scans are not published.
READMESso MCP Server
View sourcemcp-sso
OAuth 2.1 for your MCP server, so nobody pastes a shared API key into a client config.
      
Get started · Documentation · Threat model · Conformance matrix · Verification status
The problem
A remote MCP server needs to know who is calling. The usual answer is a static API key pasted into every client configuration. One key gives every caller the same identity, never expires, and leaks in a git add . or a support screenshot. Revoking it means rotating it for everyone.
MCP's answer is OAuth 2.1. That leaves a gap. An MCP client must identify itself to an authorization server and obtain a token that is valid for exactly your server. Identity providers such as Microsoft Entra ID and Cloudflare Access prove who the user is, but they do not speak the MCP client-facing flow: client registration, PKCE, consent, and audience-bound tokens.
mcp-sso fills that gap. It is a TypeScript library in two halves:
- The authorization-server bridge accepts the client's identity (a Client ID Metadata Document, or
POST /oauth/register), sends the user to your identity provider, shows a consent page, and mints its own access and refresh tokens for one MCP resource. - The resource-server verifier checks the token's signature, audience, and scope on every
/mcprequest, and answers with the RFC 9728WWW-Authenticatechallenge when it rejects one.
Your identity provider's tokens stay between the provider and mcp-sso. The MCP client only ever holds a token that mcp-sso minted for your server.
Install
npm install mcp-ssoNode 24 or later. jose is the only runtime dependency. Fastify, Express, Hono, mysql2, ioredis, and @fastify/rate-limit are optional peer dependencies: install the ones you use.
Try it on one machine
This runs a local server that uses console pairing as the identity check. It listens on loopback and refuses a non-loopback host, issuer, or resource before it writes any state.
npx mcp-sso init my-mcp-server
cd my-mcp-server
npm install
npm startIn another terminal, add it to an MCP client. With Claude Code:
claude mcp add --transport http my-bridge http://127.0.0.1:3000/mcpThe server prints a one-time code. Paste it into the browser, approve, and call a tool.
[!WARNING]
Anyone who can read that code becomes the console-operator identity. Keep the server on loopback and its console private. This is a tutorial, not a production template. For real users, follow the identity-provider tutorial.What the flow looks like
Assume Alice connects an MCP client to https://mcp.example/mcp.
- The client identifies itself with a Client ID Metadata Document (CIMD) or by calling
POST /oauth/register. mcp-ssosends Alice to your identity provider. The provider proves who she is.- Alice sees the client, the resource, and the requested scopes, and approves.
- The client exchanges the authorization code and its PKCE verifier for tokens. The access token is valid for
https://mcp.example/mcpand nothing else. - Your MCP server verifies the token's audience and required scope on every request. Refresh-token rotation detects a replayed refresh token and revokes the whole token family.
sequenceDiagram
participant C as MCP client
participant B as mcp-sso
participant I as Identity provider
participant S as MCP server
C->>B: CIMD identity or POST /oauth/register
C->>B: Authorization request with PKCE
B->>I: Sign Alice in
I-->>B: Verified identity
B-->>C: Consent, then authorization code
C->>B: Authorization code and PKCE verifier
B-->>C: Tokens for mcp.example/mcp
C->>S: MCP request with access token
S->>S: Verify audience and required scopeHow the OAuth roles fit together explains each step and where a request can stop.
Protect a route
The bridge issues tokens. The verifier is what your /mcp handler calls. This fragment shows the shape; the complete, tested composition is `examples/fastify-sqlite/`.
import { RequestAuthorizer, authorizationOccurrences, buildUnauthorizedChallenge, OAuthError, SystemClock, noopAudit } from "mcp-sso";
// `config` is the BridgeConfig you built with createBridgeConfig().
const authorizer = new RequestAuthorizer({ config, clock: new SystemClock(), audit: noopAudit });
app.post("/mcp", async (request, reply) => {
let auth;
try {
// authorizationOccurrences passes the raw Authorization occurrence array into
// RequestAuthorizer (§8.4): a framework-normalized scalar would let a duplicated
// Authorization header silently select one of the two credentials.
auth = await authorizer.authorize({
authorization: authorizationOccurrences(request.raw.headersDistinct, request.headers),
requiredScope: "mcp:read",
});
} catch (error) {
const oe = error instanceof OAuthError ? error : new OAuthError("invalid_token", "Bearer token is invalid", 401);
reply.header("www-authenticate", buildUnauthorizedChallenge(config, { scope: config.scopeCatalog, error: oe.code, errorDescription: oe.message }));
return reply.code(oe.status).send({ jsonrpc: "2.0", error: { code: -32001, message: `${oe.code}: ${oe.message}` }, id: null });
}
// auth.subject, auth.scopes, and auth.credentialKind ("interactive" or "machine") are verified here.
});RequestAuthorizer.authorize fails closed. A missing token, a wrong audience, an expired token, or a missing scope throws an OAuthError whose status is 401 or 403. There is no local or unauthenticated mode. Both the tested example and the fragment above read the header through authorizationOccurrences(request.raw.headersDistinct, ...), which passes the raw Authorization occurrence array into RequestAuthorizer so a duplicated header is rejected instead of silently collapsed. Never pass request.headers.authorization directly: the framework-normalized scalar has already discarded every occurrence but one.
What ships
| Area | Current source |
| --- | --- |
| Identity options | Cloudflare Access, Microsoft Entra ID (redirect flow plus group-to-scope ceilings), Google, generic OIDC, console pairing for local use, and claims-only website login |
| Client registration | CIMD, and POST /oauth/register in stateless or stored mode |
| Frameworks | Fastify, Express, Hono. OAuth decisions stay in the framework-free core |
| Stores | node:sqlite (one host), MySQL (shared replicas), memory (one process). One shared conformance suite, also published as mcp-sso/testing/* |
| Grants | Authorization code with PKCE S256, refresh-token rotation with family revocation, client_credentials for machine clients |
| Operations | JSONL and webhook audit sinks, Redis or Valkey rate limiting, a Fastify /mcp rate-limit helper |
| Runtime dependency | jose, and nothing else |
Not shipped: a GitHub identity port and the device authorization grant are contract-only. Neither is a release claim. The capability reference has the exact list and the deployment limits, including that one bridge protects one resource.
Why you can trust it
Security products earn trust with artifacts, not adjectives. These are the ones this project publishes.
- A STRIDE threat model with 49 attacker-driven rows, the control for each, and the residual risk that remains.
- A conformance matrix against MCP Authorization
2026-07-28and CIMD draft-00, row by row, with the two deviations recorded rather than hidden. - A release verification matrix that binds every shipped feature to a test row, plus packed-artifact checks, plus a dated live client matrix against real identity providers and real MCP clients.
- A dependency ledger:
joseis the only runtime dependency, every pin is recorded with its publish date, and ordinary updates wait 15 days. npm publishes run only from GitHub Actions with OIDC provenance, never from a laptop. - Fail-closed defaults. Ambiguous configuration, a missing identity, an unknown audience, or a replayed refresh token is a hard failure, never a degraded default. Authorization codes and refresh tokens are stored as hashes and are single-use. A stolen access token is a bearer token and stays valid until
exp, which is why it is short-lived (threat model row 1).
When to use something else
Use mcp-sso when your identity provider authenticates users but does not expose MCP-compatible OAuth endpoints.
| Project | Choose it if |
| --- | --- |
| mcp-sso (this repo) | Your identity provider is not an MCP authorization server: Entra ID, Cloudflare Access, most enterprise SSO. |
| `mcp-auth` | Your identity provider already exposes compatible OAuth 2.1 client registration and you only need resource-server wiring (compatibility list). |
| `mcp-oauth-server` | You need the device authorization grant today. mcp-sso ships client_credentials, not device flow. |
| `workers-oauth-provider` | Your MCP server is a Cloudflare Worker. |
Documentation
The documentation index routes by what you are trying to do. The short version:
- Learn: Get started.
- Do: configure client registration, provision a machine client, put SSO in front of an API-key backend, ship audit events, run live verification.
- Look up: configuration, contracts, threat model, verification status.
- Understand: OAuth roles and flow, client registration choices, redirect URI trust, rate limits and client IP trust, CIMD fetch safety.
License
MIT
Embed Security Badge
Add this badge to your README or documentation
[](https://mcpsafe.org/registry/acartag7-mcp-sso)Server Information
- Source
- GITHUB
- Language
- go
- License
- MIT
- Transport
- STDIO
- Added
- Aug 2, 2026
- Updated
- Oct 1, 2026