Skip to content

Feature request: Support pre-registered OAuth2 clients (client_id + client_secret) for remote servers that don't support DCR (e.g. official Slack MCP server) #474

@steilerDev

Description

@steilerDev

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions