understanding-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
understanding-mcp
A zero-dependency Node.js playground for debugging how MCP clients authenticate to an MCP server over HTTP. It runs two servers in one process:
MCP server (resource server) on
http://localhost:3001— a minimal streamable-HTTP MCP server (initialize,tools/list,tools/call,ping).OAuth authorization server on
http://localhost:3002— discovery metadata, Dynamic Client Registration (DCR), authorization + token endpoints.
Every request and every response — headers included — is printed to the console and
kept in a ring buffer you can fetch from GET /__log on either server. Point a real
MCP client at http://localhost:3001/mcp (or run node client.js) and watch the whole
OAuth handshake happen hop by hop.
No npm install, no dependencies. Requires Node 18+.
Quick start
node server.js # terminal 1: watch every request/response
node client.js # terminal 2: drive the full flow end-to-endclient.js models how a real MCP client connects:
POST an MCP request without a token.
Read the
WWW-Authenticatechallenge. If it carriesresource_metadata, use that URL; otherwise construct it from the MCP endpoint path:<mcp_base_url>/.well-known/oauth-protected-resource/<url_remaining_path>.Fetch the protected resource metadata, read
authorization_servers.Discover the authorization server metadata by building
<auth_server_base_url>/.well-known/oauth-authorization-server/<authz_server_remaining_url>.If
registration_endpointis advertised → DCR (POST /register). If it is absent → skip straight to the preconfigured client_id.Run the authorization-code flow (PKCE S256 +
resourceparameter +issvalidation), exchange the code for an access token.Retry the MCP request with
Authorization: Bearer <token>.
Related MCP server: Vulnerable MCP Server
Endpoints
Server | Endpoint | Purpose |
MCP (3001) |
| Streamable-HTTP MCP endpoint. No/expired token → |
MCP (3001) |
| RFC 9728 protected resource metadata. Only the path derived from the MCP endpoint serves it; any other |
MCP (3001) |
| Recent request/response log entries as JSON. |
AUTH (3002) |
| RFC 8414 authorization server metadata. |
AUTH (3002) |
| RFC 7591 Dynamic Client Registration. |
AUTH (3002) |
| Authorization endpoint (auto-approves, redirects with |
AUTH (3002) |
| Token endpoint ( |
AUTH (3002) |
| Recent request/response log entries as JSON. |
Configuration
All settings are environment variables with sensible defaults.
Variable | Default | Meaning |
|
| Canonical URI of the MCP server. Everything else (listen port, endpoint path, |
| (from | Override for the MCP server's listen port. |
| (origin of | Override for the origin used to build well-known URLs. |
| (path of | Override for the MCP endpoint path. |
|
| Port of the authorization server. |
|
| Issuer of the authorization server. |
|
|
|
|
|
|
| (empty = allow any) | Comma-separated whitelist. When set, DCR rejects any |
|
| Static client_id available without registration (public client, no secret). |
|
| redirect_uris allowed for the preconfigured client. |
|
| Access token lifetime. |
Reproduce each scenario
# 1. DCR succeeds -> token issued to a dynamically registered client
node server.js
node client.js
# 2. DCR advertised, but registration rejected (redirect URI not whitelisted)
# -> client falls back to the preconfigured client_id
DCR_ALLOWED_REDIRECT_URIS='http://only-whitelisted.example/callback' node server.js
node client.js
# 3. No DCR at all -> client uses the preconfigured client_id directly
AUTH_DCR=off node server.js
node client.js
# 4. 401 challenge without resource_metadata -> client derives the well-known URI
WWW_AUTH_RESOURCE_METADATA=off node server.js
node client.jsYou can also connect a real MCP client to http://localhost:3001/mcp and watch the
handshake in the server log.
Findings worth blogging about
These are the non-obvious behaviors the log surfaces. All four are reproducible above.
1. WWW-Authenticate may or may not tell you where the metadata lives
The 401 challenge can carry resource_metadata="<uri>" (RFC 9728 §5.1), or just
Bearer scope="...". When it does not, the client has to guess the URL by
inserting the MCP endpoint's path:
<mcp_base_url>/.well-known/oauth-protected-resource/<url_remaining_path>.
Set WWW_AUTH_RESOURCE_METADATA=off and the log shows the client doing exactly this
derivation.
2. Metadata gives you no guarantee that DCR will succeed
This is the big one. The presence of registration_endpoint only means the endpoint
exists — it says nothing about whether your registration will be accepted. Real
servers (e.g. Figma) advertise DCR but enforce a redirect-URI whitelist, so a
client registering its own redirect_uri gets a 400 invalid_redirect_uri.
Reproduce it: set DCR_ALLOWED_REDIRECT_URIS to anything that isn't the client's
callback. The metadata still advertises registration_endpoint, /register still
responds, and registration still fails. A robust client therefore treats DCR as
best-effort and falls back to a preconfigured client_id when registration is
rejected — client.js prints this fallback explicitly.
3. Preconfigured client_id skips DCR entirely
When the client already has a client_id for this server (many MCP clients let you
configure one), it never calls /register. It goes straight to
authorization_endpoint + token_endpoint from the metadata. Set AUTH_DCR=off to
see the pure preconfigured flow.
4. Scope challenges happen at runtime too
A token with mcp but not tools:execute gets a 403 with
WWW-Authenticate: Bearer error="insufficient_scope", scope="tools:execute" when
calling tools/call — a step-up authorization trigger, not a login failure.
Manual debugging
Without client.js, you can drive the flow by hand:
# discover
curl -i http://localhost:3001/mcp -X POST -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}'
curl http://localhost:3001/.well-known/oauth-protected-resource/mcp
curl http://localhost:3002/.well-known/oauth-authorization-server
# dynamic client registration
curl -i http://localhost:3002/register -X POST -H 'Content-Type: application/json' \
-d '{"client_name":"manual","application_type":"native","redirect_uris":["http://localhost:8899/callback"]}'
# authorize (auto-approves; paste into a browser and read the Location header)
curl -i "http://localhost:3002/authorize?response_type=code&client_id=<client_id>&redirect_uri=http%3A%2F%2Flocalhost%3A8899%2Fcallback&scope=mcp+tools%3Aexecute&code_challenge_method=S256"
# token
curl -i http://localhost:3002/token -X POST -H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code&code=<code>&redirect_uri=http://localhost:8899/callback&client_id=<client_id>&code_verifier=<verifier>'
# authenticated MCP call
curl -i http://localhost:3001/mcp -X POST -H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hi"}}}'The default preconfigured client is a public client (preconfigured-client, no
secret, PKCE required, token_endpoint_auth_method: none). DCR-registered clients
behave the same unless application_type: web is sent, in which case they are issued
a client_secret and must authenticate at the token endpoint.
The flow
Client MCP Server (3001) Auth Server (3002)
|--- POST /mcp (no token) ---------->|
|<-- 401 + WWW-Authenticate ---------|
| (resource_metadata URI, |
| or client derives it) |
|--- GET /.well-known/oauth-protected-resource/mcp -->|
|<-- { authorization_servers: [...] }----------------|
|--- GET /.well-known/oauth-authorization-server -->| (3002)
|<-- { registration_endpoint?, authorize, token } --|
|--- POST /register (DCR) ---------->|
|<-- { client_id, client_secret } ---|
| (or registration rejected -> use preconfigured client_id)
|--- GET /authorize (PKCE+resource) ->|
|<-- 302 redirect_uri?code&iss ------|
|--- POST /token -------------------->|
|<-- { access_token } ---------------|
|--- POST /mcp (Bearer token) ------->|
|<-- 200 MCP JSON-RPC result --------|What it deliberately does not do
No real user login / consent UI —
/authorizeauto-approves so the flow is scriptable.No refresh tokens, JWT, revocation, or OIDC userinfo. Scopes are
mcp(any access) andtools:execute(tools/call).No Client ID Metadata Documents (the newer, preferred registration mechanism) — this repo focuses on the DCR vs preconfigured-client question.
Tokens are opaque and stored in memory; restarting the server invalidates them.
Files
server.js— MCP resource server + OAuth authorization server + request/response logger.client.js— demo client that runs the discovery → DCR-or-preconfigured → OAuth → MCP flow.README.md— this file.
Related MCP Connectors
Experimental MCP server for current empirical verification of explicit public HTTPS endpoint claims.
A public MCP honeypot: a fake ops console with fake secrets. Every call is shown on decoy.day.
Minimal streamable HTTP MCP server used for owned-account registry connectivity tests.
Demo banking MCP server: accounts, transactions and payments. OAuth 2.1 with DPoP.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA Model Context Protocol (MCP) server designed for learning and experimentation. It provides a foundational setup for developers to build, run, and debug MCP server implementations using Node.js.-
- FlicenseNot gradedqualityDmaintenanceA deliberately insecure MCP server designed as a pentest lab to demonstrate common vulnerabilities in MCP deployments.-
- FlicenseNot gradedqualityCmaintenanceA minimal local HTTP MCP mock server for development and testing, providing predictable tool responses with OAuth token support and zero dependencies.-
- FlicenseNot gradedqualityBmaintenanceA simple MCP server with OAuth 2.0 authentication for testing OAuth support in mcp-cli.-