Skip to content

Commit fe07398

Browse files
Add authentication for SSE and streaming gateway modes (#190)
* Add tool-name-prefix feature flag and Server.Prefix field This commit implements tool name prefixing with the following features: 1. New feature flag 'tool-name-prefix': - When enabled, all tool names are prefixed with server_name:tool_name - Configured in Docker config file under features - Provides a default policy for tool name prefixing 2. New Server.Prefix field in catalog types: - Optional prefix field in ServerSpec - When set, overrides the default server name prefix - Applied even if tool-name-prefix feature flag is disabled - Provides per-server customization of tool naming 3. Implementation details: - Added getToolNamePrefix() helper to determine prefix based on: * Server.Prefix if set (highest priority) * Server name if ToolNamePrefix flag enabled * Empty string otherwise - Added prefixToolName() helper to apply prefix format (prefix:toolname) - Applied to both MCP server tools and POCI tool groups - Tool names are prefixed at registration time in listCapabilities() Benefits: - Avoids tool name conflicts when using multiple servers - Allows disambiguation of tools with same names - Per-server prefix customization via Server.Prefix field - Global policy control via tool-name-prefix feature flag 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add authentication for SSE and streaming gateway modes Implement secure authentication for HTTP-based gateway transports (SSE and streaming) to protect MCP server access. The authentication system supports two methods: query parameter tokens and HTTP Basic Auth. Key features: - Auto-generates 50-character cryptographically secure tokens using lowercase letters and numbers - Respects MCP_GATEWAY_AUTH_TOKEN environment variable for custom tokens - Supports query parameter auth via ?MCP_GATEWAY_AUTH_TOKEN=<token> - Supports HTTP Basic Auth with any username and token as password - Health endpoint (/health) remains accessible without authentication - Uses constant-time comparison to prevent timing attacks - Outputs complete authenticated URLs on gateway startup The gateway now displays the full URL with embedded token on startup, making it easy to copy and use in client configurations. Basic Auth credentials are also shown as an alternative authentication method. This change prevents DNS rebinding attacks by forcing any local gateway runtime to run with an auth token, ensuring that malicious websites cannot exploit the locally-running gateway service. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix linter issues in authentication code Address all golangci-lint warnings: - Check error returns from w.Write() calls - Use integer range syntax (Go 1.22+) in token generation loop - Use context-aware HTTP requests in integration tests - Rename unused parameters to underscore - Remove unused parseAuthHeader function - Use http.MethodGet constant instead of string literal 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Refactor: Extract addRemoteOAuthServer method Extract lines 471-546 of dynamic_mcps.go into a separate method called addRemoteOAuthServer for better code organization and reusability. This method handles the complete OAuth setup flow for remote OAuth servers: - Registers the DCR client - Starts the OAuth provider - Handles authorization via elicitation (if supported) - Returns appropriate URLs for manual authorization (if needed) No functional changes, purely a refactoring to improve maintainability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * remove extra files * Switch authentication from Basic Auth to Bearer token Changes: - Replace Basic Auth with Bearer token authentication in Authorization header - Update authenticationMiddleware to validate "Bearer <token>" format - Replace formatBasicAuthCredentials with formatBearerToken helper - Update all tests to use Bearer token instead of Basic Auth - Add tests for malformed Bearer token headers - Update logging messages to reflect Bearer token usage - Unhide --transport flag and document authentication in help text - Remove unused base64 imports from auth code Authentication now supports: 1. Bearer token: Authorization: Bearer <token> 2. Query parameter: ?MCP_GATEWAY_AUTH_TOKEN=<token> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Remove query parameter authentication, use Bearer token only Changes: - Remove query parameter authentication from authenticationMiddleware - Update formatGatewayURL to not include auth token in URL - Update logging to show Bearer token usage without URL parameters - Remove all query parameter auth tests from unit and integration tests - Update test counts in SSE and streaming server tests Authentication now only supports: - Bearer token: Authorization: Bearer <token> Query parameter authentication (?MCP_GATEWAY_AUTH_TOKEN=<token>) has been removed for security reasons to prevent tokens from being logged in URLs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * update docs --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 10d3c1e commit fe07398

File tree

13 files changed

+989
-78
lines changed

13 files changed

+989
-78
lines changed

CLAUDE.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
This is the **Docker MCP Gateway** - a CLI plugin that enables easy and secure running of Model Context Protocol (MCP) servers through Docker containers. The plugin acts as a gateway between AI clients and containerized MCP servers, providing isolation, security, and management capabilities.
8+
9+
## Architecture
10+
11+
The codebase follows a gateway pattern where:
12+
- **AI Client** connects to the **MCP Gateway**
13+
- **MCP Gateway** (this CLI) manages multiple **MCP Servers** running in Docker containers
14+
15+
Key architectural components:
16+
- **Gateway**: Core routing and protocol translation (`cmd/docker-mcp/internal/gateway/`)
17+
- **Client Management**: Handles connections to AI clients (`cmd/docker-mcp/client/`)
18+
- **Server Management**: Manages MCP server lifecycle (`cmd/docker-mcp/server/`)
19+
- **Catalog System**: Manages available MCP servers (`cmd/docker-mcp/catalog/`)
20+
- **Security**: Secrets management and OAuth flows (`cmd/docker-mcp/secret-management/`, `cmd/docker-mcp/oauth/`)
21+
22+
## Development Commands
23+
24+
### Building
25+
```bash
26+
# Build the CLI plugin locally
27+
make docker-mcp
28+
29+
# Cross-compile for all platforms
30+
make docker-mcp-cross
31+
```
32+
33+
### Testing
34+
```bash
35+
# Run all tests
36+
make test
37+
38+
# Run integration tests specifically
39+
make integration
40+
41+
# Run long-lived integration tests
42+
go test -count=1 ./... -run 'TestLongLived'
43+
44+
# Run specific tests by pattern
45+
go test -count=1 ./... -run 'TestIntegration'
46+
47+
# Run a single test file
48+
go test ./cmd/docker-mcp/server/server_test.go
49+
50+
# Run tests with coverage
51+
go test -cover ./...
52+
```
53+
54+
### Code Quality
55+
```bash
56+
# Format code
57+
make format
58+
59+
# Run linter
60+
make lint
61+
62+
# Run linter for specific platform
63+
make lint-linux
64+
make lint-darwin
65+
66+
# Run Go vet (static analysis)
67+
go vet ./...
68+
69+
# Run Go mod tidy to clean dependencies
70+
go mod tidy
71+
```
72+
73+
## Project Structure
74+
75+
- `cmd/docker-mcp/` - Main CLI application entry point
76+
- `cmd/docker-mcp/internal/gateway/` - Core gateway implementation with client pooling, proxy management, and transport handling
77+
- `cmd/docker-mcp/internal/docker/` - Docker integration for container management
78+
- `cmd/docker-mcp/internal/mcp/` - MCP protocol implementations (stdio, SSE)
79+
- `cmd/docker-mcp/internal/desktop/` - Docker Desktop integration and authentication
80+
- `cmd/docker-mcp/catalog/` - Server catalog management commands
81+
- `cmd/docker-mcp/client/` - Client configuration and connection management
82+
- `cmd/docker-mcp/server/` - Server lifecycle management commands
83+
- `cmd/docker-mcp/tools/` - Tool execution and management
84+
- `examples/` - Usage examples and compose configurations
85+
- `docs/` - Technical documentation
86+
87+
## Key Configuration Files
88+
89+
The CLI uses these configuration files (typically in `~/.docker/mcp/`):
90+
- `docker-mcp.yaml` - Server catalog definitions
91+
- `registry.yaml` - Registry of enabled servers
92+
- `config.yaml` - Gateway configuration and options
93+
94+
## Important Patterns
95+
96+
### Transport Modes
97+
The gateway supports multiple transport modes:
98+
- `stdio` - Standard input/output (default)
99+
- `streaming` - HTTP streaming for multiple clients
100+
- `sse` - Server-sent events
101+
102+
### Security Features
103+
- Container isolation for MCP servers
104+
- Secrets management via Docker Desktop
105+
- OAuth flow handling
106+
- API key and credential protection
107+
- Call interception and logging
108+
109+
### Client Integration
110+
The system integrates with various AI clients:
111+
- VS Code / Cursor
112+
- Claude Desktop
113+
- Continue Dev
114+
- Custom MCP clients
115+
116+
Configuration files for different clients are automatically managed in `cmd/docker-mcp/client/testdata/`.
117+
118+
## CLI Plugin Development
119+
120+
This is a Docker CLI plugin written in Go 1.24+. Key development patterns:
121+
122+
### Plugin Installation
123+
The plugin is installed as `docker-mcp` and becomes available as `docker mcp <command>`. The Makefile handles building and installing to the correct Docker CLI plugins directory (`~/.docker/cli-plugins/`).
124+
125+
### Command Structure
126+
Commands follow the Cobra CLI pattern with the main command tree defined in `cmd/docker-mcp/commands/`. Each major command area (catalog, server, client, etc.) has its own file.
127+
128+
### Configuration Management
129+
The CLI uses YAML configuration files stored in `~/.docker/mcp/`:
130+
- Server definitions are loaded from catalog files
131+
- Runtime configuration is managed through config.yaml
132+
- Server enablement tracked in registry.yaml
133+
134+
### Container Lifecycle
135+
MCP servers run as Docker containers with proper lifecycle management:
136+
- Images are pulled and validated before use
137+
- Containers have consistent naming patterns
138+
- Health checks and logging are built-in
139+
- Proper cleanup on shutdown
140+
141+
### Testing Patterns
142+
- Integration tests require Docker daemon
143+
- Long-lived tests run actual container scenarios
144+
- Mock configurations in testdata directories
145+
- Use `go test -count=1` to disable test caching

cmd/docker-mcp/commands/gateway.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
7777
// Check if dynamic tools feature is enabled
7878
options.DynamicTools = isDynamicToolsFeatureEnabled(dockerCli)
7979

80+
// Check if tool name prefix feature is enabled
81+
options.ToolNamePrefix = isToolNamePrefixFeatureEnabled(dockerCli)
82+
8083
// Update catalog URL based on mcp-oauth-dcr flag if using default Docker catalog URL
8184
if len(options.CatalogPath) == 1 && (options.CatalogPath[0] == catalog.DockerCatalogURLV2 || options.CatalogPath[0] == catalog.DockerCatalogURLV3) {
8285
options.CatalogPath[0] = catalog.GetDockerCatalogURL(options.McpOAuthDcrEnabled)
@@ -160,7 +163,7 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
160163
runCmd.Flags().StringArrayVar(&options.OciRef, "oci-ref", options.OciRef, "OCI image references to use")
161164
runCmd.Flags().StringSliceVar(&mcpRegistryUrls, "mcp-registry", nil, "MCP registry URLs to fetch servers from (can be repeated)")
162165
runCmd.Flags().IntVar(&options.Port, "port", options.Port, "TCP port to listen on (default is to listen on stdio)")
163-
runCmd.Flags().StringVar(&options.Transport, "transport", options.Transport, "stdio, sse or streaming (default is stdio)")
166+
runCmd.Flags().StringVar(&options.Transport, "transport", options.Transport, "stdio, sse or streaming. Uses MCP_GATEWAY_AUTH_TOKEN environment variable for localhost authentication to prevent dns rebinding attacks.")
164167
runCmd.Flags().BoolVar(&options.LogCalls, "log-calls", options.LogCalls, "Log calls to the tools")
165168
runCmd.Flags().BoolVar(&options.BlockSecrets, "block-secrets", options.BlockSecrets, "Block secrets from being/received sent to/from tools")
166169
runCmd.Flags().BoolVar(&options.BlockNetwork, "block-network", options.BlockNetwork, "Block tools from accessing forbidden network resources")
@@ -176,7 +179,6 @@ func gatewayCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command
176179
runCmd.Flags().StringVar(&options.LogFilePath, "log", options.LogFilePath, "Path to log file for stderr output (relative or absolute)")
177180

178181
// Very experimental features
179-
_ = runCmd.Flags().MarkHidden("transport")
180182
_ = runCmd.Flags().MarkHidden("log")
181183

182184
cmd.AddCommand(runCmd)
@@ -303,3 +305,18 @@ func isDynamicToolsFeatureEnabled(dockerCli command.Cli) bool {
303305

304306
return value == "enabled"
305307
}
308+
309+
// isToolNamePrefixFeatureEnabled checks if the tool-name-prefix feature is enabled
310+
func isToolNamePrefixFeatureEnabled(dockerCli command.Cli) bool {
311+
configFile := dockerCli.ConfigFile()
312+
if configFile == nil || configFile.Features == nil {
313+
return false
314+
}
315+
316+
value, exists := configFile.Features["tool-name-prefix"]
317+
if !exists {
318+
return false
319+
}
320+
321+
return value == "enabled"
322+
}

docs/generator/reference/docker_mcp_gateway_run.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,10 @@ options:
275275
- option: transport
276276
value_type: string
277277
default_value: stdio
278-
description: stdio, sse or streaming (default is stdio)
278+
description: |
279+
stdio, sse or streaming. Uses MCP_GATEWAY_AUTH_TOKEN environment variable for localhost authentication to prevent dns rebinding attacks.
279280
deprecated: false
280-
hidden: true
281+
hidden: false
281282
experimental: false
282283
experimentalcli: false
283284
kubernetes: false

docs/generator/reference/mcp_gateway_run.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Run the gateway
3232
| `--static` | `bool` | | Enable static mode (aka pre-started servers) |
3333
| `--tools` | `stringSlice` | | List of tools to enable |
3434
| `--tools-config` | `stringSlice` | `[tools.yaml]` | Paths to the tools files (absolute or relative to ~/.docker/mcp/) |
35+
| `--transport` | `string` | `stdio` | stdio, sse or streaming. Uses MCP_GATEWAY_AUTH_TOKEN environment variable for localhost authentication to prevent dns rebinding attacks. |
3536
| `--verbose` | `bool` | | Verbose output |
3637
| `--verify-signatures` | `bool` | | Verify signatures of the server images |
3738
| `--watch` | `bool` | `true` | Watch for changes and reconfigure the gateway |

pkg/catalog/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Server struct {
3131
AllowHosts []string `yaml:"allowHosts,omitempty" json:"allowHosts,omitempty"`
3232
Tools []Tool `yaml:"tools,omitempty" json:"tools,omitempty"`
3333
Config []any `yaml:"config,omitempty" json:"config,omitempty"`
34+
Prefix string `yaml:"prefix,omitempty" json:"prefix,omitempty"`
3435
}
3536

3637
type Secret struct {

pkg/gateway/auth.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package gateway
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/subtle"
6+
"fmt"
7+
"math/big"
8+
"net/http"
9+
"os"
10+
)
11+
12+
const (
13+
tokenLength = 50
14+
// Characters to use for random token generation (lowercase letters and numbers)
15+
tokenCharset = "abcdefghijklmnopqrstuvwxyz0123456789"
16+
)
17+
18+
// generateAuthToken generates a random 50-character string using lowercase letters and numbers
19+
func generateAuthToken() (string, error) {
20+
token := make([]byte, tokenLength)
21+
charsetLen := big.NewInt(int64(len(tokenCharset)))
22+
23+
for i := range tokenLength {
24+
num, err := rand.Int(rand.Reader, charsetLen)
25+
if err != nil {
26+
return "", fmt.Errorf("failed to generate random token: %w", err)
27+
}
28+
token[i] = tokenCharset[num.Int64()]
29+
}
30+
31+
return string(token), nil
32+
}
33+
34+
// getOrGenerateAuthToken retrieves the auth token from environment variable MCP_GATEWAY_AUTH_TOKEN
35+
// or generates a new one if not set or empty
36+
func getOrGenerateAuthToken() (string, bool, error) {
37+
envToken := os.Getenv("MCP_GATEWAY_AUTH_TOKEN")
38+
if envToken != "" {
39+
return envToken, false, nil // false indicates token was from environment
40+
}
41+
42+
token, err := generateAuthToken()
43+
if err != nil {
44+
return "", false, err
45+
}
46+
return token, true, nil // true indicates token was generated
47+
}
48+
49+
// authenticationMiddleware creates an HTTP middleware that validates requests using
50+
// Bearer token in the Authorization header.
51+
//
52+
// The /health endpoint is excluded from authentication.
53+
func authenticationMiddleware(authToken string, next http.Handler) http.Handler {
54+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55+
// Skip authentication for health check endpoint
56+
if r.URL.Path == "/health" {
57+
next.ServeHTTP(w, r)
58+
return
59+
}
60+
61+
authenticated := false
62+
63+
// Check for Bearer token in Authorization header
64+
authHeader := r.Header.Get("Authorization")
65+
if authHeader != "" {
66+
// Extract Bearer token from "Bearer <token>" format
67+
const bearerPrefix = "Bearer "
68+
if len(authHeader) > len(bearerPrefix) && authHeader[:len(bearerPrefix)] == bearerPrefix {
69+
bearerToken := authHeader[len(bearerPrefix):]
70+
// Use constant-time comparison to prevent timing attacks
71+
if subtle.ConstantTimeCompare([]byte(bearerToken), []byte(authToken)) == 1 {
72+
authenticated = true
73+
}
74+
}
75+
}
76+
77+
if !authenticated {
78+
// Return 401 Unauthorized with WWW-Authenticate header
79+
w.Header().Set("WWW-Authenticate", `Bearer realm="MCP Gateway"`)
80+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
81+
return
82+
}
83+
84+
// Authentication successful, proceed to next handler
85+
next.ServeHTTP(w, r)
86+
})
87+
}
88+
89+
// formatGatewayURL formats the gateway URL without authentication info
90+
func formatGatewayURL(port int, endpoint string) string {
91+
return fmt.Sprintf("http://localhost:%d%s", port, endpoint)
92+
}
93+
94+
// formatBearerToken formats the Bearer token for display in the Authorization header
95+
func formatBearerToken(authToken string) string {
96+
return fmt.Sprintf("Authorization: Bearer %s", authToken)
97+
}

0 commit comments

Comments
 (0)