built-in-authorization-server
built-in-authorization-server
Install & run
npm install
PORT=3005 MCP_FAKE_AUTH=1 MCP_FAKE_IDP=1 npm startServer URL is http://localhost:3005; apiPrefix is auth.
Related MCP server: OAuth MCP Server
Two modes
REAL mode
Provide real provider credentials in the environment and the real GitHub provider is wired exactly as documented:
GITHUB_CLIENT_ID=xxx GITHUB_CLIENT_SECRET=yyy JWT_SECRET=<32+ chars> PORT=3014 npm startThe interactive browser leg (/auth/authorize → GitHub → /auth/callback)
requires a live GitHub App and is only exercised in this mode.
FAKE mode (offline, MCP_FAKE_AUTH=1)
When MCP_FAKE_AUTH=1 is set and no real credentials are present, dummy
clientId/clientSecret are supplied so the module constructs without ever
contacting an IdP. Every offline-reachable feature works:
Discovery:
/.well-known/oauth-authorization-server,/.well-known/oauth-protected-resourceDynamic Client Registration (RFC 7591):
POST /auth/register(deprecated in MCP revision2026-07-28, still fully supported here)PKCE required and advertised (
code_challenge_methods_supported: ["S256"])JWT validation of locally-signed tokens by the
/mcpmiddlewareGuarded MCP calls with a locally-minted JWT
Mint a local token (signed with the same jwtSecret, so validateToken accepts
it without any IdP call):
TOKEN=$(npx ts-node scripts/mint-jwt.ts)
bunx @modelcontextprotocol/inspector --cli http://localhost:3014/mcp \
--transport http --header "Authorization: Bearer $TOKEN" --method tools/listWith MCP_FAKE_AUTH=1 alone the authorize → callback interactive leg still
needs the external IdP and is not runnable. Add MCP_FAKE_IDP=1 (below) to
get an offline stand-in for it.
What was verified (FAKE mode)
Discovery JSON shape matches the doc.
POST /auth/registerreturns a registered client (client_id).PKCE methods advertised.
/mcpaccepts a valid locally-minted JWT (whoami→Hello, Ada Lovelace!), rejects missing / malformed / wrong-secret tokens with401.disableEndpointsdisables a discovery route (→404) while keeping the other.@McpUser()projectsreq.user.
Tier 4 walkthrough: consent screen and Client ID Metadata Documents
Three opt-in environment flags, all default off. With none of them set this example behaves exactly as it did before they existed — same routes, same metadata document, same GitHub provider, same log output.
Flag | Effect |
| Turns on the interactive consent screen ( |
| Accepts URL-shaped |
| ⚠️ Replaces GitHub with a local stub that authenticates everyone as "Ada Lovelace", with no network call. Implies |
FAKE-mode shortcuts, stated plainly
Everything below except MCP_FAKE_IDP=1 is real production behaviour. Two things
are shortcuts and neither is safe outside a demo:
MCP_FAKE_IDP=1— there is no authentication. A real deployment sends the user to GitHub/Google/Keycloak here. It exists only so the browser leg (and therefore the consent screen) can be walked through offline.allowInsecureClientIdScheme: true, whichMCP_CIMD=1sets — this acceptshttp://client_idURLs and disables the SSRF guard, which is the only reasonhttp://localhost:3014/client-metadata.jsonworks as aclient_id. With it on, an unauthenticated caller can make this server fetch e.g.https://169.254.169.254/...(the cloud instance-metadata endpoint). In production the document must live on a publichttpsorigin and this option must stayfalse.
A. Consent screen only
PORT=3014 MCP_FAKE_IDP=1 MCP_CONSENT=1 npm startRegister a client and start an authorization request in a browser:
CLIENT_ID=$(curl -s -X POST http://localhost:3014/auth/register \
-H 'content-type: application/json' \
-d '{"client_name":"DCR Consent Demo","redirect_uris":["http://127.0.0.1:33418/callback"]}' \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["client_id"])')
echo "http://localhost:3014/auth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=demo-state"Open that URL. The fake IdP bounces straight back to /auth/callback, which now
answers with the consent screen instead of redirecting. It shows:
the client name, and who you are signed in as ("Ada Lovelace"),
127.0.0.1— the redirect URI hostname, as its own prominent field (draft/basic/authorization/security-considerationsmakes displaying this a MUST for a CIMD-capable server),an orange loopback warning, because the code will be delivered to a port on this machine that any local process could have bound (a SHOULD),
the scopes that will actually be granted — already narrowed, so you see what you are approving rather than what was requested.
Click Approve and the browser lands on
http://127.0.0.1:33418/callback?code=…&state=demo-state&iss=http://localhost:3014.
Nothing is listening on 33418, so the browser shows a connection error — the
code in the address bar is the point. Click Deny instead and you get
?error=access_denied&… on the same URI (RFC 6749 §4.1.2.1).
The code_challenge above is the S256 hash of the verifier
sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ, so you can redeem the code:
curl -s -X POST http://localhost:3014/auth/token \
-H 'content-type: application/x-www-form-urlencoded' \
--data-urlencode grant_type=authorization_code \
--data-urlencode "code=<the code from the address bar>" \
--data-urlencode code_verifier=sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ \
--data-urlencode redirect_uri=http://127.0.0.1:33418/callback \
--data-urlencode "client_id=$CLIENT_ID"Approving is remembered for the same (user, client, scope) triple, so a second
authorization request goes straight through with no screen. Start the server with
MCP_CONSENT=1 and consent.rememberForMs: 0 in main.ts to prompt every time.
B. Client ID Metadata Documents
PORT=3014 MCP_FAKE_IDP=1 MCP_CIMD=1 npm startThe server now advertises support, and hosts a document as if it were the client:
curl -s http://localhost:3014/.well-known/oauth-authorization-server \
| grep -o '"client_id_metadata_document_supported":true'
curl -s http://localhost:3014/client-metadata.json{
"client_id": "http://localhost:3014/client-metadata.json", // MUST match this URL exactly
"client_name": "CIMD Demo Client",
"client_uri": "http://localhost:3014",
"redirect_uris": [
"http://localhost:3014/demo-callback", // a viewable stand-in
"http://127.0.0.1:33418/callback" // what a real MCP client uses
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none" // CIMD clients are public
}There is no registration step. The URL is the client_id. Open:
http://localhost:3014/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3014%2Fclient-metadata.json&redirect_uri=http%3A%2F%2Flocalhost%3A3014%2Fdemo-callback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=demo-state&scope=offline_accessThe server fetches the document, validates it, and shows the consent screen — with one extra sentence in the loopback warning, because a name that came from a document anyone may reference deserves less trust than a registered one:
Authorization code will be sent to
localhost
http://localhost:3014/demo-callback
This client receives the authorization code on this computer.
localhost is a loopback address, so any program running locally could have
opened that port and be impersonating CIMD Demo Client, whose name and logo
come from a document that anyone may reference. Approve only if you just
started this application yourself.
Client identifier http://localhost:3014/client-metadata.json
Client website http://localhost:3014
Requested access offline_accessClick Approve and this time the browser lands on a page that shows you the
code, state and iss, because /demo-callback is served by this example.
Redeem it with the same URL as client_id (and no secret — it is a public
client):
curl -s -X POST http://localhost:3014/auth/token \
-H 'content-type: application/x-www-form-urlencoded' \
--data-urlencode grant_type=authorization_code \
--data-urlencode "code=<the code>" \
--data-urlencode code_verifier=sSQKjtmhf3TsxI14yDaGqZpyHg7l1hzoYGMHvRtknJQ \
--data-urlencode redirect_uri=http://localhost:3014/demo-callback \
--data-urlencode client_id=http://localhost:3014/client-metadata.json{ "access_token": "eyJhbGciOiJIUzI1NiIs…", "token_type": "bearer", "expires_in": 86400, "refresh_token": "…" }That token works against /mcp like any other:
bunx @modelcontextprotocol/inspector --cli http://localhost:3014/mcp \
--transport http --header "Authorization: Bearer <access_token>" --method tools/listRedemption used the document snapshot taken at /authorize, not a second
fetch — so the grant is pinned to exactly the metadata the user saw.
C. Rejections worth seeing
Every one of these is an HTTP 400 with a message naming the reason ("if the
authorization server fails to retrieve the client metadata document, it SHOULD
abort the authorization request"). Run with MCP_CIMD=1 and vary client_id:
Q='response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A3014%2Fdemo-callback&code_challenge=WMlOi-4SP-ouuMs3uOb7jhkkMKK5SSJ9Z8Eagvs1wmk&code_challenge_method=S256&state=s'
# nothing hosted there
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014%2Fnope.json"
# → client metadata document request returned HTTP 404
# no path component
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014"
# → client_id must contain a path component, e.g. https://example.com/client.json
# dot segments
curl -s "http://localhost:3014/auth/authorize?$Q&client_id=http%3A%2F%2Flocalhost%3A3014%2Fa%2F..%2Fclient-metadata.json"
# → client_id must not contain single-dot or double-dot path segments
# a URI the document does not declare
curl -s "http://localhost:3014/auth/authorize?response_type=code&client_id=http%3A%2F%2Flocalhost%3A3014%2Fclient-metadata.json&redirect_uri=http%3A%2F%2Fevil.example%2Fcb&code_challenge=x&code_challenge_method=S256"
# → Invalid redirect_uriThe SSRF refusals (private/loopback addresses, non-https schemes) are not
visible in this demo, because allowInsecureClientIdScheme: true is exactly the
switch that disables them. Drop MCP_CIMD=1's hatch (edit main.ts) and
http://… and https://127.0.0.1/… are both refused before any connection is
attempted. tests/mcp-oauth-cimd.e2e.spec.ts covers that posture.
D. Everything at once
PORT=3014 MCP_FAKE_IDP=1 MCP_CONSENT=1 MCP_CIMD=1 npm startRunning against the workspace build? When this example is installed with a
file:dependency (scripts/use-examples.sh local) the symlinked packages resolve a second@nestjs/corefrom the workspace root, which Nest's injector rejects. AddNODE_OPTIONS=--preserve-symlinks— this is a linking artifact, not a product issue, and is whate2e/harness.tsdoes. A normalnpm installof the published packages needs nothing.
This server cannot be deployed
Maintenance
Related MCP Connectors
- StytchOAuthdev.stytch.mcp
The Stytch MCP server is a reference implementation that demonstrates remote MCP server authentication and authorization using Stytch Connected Apps. It provides OAuth 2.1-compliant authorization (including PKCE), Dynamic Client Registration, and validates Stytch-issued access tokens to enable AI agents to securely interact with external services through permissioned access, supporting scopes like openid, email, profile, and manage:project_data.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
FusionAuth Documentation MCP server
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA self-hostable OAuth 2.0 server designed for the Model-Context-Protocol (MCP) that enables you to secure your MCP applications with a robust implementation you control.1,843 npm112ISC
- FlicenseNot gradedqualityDmaintenanceAn MCP server for OAuth 2.0 authentication supporting Device Code and Client Credentials flows, enabling secure token management for MCP applications.-
- FlicenseNot gradedqualityDmaintenanceA proof-of-concept MCP server implementing OAuth 2.1 authorization with CIMD client registration and PKCE, demonstrating protected resource access and step-up authentication.-
- AlicenseNot gradedqualityDmaintenanceDrop-in OAuth 2.1 + Dynamic Client Registration for MCP servers, providing authentication middleware and token verification.5 npm1MIT