Skip to content

RFC: Client ID Metadata Document (CIMD) support#71

Merged
amirejaz merged 8 commits into
mainfrom
cimd-support
May 1, 2026
Merged

RFC: Client ID Metadata Document (CIMD) support#71
amirejaz merged 8 commits into
mainfrom
cimd-support

Conversation

@amirejaz
Copy link
Copy Markdown
Contributor

Summary

Proposes CIMD (Client ID Metadata Document) support across two phases:

  • Phase 1thv run acts as a CIMD-aware OAuth client. When a remote authorization server advertises client_id_metadata_document_supported: true, thv run presents its hosted metadata URL (https://toolhive.dev/oauth/client-metadata.json) as the client_id instead of performing a DCR round-trip.
  • Phase 2 — The embedded authorization server accepts CIMD. MCP clients (VS Code, Claude Code) can present an HTTPS URL as client_id and complete the full authorization flow without calling /oauth/register.

Motivation

The MCP specification (2025-11-25) defines CIMD as the preferred client registration mechanism: pre-registered credentials → CIMD → DCR → user prompt. ToolHive currently jumps straight to DCR. This is both a spec-alignment issue and a friction-reduction issue — DCR requires a registration round-trip that CIMD eliminates entirely.

Related issues


Generated with Claude Code

Proposes CIMD support on both sides of the ToolHive proxy: as an
OAuth client preferring CIMD over DCR when connecting to remote MCP
servers (thv run), and as an authorization server accepting HTTPS
URLs as client_id from MCP clients (embedded AS).

Closes stacklok/toolhive#2728
Related: stacklok/toolhive#4825, stacklok/toolhive#4826

Signed-off-by: amirejaz <amir@stacklok.com>
Signed-off-by: amirejaz <amir@stacklok.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces RFC-0071 proposing support for Client ID Metadata Documents (CIMD) in ToolHive, covering both (1) thv run acting as a CIMD-aware OAuth client when talking to remote authorization servers, and (2) the embedded authorization server accepting CIMD client_id URLs from MCP clients to avoid Dynamic Client Registration (DCR) round-trips.

Changes:

  • Adds a new RFC detailing a two-phase rollout plan for CIMD support (proxy client first, embedded AS second).
  • Defines the intended discovery metadata flag (client_id_metadata_document_supported), caching/SSRF protections, and fallback behavior to DCR.
  • Specifies the hosted ToolHive client metadata document and operational requirements for serving it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md Outdated
amirejaz and others added 4 commits April 24, 2026 11:51
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Fix spelling: defence → defense (two occurrences)
- Align config naming: CIMDEnabled → authServer.cimd.enabled
- Resolve redirect/non-200 contradiction in Input Validation
- Add IPv6 ULA (fc00::/7) to SSRF blocked ranges; frame as globally
  routable IPs only for future-proofing

Signed-off-by: amirejaz <amir@stacklok.com>
Remove inaccurate port-agnostic http://localhost/callback entry.
The actual default callback port is 8666, defined as DefaultCallbackPort
in pkg/auth/remote/config.go and configurable via --remote-auth-callback-port.

Signed-off-by: amirejaz <amir@stacklok.com>
Blocker:
- Explicitly list ClientIDMetadataDocumentSupported field addition in
  Phase 1 task list; note both RFC 8414 and OIDC discovery must be parsed

High severity:
- Add PKCE S256 requirement for CIMD flows (mandatory per MCP 2025-11-25)
- Expand SSRF protection: use net.IP.IsGlobalUnicast() as allowlist
  baseline; add 100.64.0.0/10, IPv4-mapped IPv6, documentation ranges
- Add DisableKeepAlives and dual-stack DialContext requirements to SSRF
  transport to prevent keep-alive and Happy Eyeballs bypasses
- Change self-referential binding from strict string comparison to RFC
  3986 canonical normalization (lowercase host/scheme, strip default ports)
- Expand DCR fallback trigger: invalid_client, unauthorized_client,
  invalid_request, raw HTTP 400/401 at authorization stage only
- Add cache floor (60s), ceiling (24h), and no-store handling
- Document refresh token behavior: document not re-fetched on refresh
- Document air-gapped limitation for Phase 2 self-fetch scenario
- Add Content-Type validation before JSON parsing

Low/informational:
- Note SHOULD vs MUST normative strength of priority chain in Summary
- Add LRU cache flooding threat to threat model
- Add compromised hosting as accepted residual risk in threat model
- Note IETF draft-01 version in References

Signed-off-by: amirejaz <amir@stacklok.com>
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-XXXX-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-XXXX-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-XXXX-cimd-support.md Outdated
Comment thread rfcs/THV-XXXX-cimd-support.md Outdated
- Move cimd.go to pkg/oauth (protocol-level, not auth-specific)
- Replace new ssrf.go with enhancement to existing pkg/networking
  HttpClientBuilder; CIMD fetches reuse existing SSRF infrastructure
  with missing ranges added (100.64.0.0/10, IPv4-mapped, doc ranges)
- Clarify CIMD carries no credentials; rejection = AS compatibility
  issue, not expiry
- Note logo_uri is a URL reference, not inline data; 10 KB cap is for
  the JSON document only
- Note custom metadata URL override as a follow-up enhancement
- Clarify opt-in applies to Phase 2 only (outbound fetch gating);
  Phase 1 is automatic with no config flag

Signed-off-by: amirejaz <amir@stacklok.com>
Copy link
Copy Markdown
Contributor

@jhrozek jhrozek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second-wave review based on a deeper cross-check against the toolhive code. Most of these are concerns about the Phase 2 decorator design and a few smaller nits. Skipping straight to inline comments below.

Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md Outdated
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
Comment thread rfcs/THV-0071-cimd-support.md
- Revert to strict string comparison for self-referential binding
  (spec-mandated; full IDN normalization required to be safe, adds
  complexity for a case well-implemented clients never produce)
- Fix PerformOAuthFlow sketch: set config.ClientID before the DCR gate
  (shouldDynamicallyRegisterClient checks ClientID == empty)
- Narrow DCR fallback: remove invalid_request (request-shape error,
  not identity rejection; keeping it could mask errors or enable
  forced downgrade to DCR)
- Expand decorator: embed full storage.Storage (~15 methods), add
  singleflight.Group to deduplicate concurrent fetches on anonymous
  /authorize, expose Unwrap() for legacy RedisStorage type assertion
- Document second GetClient call site in callback.go; wired via
  storage embedding, no extra work needed
- Document RedisStorage type assertion landmine in server_impl.go;
  must run migration before wrapping or use Unwrap()
- Expand Phase 2 implementation plan with all files touched by
  config threading (RunConfig -> Config -> handler -> decorator)
- Fix AuthorizationServerMetadata field location: pkg/oauth/discovery.go
  (shared package), not the authserver handler
- Fix SSRF threat row to match expanded pkg/networking IP ranges
- Narrow Phase 1 test description to match updated fallback conditions
- Update Last Updated date

Signed-off-by: amirejaz <amir@stacklok.com>
Copy link
Copy Markdown
Contributor

@jhrozek jhrozek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work on the revisions — thanks for the thorough turnaround.

@jhrozek
Copy link
Copy Markdown
Contributor

jhrozek commented Apr 30, 2026

@amirejaz wanna merge this? :-)

@amirejaz amirejaz merged commit 2a1664a into main May 1, 2026
1 check 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.

3 participants