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 installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/rinormaloku/mcp-inspector-v2-bug-authz'
If you have feedback or need assistance with the MCP directory API, please join our Discord server