mcp-auth-template
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-auth-templateauthenticate with client_credentials and list tools"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-auth-template
A secure remote Model Context Protocol server template. Streamable HTTP transport, OAuth 2.1, Client ID Metadata Documents, and constant-time credential verification — with a mock authorization server so the whole flow runs on your laptop with no external accounts.
▶ Live demo: mcp-auth-demo-production-d421.up.railway.app/demo/ — run the real auth flow in your browser and watch each defence accept or reject a request.
Most MCP servers today run over stdio on localhost with no authentication at all. The moment one becomes remote, it needs a real authorization story. This template is that story, built to be read: every security decision has a comment explaining why, including the places where a defence is deliberately not applied.
Scaling your company's AI integrations? Coreframe Labs helps venture-backed teams and enterprises build secure, production-grade custom software architectures. Let's design your agent infrastructure safely — talk to our Core Architects →
Why this exists
AI-assisted development produces code that passes its tests and still ships security holes, because tests assert behaviour and attackers exploit properties tests never check. Three that recur:
Naive credential comparison.
if (token === expected)returns as soon as two bytes differ. That difference is measurable, and it turns a secret into something an attacker can learn a byte at a time rather than guess whole.Secrets stored in recoverable form. A leaked config file or
docker inspecthands over working credentials.Endpoints that answer questions they were not asked. A token endpoint that responds faster for an unknown client than a wrong password is an enumeration oracle for your customer list.
None of those fail a unit test. All three are addressed here explicitly, and the reasoning is in the code.
Related MCP server: Model Context Protocol Template
What's implemented
Capability | Status |
MCP Streamable HTTP transport (POST/GET/DELETE, SSE, sessions) | official |
OAuth 2.1 | yes |
| yes |
| yes, constant-time |
Client secrets stored as scrypt hashes | yes |
Client ID Metadata Documents + SSRF hardening | yes |
RFC 8707 resource indicators (audience-bound tokens) | yes |
RFC 9728 protected resource metadata | yes |
Client assertion replay prevention ( | yes, in-memory |
Scope narrowing at the authorization server | yes |
Session ownership binding | yes |
Interactive demo with tamper scenarios | yes |
| no — returns 501 |
Token persistence, revocation, distributed state | no |
Quick start — 3 steps
Requires Node 22+. No database, no Docker, no accounts, no .env.
npm install # 1. install
npm run dev # 2. run the all-in-one demo (:3000)
open http://localhost:3000/demo/ # 3. open itThat's the whole thing. npm run dev boots the authorization server, the MCP
resource server and the interactive UI in one process. Click through the
scenarios — each runs the real flow against the server and shows the actual
HTTP exchange:
Scenario | Outcome |
Valid client, correct everything | token issued, tools callable |
Assertion signed with an unpublished key | 401 at the token endpoint |
Assertion addressed to a different token endpoint | 401 at the token endpoint |
Expired assertion | 401 at the token endpoint |
The same assertion presented twice | second exchange rejected |
Valid token missing | 403 at the MCP server |
Token with an edited payload | 401 at the MCP server |
No | 401 + |
Prefer not to install anything? The same UI is live at mcp-auth-demo-production-d421.up.railway.app/demo/.
Advanced — run the pieces separately
npm run dev:as # mock authorization server on :4000
npm run dev:rs # MCP resource server on :3000 (needs the AS running)
npm run demo # scripted client against them, prints every stepConfirm the boundary is closed:
curl -i -X POST http://localhost:3000/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'You get a 401 whose WWW-Authenticate header names the metadata document.
That is not a failure — it is how a compliant MCP client discovers where to get
a token.
You cannot complete the full flow with
curlalone: a client must publish a metadata document at its ownclient_idURL for the authorization server to fetch.npm run demo(and the browser demo) stands one up for you.
Verify everything
npm test # 132 tests
npm run typecheck
npm run buildConstant-time credential verification
The template compares secrets in constant time where a secret is actually being compared, and documents where it deliberately does not. Both halves matter — blanket application is cargo cult and hides where the real boundary is.
Where it is used
src/auth/client-secret.ts — OAuth client secrets. The primary path.
const candidate = await scrypt(presented, parsed.salt, parsed.key.length, params);
return timingSafeEqual(candidate, parsed.key);Two independent properties:
Secrets are never stored recoverably.
CLIENT_SECRETSholds salted scrypt derivations. A leaked config yields no working credential.Comparison is constant-time. It runs over fixed-width derived keys, so secrets of differing lengths compare safely —
crypto.timingSafeEqualthrows on length mismatch, and the obvious guard (if (a.length !== b.length) return false) leaks the secret's length.
Unknown clients are verified against a decoy hash, so "no such client" costs the same as "wrong secret". Without that, the endpoint enumerates your client list by latency alone. There is a test asserting both responses are byte-identical.
src/util/safe-compare.ts covers the dev/CI static-token mode, HMAC-ing both
sides under a per-process random key before comparing.
Where it is deliberately not used
JWT signature verification.
josealready does this correctly. Reimplementing it here would be a downgrade, not an improvement.Issuer, audience, scope,
client_id. None are secrets. A timing oracle on a value the attacker already supplied reveals nothing.
What does more work than constant-time comparison in this codebase is uniform
failure responses: expired, wrong-audience and bad-signature all collapse to
one invalid_token message, so probing yields no signal. A test asserts the
responses are byte-identical.
Registering a client secret
npm run hash-secret -- https://client.example/idPrints the secret once (give it to the client) and the hash (put it in
CLIENT_SECRETS). The server refuses to start if it finds a plaintext value.
Client ID Metadata Documents
Instead of pre-registering clients, a client_id is an HTTPS URL serving a
document describing the client:
{
"client_id": "https://client.example/mcp-client.json",
"client_name": "Example Client",
"token_endpoint_auth_method": "private_key_jwt",
"grant_types": ["client_credentials"],
"scope": "mcp:tools",
"jwks": { "keys": [{ "kty": "EC", "crv": "P-256", "x": "…", "y": "…" }] }
}The validation flow
sequenceDiagram
autonumber
participant C as MCP Client
participant AS as Authorization Server
participant CIMD as Client's Metadata URL
participant RS as MCP Resource Server
C->>AS: POST /token (client_credentials + private_key_jwt)
Note over AS: the assertion's iss claim IS the client_id (an https URL)
AS->>AS: allowlist + SSRF guards on client_id
AS->>CIMD: GET client_id
CIMD-->>AS: metadata document (jwks, auth method)
Note over AS: document.client_id must equal the fetched URL
Note over AS: no redirects, no private IPs, size and type limits
AS->>AS: verify assertion signature vs jwks — check aud, exp, jti
AS-->>C: access_token (aud = resource, scope narrowed)
C->>RS: POST /mcp (Authorization: Bearer TOKEN)
RS->>RS: verify JWT iss, aud, scope, signature via JWKS
RS-->>C: 200 JSON-RPC result — or 401 / 403This makes the authorization server an HTTP client pointed at an
attacker-supplied URL — a textbook SSRF sink. src/auth/cimd.ts enforces:
HTTPS only (relaxed solely for loopback dev via
CIMD_ALLOW_INSECURE)no fragment, no embedded credentials in the URL
DNS resolution checked against loopback, RFC 1918, link-local (including the
169.254.169.254cloud metadata address), CGNAT, multicast, IPv6 equivalentsredirects refused outright rather than re-validated per hop
application/jsonrequired; 64 KiB streamed cap; 5s timeoutdocument.client_idmust equal the URL it was fetched from — otherwise any host could vouch for another host's identityTTL cache over successes and failures; negative caching stops a hostile
client_idbecoming a traffic amplifieran optional allowlist, which any publicly reachable deployment must set — open resolution means anyone can make your server fetch a URL and report whether it worked
Known limitation: DNS rebinding
The guard resolves the hostname, then fetch resolves it again. An entry that
changes between those lookups defeats it. Closing this needs IP pinning with a
custom agent, or an egress allowlist proxy. Not implemented here — documented
rather than hidden.
Session security
Streamable HTTP sessions are identified by an Mcp-Session-Id header, which is
effectively a bearer credential for an already-authenticated stream. Each session
records the client that opened it; presenting someone else's session id returns
404 — identical to a nonexistent session, so the status code cannot enumerate
live sessions.
Configuration
Environment-driven, validated by Zod at boot (src/config.ts). Invalid config
fails immediately and prints every problem at once, naming the offending
variables but never their values — those are exactly where secrets live.
See .env.example for every option. There are no credentials in this repository;
the mock AS generates an ephemeral ES256 keypair per boot.
Deploying
The demo (Railway / Render)
src/demo/index.ts serves the AS, the MCP server and the demo UI on one origin.
docker build -t mcp-auth-demo .
docker run -p 3000:3000 -e PUBLIC_URL=https://your-domain mcp-auth-demoRailway: point it at this repo (railway.json is included), set PUBLIC_URL to
the public origin, then add a CNAME for your subdomain. Render: use the included
render.yaml.
PUBLIC_URL must be the externally reachable origin. It becomes the OAuth
issuer, the RFC 8707 resource identifier, and the client_id URLs the AS
dereferences. Wrong value → every token fails its audience check. The server
refuses to start on a non-HTTPS PUBLIC_URL outside loopback.
Single instance only. Sessions and the jti replay guard are per-process;
a second replica splits them and produces intermittent failures.
Vercel: not without changes
Blocker | Effect |
Sessions in an in-memory | invocations do not share memory; follow-up requests 404 |
SSE streams held open | functions have a hard max duration |
Mock AS mints an ephemeral keypair per boot | each cold start publishes different JWKS; tokens fail intermittently |
| replay protection silently stops working |
Stateless mode (sessionIdGenerator: undefined) plus a real authorization
server would work. For the full stateful behaviour, use a container host.
Security posture
CI runs typecheck, tests, build,
npm audit, gitleaks secret scanning and CodeQL (security-extended) on every push and weekly.No credentials in the repository;
.envis gitignored,.env.exampleis not.CLAUDE.mddocuments the conventions that keep this true.
The mock authorization server is a mock
In-memory state, ephemeral keys, no persistence, no revocation, no rate limiting
on the standalone entrypoint. It exists so the flow runs locally. Do not deploy
it as a real authorization server. For production, point MCP_ISSUER_URL and
MCP_JWKS_URI at a real one; the resource server does not care who issued the
token as long as it verifies.
Dependency advisory
npm audit reports a moderate advisory (GHSA-frvp-7c67-39w9) against
@hono/node-server, transitively via @modelcontextprotocol/sdk: a path
traversal in serve-static on Windows. This project serves no static files
through Hono, so the path is unreachable. npm audit fix --force would downgrade
the SDK to 1.24.3, judged the worse trade. CI fails on high/critical only.
Revisit when the SDK bumps its dependency.
Project layout
src/
app.ts MCP resource server (Express)
config.ts Zod-validated environment config
auth/
cimd.ts CIMD resolution + SSRF hardening
client-authentication.ts parsing of RFC 6749 client auth
client-secret.ts scrypt hashing + constant-time verification
verifiers.ts access token verification (JWT + static)
mcp/server.ts tool definitions
mock-as/
app.ts authorization server
verify-client.ts proves client identity per mechanism
demo/
server.ts all-in-one demo (AS + RS + UI)
scenarios.ts the eight runnable scenarios
page.ts self-contained demo UI
public-url.ts robust PUBLIC_URL resolution
util/safe-compare.ts constant-time string comparison
test/ 132 tests
scripts/
demo-client.ts scripted end-to-end client
hash-secret.ts client secret generationWork with Coreframe Labs
Scaling your company's AI integrations? We help venture-backed teams and enterprises build secure, production-grade custom software architectures. Let's design your agent infrastructure safely.
Licence
MIT. Use it, fork it, ship it.
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/CoreframeLabs/mcp-auth-template'
If you have feedback or need assistance with the MCP directory API, please join our Discord server