Skip to content

Commit e4de220

Browse files
Merge pull request #166 from saucow/oauth-notification-refresh-flows
OAuth token event monitoring + refresh
2 parents 04c7f19 + 5439a05 commit e4de220

File tree

12 files changed

+950
-173
lines changed

12 files changed

+950
-173
lines changed

cmd/docker-mcp/oauth/revoke.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,35 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/docker/mcp-gateway/pkg/catalog"
78
"github.com/docker/mcp-gateway/pkg/desktop"
89
)
910

1011
func Revoke(ctx context.Context, app string) error {
1112
client := desktop.NewAuthClient()
1213

13-
// Check if this is a DCR provider
14-
dcrClient, err := client.GetDCRClient(ctx, app)
15-
if err == nil && dcrClient.State != "" {
16-
// Handle UNREGISTERED providers - they don't have tokens yet
17-
if dcrClient.State == "unregistered" {
18-
return fmt.Errorf("provider %s is not authenticated yet - nothing to revoke", app)
19-
}
14+
// Get catalog to check if this is a remote OAuth server
15+
catalogData, err := catalog.GetWithOptions(ctx, true, nil)
16+
if err != nil {
17+
return fmt.Errorf("failed to get catalog: %w", err)
18+
}
19+
20+
server, found := catalogData.Servers[app]
21+
isRemoteOAuth := found && server.IsRemoteOAuthServer()
22+
23+
fmt.Printf("Revoking OAuth access for %s...\n", app)
24+
25+
// Revoke tokens
26+
if err := client.DeleteOAuthApp(ctx, app); err != nil {
27+
return fmt.Errorf("failed to revoke OAuth access: %w", err)
28+
}
2029

21-
// REGISTERED DCR provider - revoke tokens but preserve DCR client for re-auth
22-
fmt.Printf("Revoking OAuth access for %s...\n", app)
23-
if err := client.DeleteOAuthApp(ctx, app); err != nil {
24-
return fmt.Errorf("failed to revoke OAuth access for %s: %w", app, err)
30+
// For remote OAuth servers, also delete DCR client
31+
if isRemoteOAuth {
32+
if err := client.DeleteDCRClient(ctx, app); err != nil {
33+
return fmt.Errorf("failed to remove DCR client: %w", err)
2534
}
26-
fmt.Printf("OAuth access revoked for %s\n", app)
27-
fmt.Printf("Note: DCR client registration preserved. Run 'docker mcp oauth authorize %s' to re-authenticate\n", app)
28-
return nil
2935
}
3036

31-
// Built-in OAuth provider - just revoke tokens
32-
return client.DeleteOAuthApp(ctx, app)
37+
return nil
3338
}

cmd/docker-mcp/server/enable.go

Lines changed: 2 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,11 @@ import (
99

1010
"github.com/docker/mcp-gateway/pkg/catalog"
1111
"github.com/docker/mcp-gateway/pkg/config"
12-
"github.com/docker/mcp-gateway/pkg/desktop"
1312
"github.com/docker/mcp-gateway/pkg/docker"
13+
"github.com/docker/mcp-gateway/pkg/oauth"
1414
)
1515

1616
func Disable(ctx context.Context, docker docker.Client, serverNames []string, mcpOAuthDcrEnabled bool) error {
17-
// Get catalog including user-configured catalogs to find OAuth-enabled remote servers for DCR cleanup
18-
catalog, err := catalog.GetWithOptions(ctx, true, nil)
19-
if err != nil {
20-
return fmt.Errorf("failed to get catalog: %w", err)
21-
}
22-
23-
// Clean up OAuth for disabled servers first
24-
for _, serverName := range serverNames {
25-
if server, found := catalog.Servers[serverName]; found {
26-
// Three-condition check: DCR flag enabled AND type="remote" AND oauth present
27-
if mcpOAuthDcrEnabled && server.IsRemoteOAuthServer() {
28-
cleanupOAuthForRemoteServer(ctx, serverName)
29-
}
30-
}
31-
}
32-
3317
return update(ctx, docker, nil, serverNames, mcpOAuthDcrEnabled)
3418
}
3519

@@ -76,7 +60,7 @@ func update(ctx context.Context, docker docker.Client, add []string, remove []st
7660

7761
// Three-condition check: DCR flag enabled AND type="remote" AND oauth present
7862
if mcpOAuthDcrEnabled && server.IsRemoteOAuthServer() {
79-
if err := registerProviderForLazySetup(ctx, serverName); err != nil {
63+
if err := oauth.RegisterProviderForLazySetup(ctx, serverName); err != nil {
8064
fmt.Printf("Warning: Failed to register OAuth provider for %s: %v\n", serverName, err)
8165
fmt.Printf(" You can run 'docker mcp oauth authorize %s' later to set up authentication.\n", serverName)
8266
} else {
@@ -112,71 +96,3 @@ func update(ctx context.Context, docker docker.Client, add []string, remove []st
11296

11397
return nil
11498
}
115-
116-
// registerProviderForLazySetup registers a provider for lazy DCR setup
117-
// This shows the provider in the OAuth tab immediately without doing network calls
118-
func registerProviderForLazySetup(ctx context.Context, serverName string) error {
119-
client := desktop.NewAuthClient()
120-
121-
// Check if DCR client already exists to avoid double-registration
122-
_, err := client.GetDCRClient(ctx, serverName)
123-
if err == nil {
124-
// Provider already registered, no need to register again
125-
return nil
126-
}
127-
128-
// Get catalog to extract provider name
129-
catalog, err := catalog.GetWithOptions(ctx, true, nil)
130-
if err != nil {
131-
return fmt.Errorf("failed to get catalog: %w", err)
132-
}
133-
134-
server, found := catalog.Servers[serverName]
135-
if !found {
136-
return fmt.Errorf("server %s not found in catalog", serverName)
137-
}
138-
139-
// Extract provider name from OAuth config
140-
if server.OAuth == nil || len(server.OAuth.Providers) == 0 {
141-
return fmt.Errorf("server %s has no OAuth providers configured", serverName)
142-
}
143-
144-
providerName := server.OAuth.Providers[0].Provider // Use first provider
145-
146-
fmt.Printf("Configuring OAuth provider %s (provider: %s) for authentication...\n", serverName, providerName)
147-
148-
// Use the existing DCR endpoint with pending=true to register provider without DCR
149-
dcrRequest := desktop.RegisterDCRRequest{
150-
ProviderName: providerName,
151-
}
152-
153-
if err := client.RegisterDCRClientPending(ctx, serverName, dcrRequest); err != nil {
154-
return fmt.Errorf("failed to register pending DCR provider: %w", err)
155-
}
156-
157-
return nil
158-
}
159-
160-
// cleanupOAuthForRemoteServer removes OAuth provider and DCR client for clean slate UX
161-
// This ensures disabled servers disappear completely from the Docker Desktop OAuth tab
162-
func cleanupOAuthForRemoteServer(ctx context.Context, serverName string) {
163-
client := desktop.NewAuthClient()
164-
165-
fmt.Printf("Cleaning up OAuth for %s...\n", serverName)
166-
167-
// 1. Revoke OAuth tokens (idempotent - fails gracefully if not exists)
168-
if err := client.DeleteOAuthApp(ctx, serverName); err != nil {
169-
fmt.Printf(" • No OAuth tokens to revoke\n")
170-
} else {
171-
fmt.Printf(" • OAuth tokens revoked\n")
172-
}
173-
174-
// 2. Delete DCR client data (idempotent - fails gracefully if not exists)
175-
if err := client.DeleteDCRClient(ctx, serverName); err != nil {
176-
fmt.Printf(" • No DCR client to remove\n")
177-
} else {
178-
fmt.Printf(" • DCR client data removed\n")
179-
}
180-
181-
fmt.Printf("OAuth cleanup complete for %s\n", serverName)
182-
}

pkg/desktop/auth.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ func (c *Tools) ListOAuthApps(ctx context.Context) ([]OAuthApp, error) {
5252
return result, err
5353
}
5454

55+
func (c *Tools) GetOAuthApp(ctx context.Context, app string) (*OAuthApp, error) {
56+
AvoidResourceSaverMode(ctx)
57+
58+
var result OAuthApp
59+
err := c.rawClient.Get(ctx, fmt.Sprintf("/apps/%v", app), &result)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return &result, nil
64+
}
65+
5566
func (c *Tools) PostOAuthApp(ctx context.Context, app, scopes string, disableAutoOpen bool) (AuthResponse, error) {
5667
AvoidResourceSaverMode(ctx)
5768

pkg/gateway/configuration.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"context"
77
"fmt"
88
"os"
9-
"path/filepath"
109
"sort"
1110
"strings"
1211
"time"
@@ -106,11 +105,6 @@ type FileBasedConfiguration struct {
106105
docker docker.Client
107106
}
108107

109-
// isDCRFeatureEnabled checks if the mcp-oauth-dcr feature is enabled
110-
func (c *FileBasedConfiguration) isDCRFeatureEnabled() bool {
111-
return c.McpOAuthDcrEnabled
112-
}
113-
114108
func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan Configuration, func() error, error) {
115109
configuration, err := c.readOnce(ctx)
116110
if err != nil {
@@ -214,19 +208,6 @@ func (c *FileBasedConfiguration) Read(ctx context.Context) (Configuration, chan
214208
}
215209
}
216210

217-
// Add token event file to watcher only if DCR feature is enabled
218-
if c.isDCRFeatureEnabled() {
219-
tokenEventPath := filepath.Join(os.Getenv("HOME"), ".docker", "mcp", TokenEventFilename)
220-
if err := watcher.Add(tokenEventPath); err != nil && !os.IsNotExist(err) {
221-
log(fmt.Sprintf("DCR: Warning - Could not watch token event file %s: %v", tokenEventPath, err))
222-
// Don't fail configuration loading if token event file can't be watched
223-
} else {
224-
log(fmt.Sprintf("DCR: Watching token event file: %s", tokenEventPath))
225-
}
226-
} else {
227-
log("DCR: Token event file watching disabled (mcp-oauth-dcr feature inactive)")
228-
}
229-
230211
return configuration, updates, watcher.Close, nil
231212
}
232213

pkg/gateway/dynamic_mcps.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.opentelemetry.io/otel/metric"
1919

2020
"github.com/docker/mcp-gateway/pkg/catalog"
21+
"github.com/docker/mcp-gateway/pkg/oauth"
2122
"github.com/docker/mcp-gateway/pkg/oci"
2223
"github.com/docker/mcp-gateway/pkg/telemetry"
2324
)
@@ -287,6 +288,17 @@ func (g *Gateway) createMcpAddTool(clientConfig *clientConfig) *ToolRegistration
287288
return nil, fmt.Errorf("failed to reload configuration: %w", err)
288289
}
289290

291+
// Register DCR client and start OAuth provider if this is a remote OAuth server
292+
if g.McpOAuthDcrEnabled {
293+
// Register DCR client with DD so user can authorize
294+
if err := oauth.RegisterProviderForLazySetup(ctx, serverName); err != nil {
295+
logf("Warning: Failed to register OAuth provider for %s: %v", serverName, err)
296+
}
297+
298+
// Start provider
299+
g.startProvider(ctx, serverName)
300+
}
301+
290302
return &mcp.CallToolResult{
291303
Content: []mcp.Content{&mcp.TextContent{
292304
Text: fmt.Sprintf("Successfully added server '%s'. Assume that it is fully configured and ready to use.", serverName),
@@ -350,6 +362,11 @@ func (g *Gateway) createMcpRemoveTool() *ToolRegistration {
350362
// Update the current configuration state
351363
g.configuration.serverNames = updatedServerNames
352364

365+
// Stop OAuth provider if this is an OAuth server
366+
if g.McpOAuthDcrEnabled {
367+
g.stopProvider(serverName)
368+
}
369+
353370
if err := g.removeServerConfiguration(ctx, serverName); err != nil {
354371
return nil, fmt.Errorf("failed to remove server configuration: %w", err)
355372
}

0 commit comments

Comments
 (0)