Support Client ID Metadata Document (CIMD) in the embedded authorization server
@amirejaz is already working on this.
Since May 19, 2026.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
As an MCP client (VS Code, Claude Code, or any spec-compliant client) connecting to a ToolHive-managed MCP server locally, I want to use an HTTPS URL as my client_id so the authorization server fetches my client metadata automatically, without requiring a separate DCR registration step.
Background
The MCP 2025-11-25 spec defines client registration priority as: pre-registered credentials > CIMD (preferred) > DCR > user prompt. ToolHive's embedded AS currently supports only DCR (RFC 7591). CIMD (draft-ietf-oauth-client-id-metadata-document) is the spec-preferred mechanism but neither ToolHive, Ory Hydra, nor Obot implement it today.
In the local flow, the MCP client (VS Code, Claude Code) is the OAuth client. The thv proxy is the resource server and hosts the embedded AS. The proxy itself does not need CIMD awareness for this flow.
VS Code already implements CIMD client-side support (mainThreadAuthentication.ts:160-170): when the AS advertises client_id_metadata_document_supported: true, VS Code uses its product-configured metadata URL as client_id instead of performing DCR.
Acceptance criteria
- MCP clients can pass an HTTPS URL as
client_idin/oauth/authorizeand/oauth/tokenand complete the full authorization code + PKCE flow without calling/oauth/register - Discovery documents (
/.well-known/oauth-authorization-server,/.well-known/openid-configuration) advertise"client_id_metadata_document_supported": truewhen CIMD is enabled - Existing DCR continues to work alongside CIMD (no regression)
- CIMD is opt-in via configuration (disabled by default)
Technical design
URL detection and coexistence with DCR
- A
client_idis treated as CIMD when it starts withhttps:// - Inner storage is checked first: if
GetClientfinds an existing client (DCR or static), use it. CIMD fetch only onErrNotFound. This prevents collisions with existing client IDs.
Storage decorator pattern
Fosite resolves clients via Storage.GetClient(ctx, id string) in both NewAuthorizeRequest (authorize endpoint) and NewAccessRequest (token endpoint). A decorator wrapping storage.Storage that intercepts GetClient handles both flows transparently.
- All other
Storageinterface methods delegate unchanged to the inner implementation. - Wire the decorator at
pkg/authserver/server/provider.gowherefosite.NewOAuth2Provideris called. - Fosite has no built-in CIMD support. The
ClientAuthenticationStrategyhook only covers the token endpoint, not the authorize endpoint, so the Storage decorator is the correct integration point.
Metadata document fetch and validation
- Document must be served over HTTPS with
Content-Type: application/json - Response body capped at 10 KB, fetch timeout 5 seconds
client_idin the document must exactly match the fetch URLredirect_urisvalidated via existingoauth.ValidateRedirectURI(RFC 8252 loopback or HTTPS)grant_typeslimited toauthorization_code+ optionallyrefresh_tokenresponse_typeslimited tocodetoken_endpoint_auth_methodmust benoneor absentscope, if present, validated againstConfig.ScopesSupported; if absent, defaults toregistration.DefaultScopesclient_namecapped at 256 characters- Unknown fields ignored (forward compatibility)
SSRF protection
- Reject fetches to private/internal IP ranges (10/8, 172.16/12, 192.168/16, 127/8, ::1, fe80::/10, 169.254/16)
- Custom
http.TransportwithDialContexthook: resolve hostname, validate resolved IP, then connect (DNS rebinding defense) - HTTP redirects limited to 1 hop; redirect target re-validated against same blocklist
- Optional configurable domain allowlist for locked-down deployments
- Threat model note: embedded AS runs on the user's machine, so "internal" means the user's LAN. Risk is lower than cloud-deployed AS but still worth protecting (malicious MCP server config could probe LAN services).
Client construction
- Construct
fosite.DefaultClientwrapped inLoopbackClient(public client, same as DCR) Audienceset fromConfig.AllowedAudiences(server policy, not from metadata document -- same as DCR)client_idclaim in issued JWTs will be the HTTPS URL string (format change from UUID-style DCR IDs, no code change needed)
Caching
- Cache lives inside the Storage decorator as an internal detail (no new
Storageinterface methods) - Keyed by
client_idURL, stores constructedfosite.Client - TTL from HTTP
Cache-Control: max-age, clamped to configurable max (default 1h) and min (default 5m) - Max entries configurable (default 100), LRU eviction
- Redis-backed deployments: serialize to Redis with TTL for cross-replica sharing
- Forced refresh on redirect_uri mismatch deferred to follow-up (first cut uses TTL-only expiry)
Configuration
- New
CIMDEnabled boolfield inpkg/authserver/config.go - New
ClientIDMetadataDocumentSupported boolfield inAuthorizationServerMetadata(pkg/oauth/discovery.go) with tagjson:"client_id_metadata_document_supported,omitempty"
Key files
| Area | Files |
|---|---|
| Config | pkg/authserver/config.go |
| Discovery metadata type | pkg/oauth/discovery.go |
| Discovery handler | pkg/authserver/server/handlers/discovery.go |
| Storage decorator (new) | pkg/authserver/storage/cimd.go |
| Validation + SSRF (new) | pkg/authserver/server/registration/cimd.go, ssrf.go |
| Wiring | pkg/authserver/server/provider.go |
| Redis cache extension | pkg/authserver/storage/redis.go |
Out of scope
- Consent screen displaying CIMD client hostname (no consent UI exists today; deferred until one is built)
- Proxy-side CIMD when connecting to remote MCP servers (separate issue)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.