Skip to content

fix(oauth): discovery overrides, offline_access and discovery cache (Spec 113-b) - #1499

Merged
Dumbris merged 8 commits into
mainfrom
113-b-oauth-discovery-overrides
Oct 6, 2026
Merged

Dumbris merged 8 commits into
mainfrom
113-b-oauth-discovery-overrides

Conversation

@Dumbris

@Dumbris Dumbris commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Problem

Slice 113-b of Spec 113 (gaps G3 and G5 from the Anthropic MCP-proxy talk audit):

  • G3: mcpproxy never asked for offline_access, never warned when the authorization server cannot issue refresh tokens, and had no way to point a server at an authorization server whose metadata is non-standard or advertises an unusable endpoint.
  • G5: every createOAuthConfigInternal call re-fetched the same PRM and authorization server metadata, and extra_params injection used a /token / /authorize path heuristic.

What changed

  • New optional per-server oauth.authorization_endpoint, oauth.token_endpoint, oauth.registration_endpoint, oauth.auth_server_metadata_url (https, or http on a loopback host; no query, fragment, userinfo or whitespace). Validated at write time only (OAuthConfig.Validate, Config.ValidateDetailed); an invalid value already on disk is dropped at load with a field-name-only warning, so boot never fails.
  • Endpoint overrides are applied by rewriting the authorization server metadata inside OAuthTransportWrapper (mcp-go has no endpoint config). With any endpoint override mcpproxy always passes mcp-go a metadata URL (override, else discovered, else RFC 8414 URL from the override origin), and when that metadata is unreachable and authorization + token overrides are set the wrapper serves a synthesized document, so mcp-go never falls back to /authorize, /token, /register.
  • extra_params injection and 201 to 200 normalization match on the effective (overridden or discovered) token endpoint URL; the path heuristic remains only when no endpoint is known.
  • offline_access is appended only when the scopes were taken from PRM scopes_supported and the AS metadata scopes_supported contains it. Explicit oauth.scopes and AS-derived scopes are untouched.
  • One Warn per server per process when AS grant_types_supported is present (including an empty list) and lacks refresh_token.
  • Discovery cache (internal/oauth/discovery_cache.go): key = hash(server URL, four overrides, kind); 1 h success TTL, 30 s failure TTL, single-flight, LRU 256. Covers the HEAD/POST preflights, PRM fetches, the scope fallbacks and the AS metadata document (shared with the transport wrapper so mcp-go's own metadata GET is served from the cache). A 401 advertising a different resource_metadata URL invalidates the server's entries, including in-flight fetches.
  • OAuth HTTP client redirect policy: refuses https to http on a non-loopback host and any cross-origin redirect of a POST (compared with effective ports).
  • Config-field checklist: OAuthConfigChanged, MergeOAuthConfig/copyOAuthConfig, ServerFieldMaskDecisions rows, contracts OAuth projection + runtime/management plumbing, make swagger (oas/ committed; cmd/generate-types produced no TS diff), docs/configuration.md. Per-server fields, so no MCPPROXY_ env override. Echoed masks of the four fields are reverted on write (UnmaskLiveOAuth), because the generic read view's name rule masks leaves containing "auth"/"token".
  • refresh_manager.go and persistent_token_store.go are untouched (owned by slice a).

Tests run

  • Written failing first, then implemented: config (round trip, merge, change detection, validation table, load-time sanitize, whitespace), storage round trip, discovery cache (TTLs, keys, single-flight, LRU, invalidation, atomic seeds), transport wrapper (rewrite, synthesis, learned token endpoint, heuristic fallback, redirects, log redaction), end-to-end with a real mcp-go OAuthHandler (authorization URL host equals override, exchange + refresh hit the override, advertised /std/* endpoints never hit), createOAuthConfigInternal integration (5 calls = 1 request per discovery URL, separate keys per override set, failure cached briefly, offline_access table, refresh-grant warning, PRM-URL invalidation, unmask of echoed masks).
  • go test -race on internal/config, internal/storage, internal/contracts, internal/management, cmd/generate-types, internal/oauth: pass.
  • go test -race -tags server -skip <CI regex> on runtime, httpapi, upstream, config, oauth, server/tokens: pass. internal/server (same flags, 45m timeout) also passes; a first run with the 20m CI timeout hit the timeout on the loaded shared machine (load average about 9), unrelated to this change.
  • go build ./cmd/mcpproxy and go build -tags server -o /dev/null ./cmd/mcpproxy: pass.
  • golangci-lint v2 with .github/.golangci.yml, bare and --build-tags server: no findings in touched files (the remaining repo-wide findings are pre-existing Go 1.26 deprecation notices, e.g. ecdsa.PublicKey.X).
  • ./scripts/test-api-e2e.sh on isolated ports (18177/18179/18180): the only consistent failure is Audit log: server-edition binary present, which fails identically on origin/main (reproduced on a clean checkout at 18178). Two of the three runs also hit a launcher-test reconnect flake under machine load; the third run was green apart from the audit item.
  • Review: opencode github-copilot/gpt-6.1-sol, 4 rounds (round 1 in three chunks, then three fix-and-re-review rounds). Round 1 found 9 issues, all fixed with tests (in-flight fetch repopulating after invalidation, negative cache bypass, redirected metadata, default-port origin match, POST preflight not recording the PRM URL, transient-PRM-failure fallback pinned for 1 h, empty grant_types_supported, whitespace validation bypass, masked echo on write); rounds 2 and 3 found two more cache-seeding issues, fixed by storing seeds atomically with the primary result; round 4 verdict CLEAN.

Assumptions

  • Overrides are plain per-server config fields with no env var; PATCH merge keeps the base value when the patch value is empty (same as the other scalar OAuth fields).
  • The four override fields are labelled NotSecret in ServerFieldMaskDecisions (typed projection shows them to the UI); the write door still reverts echoed masks from the generic view.
  • internal/upstream/core/connection.go is not edited: the PRM-URL invalidation hook lives in oauth.handleUnauthorizedResponse (resource auto-detection already sees every live 401 resource_metadata), which keeps the slice inside internal/oauth.
  • An authorization-server fallback result found on the MCP origin after a failed PRM step is cached only for the failure TTL, not an hour, so a transient PRM failure cannot pin a wrong AS.
  • Known limitation: after a same-origin 307/308 redirect of a token POST, the learned-endpoint match no longer applies the 201 to 200 normalization to the redirect target.
  • The task text placed the end-to-end override test under internal/upstream/core; it lives in internal/oauth (oauth_overrides_e2e_test.go) because it only needs mcp-go's handler and the config this package builds.

Spec: #1495
Review: opencode github-copilot/gpt-6.1-sol, 4 rounds, clean; follow-ups: after a same-origin 307/308 redirect of a token POST the learned-endpoint match no longer normalizes the 201 response

…g and discovery cache (Spec 113-b)

Add per-server oauth.authorization_endpoint, token_endpoint, registration_endpoint
and auth_server_metadata_url overrides (https or loopback http, write-time
validation only). Endpoint overrides rewrite the authorization server metadata
mcp-go reads, with a synthesized document when it is unreachable.
extra_params injection and 201 normalization now match the effective token
endpoint URL. offline_access is requested only for PRM-derived scopes when the
AS advertises it; a missing refresh_token grant logs one warning per server.
Preflight discovery (PRM, AS metadata, scopes) is cached per server URL and
override set (1h success, 30s failure, LRU 256). The OAuth HTTP client refuses
https->http and cross-origin POST redirects.
Invalidate in-flight discovery fetches, replay cached metadata failures,
follow redirects when loading metadata through the wrapper, compare origins
with effective ports, record the PRM URL in the POST preflight, keep a
PRM-less fallback result only for the failure TTL, warn on an explicitly
empty grant_types_supported, reject whitespace in overrides, and revert
echoed masks of the override fields on write.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9b2c9e1
Status: ✅  Deploy successful!
Preview URL: https://73c12abc.mcpproxy-docs.pages.dev
Branch Preview URL: https://113-b-oauth-discovery-overri.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: 113-b-oauth-discovery-overrides

Available Artifacts

  • archive-darwin-amd64 (31 MB)
  • archive-darwin-arm64 (28 MB)
  • archive-linux-amd64 (19 MB)
  • archive-linux-arm64 (17 MB)
  • archive-windows-amd64 (31 MB)
  • archive-windows-arm64 (27 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (27 MB)
  • installer-dmg-darwin-arm64 (24 MB)
  • smart-mcp-proxymcpproxy-goV0P5X0.dockerbuild (0 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 37500057558 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris enabled auto-merge (squash) October 6, 2026 17:24
@Dumbris
Dumbris merged commit 39a4990 into main Oct 6, 2026
56 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants