gangadharrr

Local OAuth + MCP Test Server

Community gangadharrr
Updated

Local OAuth + MCP Test Server

A tiny local server that reproduces GitHub's real OAuth behavior — includingits quirks — so you can test your AI Studio OAuth provider against somethingyou fully control, instead of debugging blind against github.com. It alsosupports refresh tokens with rotation/reuse-detection, and can optionally actas a fully spec-compliant auto-discovery provider and/or Dynamic ClientRegistration (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 notreturn an access token." The most likely cause: GitHub's token endpointreturns application/x-www-form-urlencoded by default and only returns JSONif the request sends Accept: application/json. This server reproduces thatexact behavior on purpose, plus GitHub's other real quirk — no/.well-known/oauth-authorization-server metadata (a documented, open GitHubbug, see github/github-mcp-server#921) — so full OIDC auto-discovery cannever 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 failhere 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 thewhole 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 on json → your client never sends Accept: application/json, so it never gets JSON back. Fix your client to send that header.
  • Fails on form, fails on json, but works on always_json → confirms the theory precisely: your client's problem is entirely about the response Content-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 andrefresh_token_expires_in alongside the usual fields. Full test sequence:

  1. Exchange code for token pair → get access_token + refresh_token.
  2. Call /mcp with the access token → 200.
  3. Wait past TOKEN_TTL_SECONDS, call /mcp again → 401 withWWW-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.
  4. POST /oauth/token with grant_type=refresh_token → new access tokenand a new refresh token (rotation is on by default).
  5. New access token works → 200.
  6. Reuse the old, already-rotated-out refresh token → server detects thisas reuse, returns refresh_token_reused_revoking_family, and revokes it.This is the case that catches real bugs: if your client ever fires tworefresh calls concurrently off one 401 (a common race condition), this isexactly the error you'd hit — your client needs to fall back to fullre-auth here, not retry the refresh in a loop.
  7. The newest refresh token still works fine afterward, confirming familyrevocation 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'tdiscard its stored refresh token after a refresh call, expecting a new onethat never comes.
  • Let REFRESH_TTL_SECONDS expire too, then try to refresh — confirm yourclient falls back to a full re-auth redirect instead of looping.
  • Fire two /mcp calls at once right as the token expires, and see whetheryour client's refresh logic races and burns the refresh token twice (thisis 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) wheregithub.com/login/oauth never implemented RFC 8414 metadata — which is whyGitHub always requires Manual Configuration, never Auto-Discovery.

To test your client's Auto-Discovery (OIDC) code path against a providerthat actually supports it correctly, flip this server into spec-compliantmode 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 themfrom http://localhost:4587/.well-known/oauth-authorization-server (and/.well-known/openid-configuration is also served for clients that checkthere instead).

If your AI Studio connection succeeds against this mode, that confirms yourclient's discovery code is correct — proving the GitHub failures areGitHub's bug, not something to keep chasing on your end. Run once withENABLE_DISCOVERY unset (or false) to see the matching 404, forside-by-side comparison.

Testing Dynamic Client Registration (RFC 7591)

Real github.com has no self-registration endpoint at all — GitHub OAuthApps and GitHub Apps must be created by hand in the web UI. This is exactlywhy MCP clients that expect to self-register (as recommended by the MCPAuthorization spec) can't do so against GitHub. To test that code pathagainst 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 discoverit automatically instead of needing it configured manually. Registrationalso works standalone (ENABLE_DCR=true with discovery off) if your clientlets 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, exceptredirect_uri is validated against whatever was registered — authorizingwith a redirect_uri that wasn't in the original redirect_uris array getsa 400.

To register a public client (no client secret, e.g. a PKCE-only mobile/SPAclient), pass "token_endpoint_auth_method": "none" — note that this serverdoesn't actually enforce PKCE on the authorize/token exchange (samepre-existing gap as the advertised-but-unverified code_challenge_methods_supported),so don't rely on it to test PKCE correctness itself.

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=true to serve it instead (see "Testing auto-discovery"above).
  • POST /register — 404s by default, matching the fact that realgithub.com has no Dynamic Client Registration endpoint at all. SetENABLE_DCR=true to serve it instead (see "Testing Dynamic ClientRegistration" above).
  • No real login/consent screen — authorize always auto-approves, since thepoint here is testing your token-exchange and resource-access code, notbuilding a full identity provider.

MCP Server · Populars

MCP Server · New

    adelinamart

    RoBrain

    Shared memory across your team and your AI agents — with judgment about what's worth keeping.

    Community adelinamart
    Lyellr88

    MARM: Local-First Persistent Multi-Agent Memory Layer for MCP Clients v2.20.0

    Self-hosted, local-first MCP memory server for AI agents. Sub-20ms recall at 10k memories, hybrid semantic + exact-syntax search (RAG), code & concept knowledge graphs (158 languages). All local in SQLite: no cloud, no vector DB, no API keys. Works with Claude Code, Codex, Cursor, Gemini & any MCP client.

    Community Lyellr88
    Bumblebiber

    hmem — Humanlike Memory for AI Agents

    Persistent memory and agent lifecycle for Claude Code — because sessions shouldn't start from zero.

    Community Bumblebiber
    firish

    Claude Code for Visual Studio

    Bring Claude Code to Visual Studio 2026: A native diff with accept/reject, a live debugger Claude can drive autonomously, Roslyn code navigation, and Test Explorer integration. The IDE half of Claude Code's integration protocol. Community-built, unofficial.

    Community firish
    uiNerd16

    @aicanvas/mcp

    Open-source React and Tailwind component marketplace. Install via the shadcn CLI or your AI editor over MCP, with reproduction prompts for Claude Code, Lovable, and V0.

    Community uiNerd16