Problem
The MCP Gateway's OAuth flow relies entirely on RFC 7591 Dynamic Client Registration (DCR) to obtain a client_id before driving the PKCE browser flow. This makes it impossible to connect to remote MCP servers that use standard OAuth 2.0 but do not expose a registration_endpoint — including the official Slack MCP server.
Verified against the official Slack MCP server
The Slack MCP server at https://mcp.slack.com/mcp is fully RFC 8414 / RFC 9728 compliant:
GET https://mcp.slack.com/.well-known/oauth-protected-resource
{
"resource": "https://mcp.slack.com",
"authorization_servers": ["https://mcp.slack.com"],
"bearer_methods_supported": ["header", "form"],
"scopes_supported": ["search:read.public", "chat:write", "channels:history", ...]
}
GET https://mcp.slack.com/.well-known/oauth-authorization-server
{
"issuer": "https://slack.com",
"authorization_endpoint": "https://slack.com/oauth/v2_user/authorize",
"token_endpoint": "https://slack.com/api/oauth.v2.user.access",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": [...]
}
No registration_endpoint is present — Slack explicitly does not support DCR. Their docs confirm: "SSE-based connections or Dynamic Client Registration are not supported." Users must register a Slack app manually at api.slack.com to obtain a client_id + client_secret, then provide those credentials to their MCP client.
The current gateway fails at the DCR step with no fallback for pre-registered credentials.
Proposed Solution
Allow users to supply pre-registered client_id and client_secret directly in the catalog server.yaml. The gateway should skip DCR entirely when these are present and proceed straight to the PKCE authorization flow using the provided credentials.
New server.yaml schema
name: slack-official
title: Slack (Official MCP)
type: remote
description: "Official Slack MCP server"
remote:
url: https://mcp.slack.com/mcp
oauth:
scopes:
- search:read.public
- chat:write
- channels:history
credentials:
clientId: "your-slack-app-client-id"
clientSecret: slack-official.client_secret # references a secret by name
tokenEndpoint: "https://slack.com/api/oauth.v2.user.access"
authorizationEndpoint: "https://slack.com/oauth/v2_user/authorize"
secrets:
- name: slack-official.client_secret
env: SLACK_CLIENT_SECRET
Then docker mcp oauth authorize slack-official drives the PKCE browser flow using the pre-registered credentials instead of attempting DCR.
Required Code Changes
Based on analysis of the codebase, six files need changes — all contained within the existing OAuth plumbing:
1. pkg/catalog/types.go — extend schema
Add OAuthCredentials to the OAuth struct:
type OAuth struct {
Providers []OAuthProvider `yaml:"providers,omitempty"`
Scopes []string `yaml:"scopes,omitempty"`
Credentials *OAuthCredentials `yaml:"credentials,omitempty"` // NEW
}
type OAuthCredentials struct {
ClientID string `yaml:"clientId"`
ClientSecret string `yaml:"clientSecret"` // secret name reference
TokenEndpoint string `yaml:"tokenEndpoint,omitempty"`
AuthorizationEndpoint string `yaml:"authorizationEndpoint,omitempty"`
}
2. pkg/oauth/dcr/credentials.go — add secret + flag to Client struct
Client currently has no ClientSecret field (public-client assumption baked in):
// Add to existing struct:
ClientSecret string `json:"clientSecret,omitempty"`
IsPublic bool `json:"isPublic,omitempty"`
3. pkg/oauth/dcr/manager.go — bypass DCR when credentials are provided
PerformDiscoveryAndRegistration() unconditionally calls RFC 7591 DCR. Add an early-exit path:
if credentials != nil && credentials.ClientID != "" {
return m.credentials.SaveClient(serverName, Client{
ClientID: credentials.ClientID,
ClientSecret: resolvedSecret,
TokenEndpoint: credentials.TokenEndpoint,
AuthorizationEndpoint: credentials.AuthorizationEndpoint,
IsPublic: false,
})
}
// existing DCR flow continues below...
4. pkg/oauth/provider.go — pass ClientSecret into oauth2.Config
Currently hardcodes ClientSecret: "":
config := &oauth2.Config{
ClientID: dcrClient.ClientID,
ClientSecret: dcrClient.ClientSecret, // now populated for confidential clients
...
}
The golang.org/x/oauth2 library already handles client_secret_post and HTTP Basic Auth auto-detection — no further changes needed for token exchange or refresh.
5. pkg/mcp/remote.go — resolve secret value before OAuth init
When the server config has oauth.credentials, resolve the clientSecret secret name to its actual value before constructing the dcr.Client.
6. cmd/docker-mcp/oauth/auth.go — handle pre-registered clients in CE mode
authorizeCEMode() currently always calls PerformDiscoveryAndRegistration(). Needs the same DCR bypass for when credentials are already in the catalog config.
Notes
- The
golang.org/x/oauth2 library already handles both client_secret_post (body params) and HTTP Basic Auth. No changes needed to token exchange or refresh logic — just populate ClientSecret in oauth2.Config and the library does the right thing.
- If
tokenEndpoint and authorizationEndpoint are provided in the credentials block, RFC 8414/9728 discovery can be skipped entirely for a faster cold start.
- This is strictly additive — existing DCR-based flows are unchanged; the bypass only activates when
oauth.credentials is present.
Related Issues
Problem
The MCP Gateway's OAuth flow relies entirely on RFC 7591 Dynamic Client Registration (DCR) to obtain a
client_idbefore driving the PKCE browser flow. This makes it impossible to connect to remote MCP servers that use standard OAuth 2.0 but do not expose aregistration_endpoint— including the official Slack MCP server.Verified against the official Slack MCP server
The Slack MCP server at
https://mcp.slack.com/mcpis fully RFC 8414 / RFC 9728 compliant:GET https://mcp.slack.com/.well-known/oauth-protected-resource{ "resource": "https://mcp.slack.com", "authorization_servers": ["https://mcp.slack.com"], "bearer_methods_supported": ["header", "form"], "scopes_supported": ["search:read.public", "chat:write", "channels:history", ...] }GET https://mcp.slack.com/.well-known/oauth-authorization-server{ "issuer": "https://slack.com", "authorization_endpoint": "https://slack.com/oauth/v2_user/authorize", "token_endpoint": "https://slack.com/api/oauth.v2.user.access", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "token_endpoint_auth_methods_supported": ["client_secret_post"], "code_challenge_methods_supported": ["S256"], "scopes_supported": [...] }No
registration_endpointis present — Slack explicitly does not support DCR. Their docs confirm: "SSE-based connections or Dynamic Client Registration are not supported." Users must register a Slack app manually atapi.slack.comto obtain aclient_id+client_secret, then provide those credentials to their MCP client.The current gateway fails at the DCR step with no fallback for pre-registered credentials.
Proposed Solution
Allow users to supply pre-registered
client_idandclient_secretdirectly in the catalogserver.yaml. The gateway should skip DCR entirely when these are present and proceed straight to the PKCE authorization flow using the provided credentials.New
server.yamlschemaThen
docker mcp oauth authorize slack-officialdrives the PKCE browser flow using the pre-registered credentials instead of attempting DCR.Required Code Changes
Based on analysis of the codebase, six files need changes — all contained within the existing OAuth plumbing:
1.
pkg/catalog/types.go— extend schemaAdd
OAuthCredentialsto theOAuthstruct:2.
pkg/oauth/dcr/credentials.go— add secret + flag toClientstructClientcurrently has noClientSecretfield (public-client assumption baked in):3.
pkg/oauth/dcr/manager.go— bypass DCR when credentials are providedPerformDiscoveryAndRegistration()unconditionally calls RFC 7591 DCR. Add an early-exit path:4.
pkg/oauth/provider.go— passClientSecretintooauth2.ConfigCurrently hardcodes
ClientSecret: "":The
golang.org/x/oauth2library already handlesclient_secret_postand HTTP Basic Auth auto-detection — no further changes needed for token exchange or refresh.5.
pkg/mcp/remote.go— resolve secret value before OAuth initWhen the server config has
oauth.credentials, resolve theclientSecretsecret name to its actual value before constructing thedcr.Client.6.
cmd/docker-mcp/oauth/auth.go— handle pre-registered clients in CE modeauthorizeCEMode()currently always callsPerformDiscoveryAndRegistration(). Needs the same DCR bypass for when credentials are already in the catalog config.Notes
golang.org/x/oauth2library already handles bothclient_secret_post(body params) and HTTP Basic Auth. No changes needed to token exchange or refresh logic — just populateClientSecretinoauth2.Configand the library does the right thing.tokenEndpointandauthorizationEndpointare provided in the credentials block, RFC 8414/9728 discovery can be skipped entirely for a faster cold start.oauth.credentialsis present.Related Issues