From e06c9e5c0d251e0cf167c61e60b166ab4394ec12 Mon Sep 17 00:00:00 2001 From: OzairP Date: Tue, 20 Jan 2026 00:42:06 -0500 Subject: [PATCH 1/3] oauth: add NewManagerWithRedirectURI constructor Add a new constructor that accepts a custom redirect URI parameter. This enables callers to specify localhost callbacks instead of the default mcp.docker.com proxy, which is needed for OAuth providers not registered in Docker's provider registry. The existing NewManager() delegates to the new constructor with the default redirect URI, maintaining backward compatibility. --- pkg/oauth/manager.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/oauth/manager.go b/pkg/oauth/manager.go index e7f67c105..78e9bdf80 100644 --- a/pkg/oauth/manager.go +++ b/pkg/oauth/manager.go @@ -25,11 +25,17 @@ type Manager struct { // NewManager creates a new OAuth manager for CE mode func NewManager(credHelper credentials.Helper) *Manager { + return NewManagerWithRedirectURI(credHelper, DefaultRedirectURI) +} + +// NewManagerWithRedirectURI creates a new OAuth manager with a custom redirect URI +// Use this for CE mode with localhost callbacks +func NewManagerWithRedirectURI(credHelper credentials.Helper, redirectURI string) *Manager { return &Manager{ - dcrManager: dcr.NewManager(credHelper, DefaultRedirectURI), + dcrManager: dcr.NewManager(credHelper, redirectURI), tokenStore: NewTokenStore(credHelper), stateManager: NewStateManager(), - redirectURI: DefaultRedirectURI, + redirectURI: redirectURI, } } From 4de9695f976ff029ecff33de4c352fa3b119dea5 Mon Sep 17 00:00:00 2001 From: OzairP Date: Tue, 20 Jan 2026 00:42:22 -0500 Subject: [PATCH 2/3] oauth: use localhost redirect for CE mode with remote servers Fix OAuth authorization for remote MCP servers not registered in Docker's provider registry (e.g., Honeycomb). Problem: CE mode registered DCR clients with mcp.docker.com/oauth/callback as the redirect URI, but Docker's proxy only routes callbacks for pre-registered providers. Unregistered providers like Honeycomb would complete the OAuth consent flow successfully, but the callback routing would fail with 'provider not found'. Solution: - Create the callback server first to get the localhost URL - Register DCR client with localhost redirect URI directly - Only apply CE mode to remote servers (container-based servers like github-official still use Docker Desktop OAuth) This allows any remote MCP server that supports OAuth Discovery (RFC 9728) and Dynamic Client Registration (RFC 7591) to work with the MCP Gateway, regardless of whether it's in Docker's provider registry. --- cmd/docker-mcp/oauth/auth.go | 53 +++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/cmd/docker-mcp/oauth/auth.go b/cmd/docker-mcp/oauth/auth.go index 6d5cb6c99..d3c32c451 100644 --- a/cmd/docker-mcp/oauth/auth.go +++ b/cmd/docker-mcp/oauth/auth.go @@ -5,20 +5,36 @@ import ( "fmt" "time" + "github.com/docker/mcp-gateway/pkg/catalog" "github.com/docker/mcp-gateway/pkg/desktop" pkgoauth "github.com/docker/mcp-gateway/pkg/oauth" ) func Authorize(ctx context.Context, app string, scopes string) error { - // Check if running in CE mode - if pkgoauth.IsCEMode() { + // Check if running in CE mode AND if this is a remote server + // CE mode with localhost redirect only works for remote MCP servers + // Container-based servers (like github-official) need Docker Desktop OAuth + if pkgoauth.IsCEMode() && isRemoteServer(ctx, app) { return authorizeCEMode(ctx, app, scopes) } - // Desktop mode - existing implementation + // Desktop mode - existing implementation (for container servers or when CE mode is off) return authorizeDesktopMode(ctx, app, scopes) } +// isRemoteServer checks if the server is a remote MCP server (not a container) +func isRemoteServer(ctx context.Context, serverName string) bool { + cat, err := catalog.GetWithOptions(ctx, true, nil) + if err != nil { + return false + } + server, found := cat.Servers[serverName] + if !found { + return false + } + return server.Remote.URL != "" +} + // authorizeDesktopMode handles OAuth via Docker Desktop (existing behavior) func authorizeDesktopMode(ctx context.Context, app string, scopes string) error { client := desktop.NewAuthClient() @@ -42,22 +58,28 @@ func authorizeDesktopMode(ctx context.Context, app string, scopes string) error func authorizeCEMode(ctx context.Context, serverName string, scopes string) error { fmt.Printf("Starting OAuth authorization for %s...\n", serverName) - // Create OAuth manager with read-write credential helper + // Step 1: Create callback server FIRST to get the localhost URL + // This allows us to register DCR with localhost redirect instead of mcp.docker.com proxy + callbackServer, err := pkgoauth.NewCallbackServer() + if err != nil { + return fmt.Errorf("failed to create callback server: %w", err) + } + + // Use localhost callback URL as redirect URI for DCR registration + // This bypasses Docker's OAuth proxy which doesn't know about all providers + callbackURL := callbackServer.URL() + fmt.Printf("Using localhost redirect: %s\n", callbackURL) + + // Create OAuth manager with localhost redirect URI credHelper := pkgoauth.NewReadWriteCredentialHelper() - manager := pkgoauth.NewManager(credHelper) + manager := pkgoauth.NewManagerWithRedirectURI(credHelper, callbackURL) - // Step 1: Ensure DCR client is registered + // Step 2: Ensure DCR client is registered with localhost redirect fmt.Printf("Checking DCR registration...\n") if err := manager.EnsureDCRClient(ctx, serverName, scopes); err != nil { return fmt.Errorf("DCR registration failed: %w", err) } - // Step 2: Create callback server - callbackServer, err := pkgoauth.NewCallbackServer() - if err != nil { - return fmt.Errorf("failed to create callback server: %w", err) - } - // Start callback server in background go func() { if err := callbackServer.Start(); err != nil { @@ -72,7 +94,7 @@ func authorizeCEMode(ctx context.Context, serverName string, scopes string) erro } }() - // Step 3: Build authorization URL with callback URL in state + // Step 3: Build authorization URL (no proxy state needed - direct localhost redirect) fmt.Printf("Generating authorization URL...\n") scopesList := []string{} @@ -80,9 +102,8 @@ func authorizeCEMode(ctx context.Context, serverName string, scopes string) erro scopesList = []string{scopes} } - // Pass callback URL - will be embedded in state for mcp-oauth proxy routing - callbackURL := callbackServer.URL() - authURL, baseState, _, err := manager.BuildAuthorizationURL(ctx, serverName, scopesList, callbackURL) + // Empty callbackURL since we're using direct localhost redirect (no proxy routing) + authURL, baseState, _, err := manager.BuildAuthorizationURL(ctx, serverName, scopesList, "") if err != nil { return fmt.Errorf("failed to generate authorization URL: %w", err) } From 2c9ba22c406cfc26fd8ea99967f99d19b4136501 Mon Sep 17 00:00:00 2001 From: OzairP Date: Tue, 20 Jan 2026 00:43:39 -0500 Subject: [PATCH 3/3] oauth: add test for NewManagerWithRedirectURI Verify that the new constructor correctly initializes all manager components with the custom redirect URI. --- pkg/oauth/manager_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pkg/oauth/manager_test.go b/pkg/oauth/manager_test.go index 756f1928b..cd1a02044 100644 --- a/pkg/oauth/manager_test.go +++ b/pkg/oauth/manager_test.go @@ -238,6 +238,18 @@ func TestManager_SetRedirectURI(t *testing.T) { assert.Equal(t, customURI, manager.redirectURI) } +func TestNewManagerWithRedirectURI(t *testing.T) { + helper := newFakeCredentialHelper() + localhostURI := "http://localhost:5001/callback" + + manager := NewManagerWithRedirectURI(helper, localhostURI) + + assert.Equal(t, localhostURI, manager.redirectURI) + assert.NotNil(t, manager.dcrManager) + assert.NotNil(t, manager.tokenStore) + assert.NotNil(t, manager.stateManager) +} + func TestManager_EnsureDCRClient_AlreadyExists(t *testing.T) { manager := setupTestManager(t) serverName := "test-server"