Skip to content

Prefer CIMD over DCR when the proxy acts as OAuth client to remote MCP servers #4826

@jhrozek

Description

@jhrozek

As a user running thv proxy to connect to a remote MCP server that requires authentication, I want the proxy to use CIMD when the remote authorization server supports it, so the proxy follows the MCP spec's preferred client registration mechanism and doesn't depend on the remote AS supporting DCR.

Background

When thv proxy connects to a remote MCP server, the proxy itself is the OAuth client. At startup, it discovers auth requirements from the remote server (RFC 9728), performs DCR with the remote AS, obtains tokens, and injects Authorization: Bearer headers on all proxied requests. VS Code / Claude Code never interacts with the remote AS -- it only talks to the local proxy.

Today the proxy always uses DCR (RFC 7591). The MCP spec prefers CIMD over DCR (draft-ietf-oauth-client-id-metadata-document). Some future AS implementations may support CIMD but not DCR.

How CIMD works for the proxy

The proxy uses a static, publicly-hosted metadata document URL as its client_id. The remote AS fetches the document from that URL to learn about the proxy. The proxy does not serve the document itself -- it's a static JSON file hosted by the ToolHive project, following the same pattern VS Code uses (productService.authClientIdMetadataUrl in product.json).

The redirect_uris in the document point to loopback addresses for the local browser-based auth flow. This sidesteps the localhost reachability problem entirely: the AS only needs to reach the static metadata URL (public), not the proxy's callback server (localhost).

Acceptance criteria

  • After discovering a remote AS, the proxy checks for client_id_metadata_document_supported: true in the AS metadata
  • When CIMD is supported, the proxy uses the built-in metadata document URL as client_id in authorize and token requests (no /oauth/register call)
  • When CIMD is not supported, falls back to DCR (current behavior, no regression)
  • If the remote AS rejects the CIMD client_id, falls back to DCR gracefully
  • Registration priority: stored credentials > CIMD > DCR

Technical design

Detection

Client metadata document (static, project-hosted)

  • Hosted by the ToolHive project at a stable public HTTPS URL (e.g., https://toolhive.dev/.well-known/oauth-client/thv-proxy)
  • Built-in as a default in the proxy binary -- no user configuration required (same pattern as VS Code's authClientIdMetadataUrl in product.json)
  • Document contents:
    {
      "client_id": "https://toolhive.dev/.well-known/oauth-client/thv-proxy",
      "client_name": "ToolHive Proxy",
      "redirect_uris": [
        "http://127.0.0.1/callback",
        "http://127.0.0.1:33419/callback"
      ],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "token_endpoint_auth_method": "none"
    }
  • Two loopback redirect URIs following VS Code's pattern (src/vs/base/common/oauth.ts:869-878): one without port (RFC 8252 Section 7.3 port-agnostic matching) and one with a fixed default port (for servers that do exact matching despite the spec)

Loopback callback server

  • Try binding to a fixed default port first (e.g., 33419)
  • Fall back to OS-assigned port 0 if the default is busy (same approach as VS Code at loopbackServer.ts:152)
  • This keeps the redirect URI stable across runs when possible

Auth flow changes in pkg/auth/remote/handler.go

  • After AS discovery, before attempting DCR, check CIMD viability (AS supports it + metadata URL available)
  • When using CIMD: pass the metadata document URL as client_id in authorize and token requests. Skip /oauth/register.
  • When falling back: call /oauth/register as today
  • Matches the priority chain VS Code uses (mainThreadAuthentication.ts:160-170): stored > CIMD > DCR > manual

Configuration

  • Default metadata document URL baked into the binary (overridable via config for forks or custom deployments)
  • No user configuration needed for the common case

Key files

Area Files
Metadata type (shared with #4825) pkg/oauth/discovery.go
Discovery / AS detection pkg/auth/discovery/discovery.go
Remote auth flow pkg/auth/remote/handler.go
OAuth flow orchestration pkg/auth/discovery/discovery.go (PerformOAuthFlow, handleDynamicRegistration)

Prior art

VS Code's implementation (src/vs/workbench/api/browser/mainThreadAuthentication.ts:160-170, src/vs/base/common/oauth.ts:869-878) confirms the pattern:

  • Static metadata document URL baked into the product config
  • Dual loopback redirect URIs (port-less + fixed-port) for compatibility
  • Fixed default callback port with dynamic fallback
  • CIMD preferred over DCR when the AS advertises support

Metadata

Metadata

Assignees

Labels

authenhancementNew feature or requestgoPull requests that update go codeproxy
No fields configured for Story 🗺️.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions