Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions cmd/docker-mcp/commands/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func oauthCommand() *cobra.Command {
cmd.AddCommand(lsOauthCommand())
cmd.AddCommand(authorizeOauthCommand())
cmd.AddCommand(revokeOauthCommand())
cmd.AddCommand(registerOauthCommand())
return cmd
}

Expand Down Expand Up @@ -61,3 +62,48 @@ func revokeOauthCommand() *cobra.Command {
},
}
}

func registerOauthCommand() *cobra.Command {
var opts oauth.RegisterOptions
cmd := &cobra.Command{
Use: "register <server-name>",
Short: "Manually register OAuth client credentials for a server.",
Long: `Manually register OAuth client credentials for servers that don't support Dynamic Client Registration (DCR).

This command allows you to configure pre-registered OAuth client credentials from your OAuth provider.
After registration, you can authorize with: docker mcp oauth authorize <server-name>

Examples:
# Register with client ID and secret (confidential client)
docker mcp oauth register my-server \
--client-id "abc123" \
--client-secret "secret456" \
--auth-endpoint "https://provider.com/oauth/authorize" \
--token-endpoint "https://provider.com/oauth/token" \
--scopes "read,write"

# Register public client (no secret)
docker mcp oauth register my-server \
--client-id "public-client-id" \
--auth-endpoint "https://provider.com/oauth/authorize" \
--token-endpoint "https://provider.com/oauth/token"`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return oauth.Register(cmd.Context(), args[0], opts)
},
}
flags := cmd.Flags()
flags.StringVar(&opts.ClientID, "client-id", "", "OAuth client ID (required)")
flags.StringVar(&opts.ClientSecret, "client-secret", "", "OAuth client secret (optional, for confidential clients)")
flags.StringVar(&opts.AuthorizationEndpoint, "auth-endpoint", "", "Authorization endpoint URL (required)")
flags.StringVar(&opts.TokenEndpoint, "token-endpoint", "", "Token endpoint URL (required)")
flags.StringVar(&opts.Scopes, "scopes", "", "Comma-separated list of OAuth scopes")
flags.StringVar(&opts.Provider, "provider", "", "Provider name (defaults to server name)")
flags.StringVar(&opts.ResourceURL, "resource-url", "", "Resource URL for the OAuth provider (defaults to auth endpoint base)")

_ = cmd.MarkFlagRequired("client-id")
_ = cmd.MarkFlagRequired("auth-endpoint")
_ = cmd.MarkFlagRequired("token-endpoint")

return cmd
}
132 changes: 132 additions & 0 deletions cmd/docker-mcp/oauth/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package oauth

import (
"context"
"fmt"
"net/url"
"strings"
"time"

pkgoauth "github.com/docker/mcp-gateway/pkg/oauth"
"github.com/docker/mcp-gateway/pkg/oauth/dcr"
)

// RegisterOptions contains configuration for manually registering OAuth credentials
type RegisterOptions struct {
ClientID string
ClientSecret string
AuthorizationEndpoint string
TokenEndpoint string
Scopes string
Provider string
ResourceURL string
}

// Register manually registers OAuth client credentials for a server
// This is used when the OAuth provider does not support Dynamic Client Registration (DCR)
func Register(_ context.Context, serverName string, opts RegisterOptions) error {
// Validate required fields
if err := validateRegisterOptions(serverName, opts); err != nil {
return err
}

// Parse scopes
var scopesList []string
if opts.Scopes != "" {
scopesList = strings.Split(opts.Scopes, ",")
// Trim whitespace from each scope
for i, scope := range scopesList {
scopesList[i] = strings.TrimSpace(scope)
}
}

// Use server name as provider if not specified
provider := opts.Provider
if provider == "" {
provider = serverName
}

// Use authorization endpoint as resource URL if not specified
resourceURL := opts.ResourceURL
if resourceURL == "" {
// Try to extract base URL from authorization endpoint
if u, err := url.Parse(opts.AuthorizationEndpoint); err == nil {
resourceURL = fmt.Sprintf("%s://%s", u.Scheme, u.Host)
}
}

// Create DCR client struct
client := dcr.Client{
ServerName: serverName,
ClientID: opts.ClientID,
ClientSecret: opts.ClientSecret,
ProviderName: provider,
AuthorizationEndpoint: opts.AuthorizationEndpoint,
TokenEndpoint: opts.TokenEndpoint,
RequiredScopes: scopesList,
ResourceURL: resourceURL,
RegisteredAt: time.Now(),
ClientName: fmt.Sprintf("MCP Gateway - %s (manual)", serverName),
}

// Store in credential helper
credHelper := pkgoauth.NewReadWriteCredentialHelper()
credentials := dcr.NewCredentials(credHelper)

if err := credentials.SaveClient(serverName, client); err != nil {
return fmt.Errorf("failed to store OAuth credentials: %w", err)
}

fmt.Printf("Successfully registered OAuth client for server: %s\n", serverName)
fmt.Printf(" Provider: %s\n", provider)
fmt.Printf(" Client ID: %s\n", opts.ClientID)
if opts.ClientSecret != "" {
fmt.Printf(" Client Secret: [configured]\n")
} else {
fmt.Printf(" Client Secret: [none - public client]\n")
}
fmt.Printf(" Authorization Endpoint: %s\n", opts.AuthorizationEndpoint)
fmt.Printf(" Token Endpoint: %s\n", opts.TokenEndpoint)
if len(scopesList) > 0 {
fmt.Printf(" Scopes: %s\n", strings.Join(scopesList, ", "))
}
fmt.Printf("\nYou can now authorize with: docker mcp oauth authorize %s\n", serverName)

return nil
}

// validateRegisterOptions validates the registration options
func validateRegisterOptions(serverName string, opts RegisterOptions) error {
if serverName == "" {
return fmt.Errorf("server name is required")
}

if opts.ClientID == "" {
return fmt.Errorf("client-id is required")
}

if opts.AuthorizationEndpoint == "" {
return fmt.Errorf("auth-endpoint is required")
}

if opts.TokenEndpoint == "" {
return fmt.Errorf("token-endpoint is required")
}

// Validate URLs
if _, err := url.Parse(opts.AuthorizationEndpoint); err != nil {
return fmt.Errorf("invalid auth-endpoint URL: %w", err)
}

if _, err := url.Parse(opts.TokenEndpoint); err != nil {
return fmt.Errorf("invalid token-endpoint URL: %w", err)
}

if opts.ResourceURL != "" {
if _, err := url.Parse(opts.ResourceURL); err != nil {
return fmt.Errorf("invalid resource-url: %w", err)
}
}

return nil
}
Loading
Loading