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
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
As a user running
thv proxyto 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 proxyconnects 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 injectsAuthorization: Bearerheaders 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.authClientIdMetadataUrlinproduct.json).The
redirect_urisin 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
client_id_metadata_document_supported: truein the AS metadataclient_idin authorize and token requests (no/oauth/registercall)client_id, falls back to DCR gracefullyTechnical design
Detection
client_id_metadata_document_supportedfrom the remote AS metadata after RFC 8414/OIDC discoveryClientIDMetadataDocumentSupportedfield onAuthorizationServerMetadataadded in Support Client ID Metadata Document (CIMD) in the embedded authorization server #4825Client metadata document (static, project-hosted)
https://toolhive.dev/.well-known/oauth-client/thv-proxy)authClientIdMetadataUrlinproduct.json){ "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" }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
loopbackServer.ts:152)Auth flow changes in
pkg/auth/remote/handler.goclient_idin authorize and token requests. Skip/oauth/register./oauth/registeras todaymainThreadAuthentication.ts:160-170): stored > CIMD > DCR > manualConfiguration
Key files
pkg/oauth/discovery.gopkg/auth/discovery/discovery.gopkg/auth/remote/handler.gopkg/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: