Status update (2026-08-28)
DCR is implemented and enabled by default.
mcpgateway/services/dcr_service.py implements RFC 7591 registration, AS metadata discovery, client persistence, update and delete.
DCR_ENABLED and DCR_AUTO_REGISTER_ON_MISSING_CREDENTIALS both default to true (config.py:1008, :1011).
/oauth/authorize/{gateway_id} auto-registers when a gateway has an issuer and no client_id (oauth_router.py:670), persists the credentials and sets auth_type=oauth.
- Registration sends
redirect_uri programmatically, so for DCR-capable providers the operator never copies it.
/oauth/registered-clients covers listing, per-gateway lookup and deletion.
What is missing is UI surfacing: nothing tells the user that Client ID and Client Secret can be left blank for a DCR-capable issuer. That hint is tracked in #6460 and depends on the v1 discovery endpoint in #5717.
The deprecation label is correct and traces to #5692 ([Deprecated-3] DCR to Client ID Metadata Documents). The MCP 2026-07-28 spec deprecates RFC 7591 in favour of CIMD, retaining DCR only as a fallback for authorization servers without CIMD support. CIMD is not implemented here yet, so DCR is currently the only registration path.
Given that, do not close this as delivered. Two of its own requirements are unmet by what shipped: Security Requirement 2 (confirmation before registration, not skippable via API) and Scenario 1.5 (de-registration on server deletion). The shipped path auto-registers silently inside /oauth/authorize. Any further investment here should be framed as fallback-path work, and user-facing copy should avoid naming DCR so it survives the move to CIMD.
Note for anyone planning from this issue: because DCR sends the redirect URI without displaying it, the origin mismatch in #6458 becomes invisible on this path.
🧭 Type of Feature
🧭 Epic
Dynamic Client Registration (RFC 7591) for OAuth-protected MCP servers
Goal: When the discovery flow (#5717 or #5719) surfaces a registration_endpoint, the user can register a new OAuth client at the provider with one click. ContextForge handles the RFC 7591 protocol exchange, securely stores the returned registration_access_token, and populates client_id/client_secret in the form.
Why now: Companion issues #5717 and #5718 surface DCR availability as a hint ("you may leave Client ID/Secret blank"). The next step — actually performing the registration — eliminates the two largest remaining manual fields on the OAuth form for the providers people most commonly use (Keycloak, Okta, Auth0).
Parent Epic: #5716
🧑🏻💻 User Story 1
As a platform administrator registering a Keycloak / Okta / Auth0 MCP server, I want ContextForge to create the OAuth client at my provider automatically, so I do not have to switch to the provider's admin console, create a client manually, and copy IDs back into the form.
Acceptance Criteria
Scenario 1.1: DCR available, no initial access token required
Given discovery returned a registration_endpoint and the provider does not require an initial access token (e.g., Keycloak with anonymous registration enabled), when I click "Register OAuth client automatically", a confirmation dialog summarises what will be registered (client name, redirect URI, grant type, scopes) and warns that the action creates a real OAuth client at the provider. On confirmation, the backend POSTs RFC 7591 client metadata and:
client_id is populated in the form
client_secret, if returned, is stored server-side encrypted and shown only as •••••• in the UI (never round-tripped in plaintext after the response)
registration_access_token is stored encrypted alongside the MCP server record for future updates/deletes
- A success notice surfaces
Scenario 1.2: Provider requires an initial access token
If a previous DCR attempt returned 401 invalid_token or the provider's metadata signals required initial-access tokens, the UI prompts for an initial access token. The token is sent in the DCR Authorization: Bearer header and is not persisted after the call completes.
Scenario 1.3: DCR not available
If discovery did not surface a registration_endpoint, the "Register OAuth client automatically" button does not appear. The form behaves as today (manual Client ID / Secret entry).
Scenario 1.4: DCR fails
The form surfaces a structured error code (invalid_redirect_uri, invalid_client_metadata, unauthorized, unreachable, timeout) and a human-readable message. Nothing is persisted server-side. The user can retry or fall back to manual entry.
Scenario 1.5: User-initiated client de-registration
A registered client retains its registration_access_token. Deleting the MCP server record offers an option ("Also delete the client at the OAuth provider"). If selected, the backend issues DELETE to the client's registration URI using the stored access token. If de-registration fails, MCP server deletion still proceeds; the failure is logged and the user is informed.
🛡 Security Requirements (must-have)
- RBAC: default-deny. Same permission as MCP server creation.
- Explicit confirmation before registration. DCR creates a real, persistent side effect at the provider. Confirm dialog is mandatory; cannot be skipped via API.
- Per-user rate limit on DCR endpoint (~5/min). Prevents using ContextForge to spam an IdP's registration endpoint.
- Encrypt
registration_access_token and client_secret at rest using the existing AUTH_ENCRYPTION_SECRET mechanism. Same key handling as other secrets in oauth_config.
- Never log client secrets or registration access tokens. Apply redaction in audit log and error-log paths.
- HTTPS-only target with
SharedHttpClient SSRF posture.
- Validate DCR response before persisting:
client_id must be a non-empty string; client_secret (if present) must be non-empty; registration_access_token (if present) must be non-empty.
- Audit log every DCR call (user, provider URL, outcome, not the secrets themselves).
- Software statements (signed JWT registration tokens) are out of scope for v1 — document the gap. Providers requiring software statements (some enterprise OIDC providers) cannot use this flow yet.
- Deny-path regression tests: unauthenticated caller, insufficient-permission caller, no
registration_endpoint in cached discovery, non-HTTPS registration endpoint, oversized response, provider returns 4xx/5xx, secret persisted only encrypted, secret never returned in plaintext after initial registration response.
📐 Endpoint Contract
POST /gateways/dcr-register
Authorization: Bearer <token> # gateway-create permission required
Content-Type: application/json
{
"issuer": "https://auth.example.com", // must already have been discovered
"redirect_uri": "https://gw.example.com/oauth/gateway/{id}/callback",
"grant_types": ["authorization_code"],
"scopes": ["openid", "profile"],
"client_name": "ContextForge — MyGateway",
"initial_access_token": null // optional
}
200 OK
{
"registered": true,
"client_id": "abc123",
"client_secret_set": true, // boolean only; secret itself is server-side
"registration_access_token_set": true,
"error": null,
"error_code": null
}
200 OK — failed (form falls back to manual)
{
"registered": false,
"client_id": null,
"client_secret_set": false,
"registration_access_token_set": false,
"error": "Provider rejected the client metadata.",
"error_code": "invalid_client_metadata"
}
A separate DELETE /gateways/{id}/dcr-client removes the client at the provider using the stored registration_access_token.
✅ MCP Standards Check
- Implements RFC 7591 Dynamic Client Registration.
- Implements RFC 7592 client registration management (for delete).
- No breaking changes.
❎ Alternatives Considered
- Auto-register on form submit (no confirmation). Rejected. DCR is a real side effect at a third-party provider; explicit consent is mandatory.
- Store secrets client-side after registration. Rejected. Secrets are encrypted server-side via the existing
AUTH_ENCRYPTION_SECRET pattern, mirroring how other MCP server secrets are handled.
- Reuse
client_credentials token endpoint for client metadata. Rejected. RFC 7591 is the standard; reusing token endpoints is not.
📎 Additional Context
Builds on:
New code required:
DcrService.register_client() and DcrService.delete_client() in mcpgateway/services/dcr_service.py.
- New router endpoints in
mcpgateway/routers/gateways.py.
- Storage column(s) for
registration_access_token and registration management URI on the MCP server record (Alembic migration; idempotent per project convention).
React client:
- "Register OAuth client automatically" button, visible only when
dcr_available: true.
- Confirmation dialog with summary of what will be registered.
🔗 Related Issues
Status update (2026-08-28)
DCR is implemented and enabled by default.
mcpgateway/services/dcr_service.pyimplements RFC 7591 registration, AS metadata discovery, client persistence, update and delete.DCR_ENABLEDandDCR_AUTO_REGISTER_ON_MISSING_CREDENTIALSboth default totrue(config.py:1008,:1011)./oauth/authorize/{gateway_id}auto-registers when a gateway has an issuer and noclient_id(oauth_router.py:670), persists the credentials and setsauth_type=oauth.redirect_uriprogrammatically, so for DCR-capable providers the operator never copies it./oauth/registered-clientscovers listing, per-gateway lookup and deletion.What is missing is UI surfacing: nothing tells the user that Client ID and Client Secret can be left blank for a DCR-capable issuer. That hint is tracked in #6460 and depends on the v1 discovery endpoint in #5717.
The
deprecationlabel is correct and traces to #5692 ([Deprecated-3] DCR to Client ID Metadata Documents). The MCP 2026-07-28 spec deprecates RFC 7591 in favour of CIMD, retaining DCR only as a fallback for authorization servers without CIMD support. CIMD is not implemented here yet, so DCR is currently the only registration path.Given that, do not close this as delivered. Two of its own requirements are unmet by what shipped: Security Requirement 2 (confirmation before registration, not skippable via API) and Scenario 1.5 (de-registration on server deletion). The shipped path auto-registers silently inside
/oauth/authorize. Any further investment here should be framed as fallback-path work, and user-facing copy should avoid naming DCR so it survives the move to CIMD.Note for anyone planning from this issue: because DCR sends the redirect URI without displaying it, the origin mismatch in #6458 becomes invisible on this path.
🧭 Type of Feature
🧭 Epic
Dynamic Client Registration (RFC 7591) for OAuth-protected MCP servers
Goal: When the discovery flow (#5717 or #5719) surfaces a
registration_endpoint, the user can register a new OAuth client at the provider with one click. ContextForge handles the RFC 7591 protocol exchange, securely stores the returnedregistration_access_token, and populatesclient_id/client_secretin the form.Why now: Companion issues #5717 and #5718 surface DCR availability as a hint ("you may leave Client ID/Secret blank"). The next step — actually performing the registration — eliminates the two largest remaining manual fields on the OAuth form for the providers people most commonly use (Keycloak, Okta, Auth0).
Parent Epic: #5716
🧑🏻💻 User Story 1
As a platform administrator registering a Keycloak / Okta / Auth0 MCP server, I want ContextForge to create the OAuth client at my provider automatically, so I do not have to switch to the provider's admin console, create a client manually, and copy IDs back into the form.
Acceptance Criteria
Scenario 1.1: DCR available, no initial access token required
Given discovery returned a
registration_endpointand the provider does not require an initial access token (e.g., Keycloak with anonymous registration enabled), when I click "Register OAuth client automatically", a confirmation dialog summarises what will be registered (client name, redirect URI, grant type, scopes) and warns that the action creates a real OAuth client at the provider. On confirmation, the backend POSTs RFC 7591 client metadata and:client_idis populated in the formclient_secret, if returned, is stored server-side encrypted and shown only as••••••in the UI (never round-tripped in plaintext after the response)registration_access_tokenis stored encrypted alongside the MCP server record for future updates/deletesScenario 1.2: Provider requires an initial access token
If a previous DCR attempt returned
401 invalid_tokenor the provider's metadata signals required initial-access tokens, the UI prompts for an initial access token. The token is sent in the DCRAuthorization: Bearerheader and is not persisted after the call completes.Scenario 1.3: DCR not available
If discovery did not surface a
registration_endpoint, the "Register OAuth client automatically" button does not appear. The form behaves as today (manual Client ID / Secret entry).Scenario 1.4: DCR fails
The form surfaces a structured error code (
invalid_redirect_uri,invalid_client_metadata,unauthorized,unreachable,timeout) and a human-readable message. Nothing is persisted server-side. The user can retry or fall back to manual entry.Scenario 1.5: User-initiated client de-registration
A registered client retains its
registration_access_token. Deleting the MCP server record offers an option ("Also delete the client at the OAuth provider"). If selected, the backend issuesDELETEto the client's registration URI using the stored access token. If de-registration fails, MCP server deletion still proceeds; the failure is logged and the user is informed.🛡 Security Requirements (must-have)
registration_access_tokenandclient_secretat rest using the existingAUTH_ENCRYPTION_SECRETmechanism. Same key handling as other secrets inoauth_config.SharedHttpClientSSRF posture.client_idmust be a non-empty string;client_secret(if present) must be non-empty;registration_access_token(if present) must be non-empty.registration_endpointin cached discovery, non-HTTPS registration endpoint, oversized response, provider returns 4xx/5xx, secret persisted only encrypted, secret never returned in plaintext after initial registration response.📐 Endpoint Contract
200 OK
{ "registered": true, "client_id": "abc123", "client_secret_set": true, // boolean only; secret itself is server-side "registration_access_token_set": true, "error": null, "error_code": null }200 OK — failed (form falls back to manual)
{ "registered": false, "client_id": null, "client_secret_set": false, "registration_access_token_set": false, "error": "Provider rejected the client metadata.", "error_code": "invalid_client_metadata" }A separate
DELETE /gateways/{id}/dcr-clientremoves the client at the provider using the storedregistration_access_token.✅ MCP Standards Check
❎ Alternatives Considered
AUTH_ENCRYPTION_SECRETpattern, mirroring how other MCP server secrets are handled.client_credentialstoken endpoint for client metadata. Rejected. RFC 7591 is the standard; reusing token endpoints is not.📎 Additional Context
Builds on:
registration_endpoint.dcr: truefor Keycloak, Okta, Auth0).redirect_uriandclient_namecome from MCP server state, not user input.New code required:
DcrService.register_client()andDcrService.delete_client()inmcpgateway/services/dcr_service.py.mcpgateway/routers/gateways.py.registration_access_tokenand registration management URI on the MCP server record (Alembic migration; idempotent per project convention).React client:
dcr_available: true.🔗 Related Issues
[Deprecated-3][CONTROL-PLANE] DCR → Client ID Metadata Documents #5692 [Deprecated-3] DCR to Client ID Metadata Documents. Upstream deprecation of RFC 7591 in the MCP 2026-07-28 spec; DCR retained as the fallback path this issue builds on.
Companion: [FEATURE]: Auto-fill MCP server OAuth endpoints via issuer discovery (RFC 8414 / OIDC Discovery) #5717 — Auto-fill MCP server OAuth endpoints via issuer discovery
Companion: [FEATURE]: OAuth provider presets for MCP server registration #5718 — OAuth provider presets for MCP server registration
Phase 2: Detect OAuth requirement from MCP server URL via RFC 9728 Protected Resource Metadata #5719 — Detect OAuth requirement from MCP server URL via RFC 9728 PRM
[EPIC][UI-REWRITE]: Frontend refactor and redesign — [UI-REWRITE]: Frontend refactor and redesign - Replace HTMX with modern framework #4093
[EPIC][UI][SECURITY]: OAuth Gateway Authorization UX Overhaul — [EPIC][UI][SECURITY]: OAuth Gateway Authorization UX Overhaul — Guided Flows, Validation, and Error Recovery #2861