Local OAuth + Test Server
БесплатноНе проверенA local test server that reproduces GitHub's OAuth behavior for debugging MCP OAuth provider configurations, supporting refresh tokens and auto-discovery.
Описание
A local test server that reproduces GitHub's OAuth behavior for debugging MCP OAuth provider configurations, supporting refresh tokens and auto-discovery.
README
A tiny local server that reproduces GitHub's real OAuth behavior — including
its quirks — so you can test your AI Studio OAuth provider against something
you fully control, instead of debugging blind against github.com. It also
supports refresh tokens with rotation/reuse-detection, and can optionally act
as a fully spec-compliant auto-discovery provider and/or Dynamic Client
Registration (RFC 7591) provider so you can test those code paths too.
Why this exists
Your AI Studio flow against real GitHub failed with "OAuth provider did not
return an access token." The most likely cause: GitHub's token endpoint
returns application/x-www-form-urlencoded by default and only returns JSON
if the request sends Accept: application/json. This server reproduces that
exact behavior on purpose, plus GitHub's other real quirk — no
/.well-known/oauth-authorization-server metadata (a documented, open GitHub
bug, see github/github-mcp-server#921) — so full OIDC auto-discovery can
never succeed here either, same as with the real thing.
Setup
npm install
CLIENT_ID=test-client CLIENT_SECRET=test-secret node server.js
Server starts on http://localhost:4587 by default (override with PORT).
All environment variables
| Variable | Default | Purpose |
|---|---|---|
PORT |
4587 |
Server port |
CLIENT_ID / CLIENT_SECRET |
test-client / test-secret |
Must match what you configure in AI Studio |
TOKEN_RESPONSE_MODE |
form |
form | json | always_json — controls token endpoint response format |
ISSUE_REFRESH_TOKENS |
false |
Set true to get a refresh_token back alongside access_token |
TOKEN_TTL_SECONDS |
3600 |
Access token lifetime — set low (e.g. 5) to test expiry fast |
REFRESH_TTL_SECONDS |
86400 |
Refresh token lifetime |
ROTATE_REFRESH_TOKENS |
true |
Each refresh issues a new refresh token and invalidates the old one; set false for "static" refresh tokens |
ENABLE_DISCOVERY |
false |
Set true to serve real RFC 8414 / OIDC discovery metadata instead of 404ing like GitHub does |
ENABLE_DCR |
false |
Set true to serve POST /register (RFC 7591 Dynamic Client Registration), which real GitHub has no equivalent of |
Point your AI Studio provider config at it
- Endpoint Configuration: Manual Configuration (auto-discovery will fail here on purpose, just like with real GitHub)
- Authorization URL:
http://localhost:4587/oauth/authorize - Token URL:
http://localhost:4587/oauth/token - MCP Server URL:
http://localhost:4587/mcp - Client ID:
test-client - Client Secret:
test-secret - Scopes: anything — they're not validated, just echoed back
The authorize endpoint auto-approves (no login screen) so you can drive the whole flow without a browser if you want, or through your real UI.
Testing the Accept-header theory
Control the token endpoint's response format with an env var:
# Default: GitHub's real behavior — form-urlencoded unless Accept: application/json is sent
TOKEN_RESPONSE_MODE=form node server.js
# Only returns JSON when Accept: application/json is actually sent (still GitHub-accurate)
TOKEN_RESPONSE_MODE=json node server.js
# Always returns JSON regardless of Accept header — use this to confirm the fix
# in isolation: if your AI Studio flow suddenly works with this mode, the bug
# really is that your client doesn't set the Accept header.
TOKEN_RESPONSE_MODE=always_json node server.js
Run your AI Studio "Test Connection" against each mode:
- Fails on
form, fails onjson→ your client never sendsAccept: application/json, so it never gets JSON back. Fix your client to send that header. - Fails on
form, fails onjson, but works onalways_json→ confirms the theory precisely: your client's problem is entirely about the responseContent-Type/format, not the token content itself. - Fails on all three → the bug isn't about response format at all (check redirect_uri matching, client_secret, or how your client parses the response body).
Testing refresh token logic
ISSUE_REFRESH_TOKENS=true TOKEN_TTL_SECONDS=5 REFRESH_TTL_SECONDS=60 \
CLIENT_ID=test-client CLIENT_SECRET=test-secret node server.js
With this on, the authorization_code exchange returns refresh_token and
refresh_token_expires_in alongside the usual fields. Full test sequence:
- Exchange code for token pair → get
access_token+refresh_token. - Call
/mcpwith the access token →200. - Wait past
TOKEN_TTL_SECONDS, call/mcpagain →401withWWW-Authenticate: ... error="invalid_token", error_description="The access token expired". This is the exact signal your client's refresh logic should key off of — not just any 401. POST /oauth/tokenwithgrant_type=refresh_token→ new access token and a new refresh token (rotation is on by default).- New access token works →
200. - Reuse the old, already-rotated-out refresh token → server detects this
as reuse, returns
refresh_token_reused_revoking_family, and revokes it. This is the case that catches real bugs: if your client ever fires two refresh calls concurrently off one 401 (a common race condition), this is exactly the error you'd hit — your client needs to fall back to full re-auth here, not retry the refresh in a loop. - The newest refresh token still works fine afterward, confirming family revocation only killed the reused one, not the whole chain going forward.
Other scenarios worth testing deliberately:
ROTATE_REFRESH_TOKENS=false— simulates providers (like classic GitHub) that keep one static refresh token forever. Confirm your client doesn't discard its stored refresh token after a refresh call, expecting a new one that never comes.- Let
REFRESH_TTL_SECONDSexpire too, then try to refresh — confirm your client falls back to a full re-auth redirect instead of looping. - Fire two
/mcpcalls at once right as the token expires, and see whether your client's refresh logic races and burns the refresh token twice (this is exactly what step 6 above is designed to catch).
Testing auto-discovery
By default this server 404s on /.well-known/oauth-authorization-server,
mirroring the real, open GitHub bug (github/github-mcp-server#921) where
github.com/login/oauth never implemented RFC 8414 metadata — which is why
GitHub always requires Manual Configuration, never Auto-Discovery.
To test your client's Auto-Discovery (OIDC) code path against a provider that actually supports it correctly, flip this server into spec-compliant mode instead:
ENABLE_DISCOVERY=true CLIENT_ID=test-client CLIENT_SECRET=test-secret node server.js
Then in AI Studio, set:
- Endpoint Configuration: Auto-Discovery (OIDC)
- Issuer URL:
http://localhost:4587
No separate Authorization/Token URL fields needed — discovery resolves them
from http://localhost:4587/.well-known/oauth-authorization-server (and
/.well-known/openid-configuration is also served for clients that check
there instead).
If your AI Studio connection succeeds against this mode, that confirms your
client's discovery code is correct — proving the GitHub failures are
GitHub's bug, not something to keep chasing on your end. Run once with
ENABLE_DISCOVERY unset (or false) to see the matching 404, for
side-by-side comparison.
Testing Dynamic Client Registration (RFC 7591)
Real github.com has no self-registration endpoint at all — GitHub OAuth
Apps and GitHub Apps must be created by hand in the web UI. This is exactly
why MCP clients that expect to self-register (as recommended by the MCP
Authorization spec) can't do so against GitHub. To test that code path
against a provider that actually supports it:
ENABLE_DCR=true ENABLE_DISCOVERY=true node server.js
With ENABLE_DISCOVERY=true too, registration_endpoint is added to the
/.well-known/oauth-authorization-server metadata so a client can discover
it automatically instead of needing it configured manually. Registration
also works standalone (ENABLE_DCR=true with discovery off) if your client
lets you point it at /register directly.
# Register a new client
curl -X POST http://localhost:4587/register \
-H "Content-Type: application/json" \
-d '{"redirect_uris": ["http://localhost:9999/callback"], "client_name": "My Test Client"}'
# -> 201 with { client_id, client_secret, redirect_uris, ... }
The returned client_id/client_secret work with /oauth/authorize and
/oauth/token exactly like the static CLIENT_ID/CLIENT_SECRET do, except
redirect_uri is validated against whatever was registered — authorizing
with a redirect_uri that wasn't in the original redirect_uris array gets
a 400.
To register a public client (no client secret, e.g. a PKCE-only mobile/SPA
client), pass "token_endpoint_auth_method": "none" — note that this server
doesn't actually enforce PKCE on the authorize/token exchange (same
pre-existing gap as the advertised-but-unverified code_challenge_methods_supported),
so don't rely on it to test PKCE correctness itself.
Registered clients survive server restarts
Registrations are persisted to .dcr-clients.json (gitignored, since it
contains client secrets) and reloaded on startup, so restarting the server
while iterating doesn't invalidate client_ids a real client may have
already cached. If you see invalid_client: unknown client_id ... for an
id that looks like it came from /register (dcr_...), it means either:
- that id was registered before this persistence feature existed / before the store file existed, or
- your OAuth client is replaying a
client_idit cached from a different server instance (a stale local dev server, an old ngrok session, etc.)
Either way, the fix is to re-register: call POST /register again and use
the fresh client_id/client_secret (or get your client to forget its
cached registration and do DCR again). Set DCR_STORE_PATH=/dev/null (or
delete .dcr-clients.json) to start with a clean slate.
Manual curl walkthrough
# 1. Get an auth code (simulates user clicking "Authorize")
curl -i "http://localhost:4587/oauth/authorize?client_id=test-client&redirect_uri=http://localhost:9999/callback&state=xyz"
# -> 302 redirect with ?code=XXXX&state=xyz in the Location header
# 2. Exchange the code for a token, GitHub-style (form-encoded response)
curl -X POST http://localhost:4587/oauth/token \
-d "client_id=test-client&client_secret=test-secret&code=XXXX&grant_type=authorization_code"
# 3. Same, but forcing JSON like a spec-compliant client would
curl -X POST http://localhost:4587/oauth/token \
-H "Accept: application/json" \
-d "client_id=test-client&client_secret=test-secret&code=XXXX&grant_type=authorization_code"
# 4. Call the protected MCP resource
curl -H "Authorization: Bearer <access_token from step 2 or 3>" http://localhost:4587/mcp
What's NOT implemented by default
/.well-known/oauth-authorization-server— 404s by default, matching realgithub.com/login/oauth's missing RFC 8414 metadata. SetENABLE_DISCOVERY=trueto serve it instead (see "Testing auto-discovery" above).POST /register— 404s by default, matching the fact that realgithub.comhas no Dynamic Client Registration endpoint at all. SetENABLE_DCR=trueto serve it instead (see "Testing Dynamic Client Registration" above).- No real login/consent screen — authorize always auto-approves, since the point here is testing your token-exchange and resource-access code, not building a full identity provider.
Установка Local OAuth + Test Server
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/gangadharrr/Test-MCPFAQ
Local OAuth + Test Server MCP бесплатный?
Да, Local OAuth + Test Server MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Local OAuth + Test Server?
Нет, Local OAuth + Test Server работает без API-ключей и переменных окружения.
Local OAuth + Test Server — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Local OAuth + Test Server в Claude Desktop, Claude Code или Cursor?
Открой Local OAuth + Test Server на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectCompare Local OAuth + Test Server with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
