Skip to content

Commit 7c5a330

Browse files
authored
Merge pull request #8 from pvliesdonk/copilot/add-authentication-feature
Add configurable authentication and transport modes to FastMCP server
2 parents 12f06af + beeb23e commit 7c5a330

File tree

5 files changed

+534
-8
lines changed

5 files changed

+534
-8
lines changed

README.md

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,20 +79,121 @@ Configuration is managed through environment variables with the `MCP_` prefix:
7979
| `MCP_DOCKER_HOST` | (auto-detect) | Docker daemon host URL |
8080
| `MCP_LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) |
8181
| `MCP_LOG_FORMAT` | `json` | Log format (json or text) |
82-
| `MCP_HOST` | `0.0.0.0` | Server host to bind to |
83-
| `MCP_PORT` | `8000` | Server port to bind to |
82+
| `MCP_HOST` | `0.0.0.0` | Server host to bind to (for HTTP-based transports) |
83+
| `MCP_PORT` | `8000` | Server port to bind to (for HTTP-based transports) |
8484
| `MCP_DEFAULT_IMAGE_ALIAS` | `python:3.11-slim` | Default image for warm container pool |
8585
| `MCP_WARM_POOL_ENABLED` | `true` | Enable warm container pool for fast attach |
8686
| `MCP_WARM_HEALTH_CHECK_INTERVAL` | `60` | Interval in seconds for warm container health checks |
8787

88+
### Transport Configuration
89+
90+
MCP DevBench supports multiple transport modes:
91+
92+
| Variable | Default | Description |
93+
|----------|---------|-------------|
94+
| `MCP_TRANSPORT_MODE` | `streamable-http` | Transport protocol: `stdio`, `sse`, or `streamable-http` |
95+
| `MCP_PATH` | `/mcp` | Path for HTTP-based transports (sse or streamable-http) |
96+
97+
**Transport Modes:**
98+
- `stdio`: Standard input/output for local desktop clients (Claude Desktop, Cursor, etc.)
99+
- `sse`: Server-Sent Events (legacy HTTP transport, use streamable-http instead)
100+
- `streamable-http`: Recommended HTTP transport with full bidirectional streaming support
101+
102+
### Authentication Configuration
103+
104+
MCP DevBench supports multiple authentication modes for securing your server:
105+
106+
| Variable | Default | Description |
107+
|----------|---------|-------------|
108+
| `MCP_AUTH_MODE` | `none` | Authentication mode: `none`, `bearer`, `oauth`, or `oidc` |
109+
| `MCP_BEARER_TOKEN` | (none) | Bearer token for `bearer` authentication mode |
110+
| `MCP_OAUTH_CLIENT_ID` | (none) | OAuth/OIDC client ID |
111+
| `MCP_OAUTH_CLIENT_SECRET` | (none) | OAuth/OIDC client secret |
112+
| `MCP_OAUTH_CONFIG_URL` | (none) | OIDC provider configuration URL (e.g., `https://auth.example.com/.well-known/openid-configuration`) |
113+
| `MCP_OAUTH_BASE_URL` | (none) | Base URL of this server for OAuth callbacks |
114+
| `MCP_OAUTH_REDIRECT_PATH` | `/auth/callback` | OAuth callback redirect path |
115+
| `MCP_OAUTH_AUDIENCE` | (none) | OAuth audience parameter (required by some providers like Auth0) |
116+
| `MCP_OAUTH_REQUIRED_SCOPES` | (empty) | Comma-separated list of required OAuth scopes |
117+
118+
**Authentication Modes:**
119+
120+
1. **none** (default): No authentication required
121+
```bash
122+
MCP_AUTH_MODE=none
123+
```
124+
125+
2. **bearer**: Simple bearer token authentication for API keys and service accounts
126+
```bash
127+
MCP_AUTH_MODE=bearer
128+
MCP_BEARER_TOKEN=your-secret-token-here
129+
```
130+
131+
3. **oidc**: OpenID Connect authentication with automatic provider discovery
132+
```bash
133+
MCP_AUTH_MODE=oidc
134+
MCP_OAUTH_CLIENT_ID=your-client-id
135+
MCP_OAUTH_CLIENT_SECRET=your-client-secret
136+
MCP_OAUTH_CONFIG_URL=https://your-provider.com/.well-known/openid-configuration
137+
MCP_OAUTH_BASE_URL=https://your-server.com
138+
MCP_OAUTH_REDIRECT_PATH=/auth/callback
139+
# Optional:
140+
MCP_OAUTH_AUDIENCE=https://api.your-server.com
141+
MCP_OAUTH_REQUIRED_SCOPES=read,write,admin
142+
```
143+
144+
4. **oauth**: Not directly supported - use `oidc` mode instead for OAuth providers with OIDC discovery support
145+
146+
**OIDC Provider Examples:**
147+
148+
- **Auth0**: Use `https://YOUR_DOMAIN.auth0.com/.well-known/openid-configuration`
149+
- **Google**: Use `https://accounts.google.com/.well-known/openid-configuration`
150+
- **Azure AD**: Use `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration`
151+
- **Keycloak**: Use `https://YOUR_KEYCLOAK_DOMAIN/realms/YOUR_REALM/.well-known/openid-configuration`
152+
88153
### Example .env file
154+
155+
**Basic configuration (no auth, HTTP transport):**
89156
```bash
90157
MCP_ALLOWED_REGISTRIES=docker.io,ghcr.io,registry.example.com
91158
MCP_STATE_DB=/data/state.db
92159
MCP_LOG_LEVEL=DEBUG
93160
MCP_LOG_FORMAT=json
94161
MCP_DEFAULT_IMAGE_ALIAS=python:3.11-slim
95162
MCP_WARM_POOL_ENABLED=true
163+
MCP_TRANSPORT_MODE=streamable-http
164+
MCP_HOST=0.0.0.0
165+
MCP_PORT=8000
166+
MCP_PATH=/mcp
167+
```
168+
169+
**With bearer token authentication:**
170+
```bash
171+
MCP_TRANSPORT_MODE=streamable-http
172+
MCP_HOST=0.0.0.0
173+
MCP_PORT=8000
174+
MCP_AUTH_MODE=bearer
175+
MCP_BEARER_TOKEN=my-secret-api-key-12345
176+
```
177+
178+
**With OIDC authentication (Auth0 example):**
179+
```bash
180+
MCP_TRANSPORT_MODE=streamable-http
181+
MCP_HOST=0.0.0.0
182+
MCP_PORT=8000
183+
MCP_AUTH_MODE=oidc
184+
MCP_OAUTH_CLIENT_ID=your-auth0-client-id
185+
MCP_OAUTH_CLIENT_SECRET=your-auth0-client-secret
186+
MCP_OAUTH_CONFIG_URL=https://your-tenant.auth0.com/.well-known/openid-configuration
187+
MCP_OAUTH_BASE_URL=https://your-mcp-server.com
188+
MCP_OAUTH_AUDIENCE=https://your-mcp-server.com/api
189+
MCP_OAUTH_REQUIRED_SCOPES=openid,profile,email
190+
```
191+
192+
**STDIO transport for local use (e.g., Claude Desktop):**
193+
```bash
194+
MCP_TRANSPORT_MODE=stdio
195+
MCP_AUTH_MODE=none
196+
# Host and port are not used for stdio transport
96197
```
97198

98199
## Development

src/mcp_devbench/auth.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Authentication provider factory for MCP DevBench."""
2+
3+
from typing import Any
4+
5+
from mcp_devbench.config import get_settings
6+
from mcp_devbench.utils import get_logger
7+
8+
logger = get_logger(__name__)
9+
10+
11+
def create_auth_provider() -> Any | None:
12+
"""
13+
Create authentication provider based on configuration.
14+
15+
Returns:
16+
Authentication provider instance or None for no authentication
17+
"""
18+
settings = get_settings()
19+
20+
if settings.auth_mode == "none":
21+
logger.info("No authentication configured")
22+
return None
23+
24+
if settings.auth_mode == "bearer":
25+
if not settings.bearer_token:
26+
raise ValueError("Bearer token authentication requires MCP_BEARER_TOKEN to be set")
27+
28+
# Use StaticTokenVerifier for simple bearer token authentication
29+
from fastmcp.server.auth import StaticTokenVerifier
30+
31+
logger.info("Configuring bearer token authentication with StaticTokenVerifier")
32+
33+
# StaticTokenVerifier expects a dict of token -> claims
34+
# For simple bearer auth, we just validate the token exists
35+
tokens = {settings.bearer_token: {"sub": "api-client", "scope": "api:full"}}
36+
return StaticTokenVerifier(tokens=tokens)
37+
38+
if settings.auth_mode == "oauth":
39+
# Note: OAuth proxy requires manual configuration of provider endpoints
40+
# For full OAuth support, users should configure an OIDC provider instead
41+
logger.warning(
42+
"OAuth mode requires manual endpoint configuration. "
43+
"Consider using 'oidc' mode for automatic provider discovery."
44+
)
45+
raise NotImplementedError(
46+
"OAuth mode requires manual endpoint configuration. "
47+
"Use 'oidc' mode for providers with OIDC discovery support, "
48+
"or implement a custom OAuth provider."
49+
)
50+
51+
if settings.auth_mode == "oidc":
52+
# Validate required OIDC configuration
53+
if not settings.oauth_client_id:
54+
raise ValueError("OIDC authentication requires MCP_OAUTH_CLIENT_ID to be set")
55+
if not settings.oauth_client_secret:
56+
raise ValueError("OIDC authentication requires MCP_OAUTH_CLIENT_SECRET to be set")
57+
if not settings.oauth_config_url:
58+
raise ValueError("OIDC authentication requires MCP_OAUTH_CONFIG_URL to be set")
59+
if not settings.oauth_base_url:
60+
raise ValueError("OIDC authentication requires MCP_OAUTH_BASE_URL to be set")
61+
62+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
63+
64+
logger.info(
65+
"Configuring OIDC proxy authentication",
66+
extra={"config_url": settings.oauth_config_url},
67+
)
68+
69+
# Build OIDC proxy configuration
70+
oidc_kwargs = {
71+
"config_url": settings.oauth_config_url,
72+
"client_id": settings.oauth_client_id,
73+
"client_secret": settings.oauth_client_secret,
74+
"base_url": settings.oauth_base_url,
75+
"redirect_path": settings.oauth_redirect_path,
76+
}
77+
78+
# Add optional parameters if configured
79+
if settings.oauth_audience:
80+
oidc_kwargs["audience"] = settings.oauth_audience
81+
82+
if settings.oauth_required_scopes_list:
83+
oidc_kwargs["required_scopes"] = settings.oauth_required_scopes_list
84+
85+
return OIDCProxy(**oidc_kwargs)
86+
87+
raise ValueError(f"Invalid auth_mode: {settings.auth_mode}")

src/mcp_devbench/config/settings.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Settings and configuration management for MCP DevBench."""
22

33
from functools import lru_cache
4-
from typing import List
4+
from typing import List, Literal
55

66
from pydantic import Field
77
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -84,11 +84,79 @@ class Settings(BaseSettings):
8484
description="Interval in seconds for warm container health checks",
8585
)
8686

87+
# Transport configuration
88+
transport_mode: Literal["stdio", "sse", "streamable-http"] = Field(
89+
default="streamable-http",
90+
description="Transport protocol for MCP server (stdio, sse, or streamable-http)",
91+
)
92+
93+
path: str = Field(
94+
default="/mcp",
95+
description="Path for HTTP-based transports (sse or streamable-http)",
96+
)
97+
98+
# Authentication configuration
99+
auth_mode: Literal["none", "bearer", "oauth", "oidc"] = Field(
100+
default="none",
101+
description="Authentication mode (none, bearer, oauth, or oidc)",
102+
)
103+
104+
# Bearer token authentication
105+
bearer_token: str | None = Field(
106+
default=None,
107+
description="Bearer token for bearer authentication mode",
108+
repr=False,
109+
)
110+
111+
# OAuth/OIDC configuration
112+
oauth_client_id: str | None = Field(
113+
default=None,
114+
description="OAuth/OIDC client ID",
115+
)
116+
117+
oauth_client_secret: str | None = Field(
118+
default=None,
119+
description="OAuth/OIDC client secret",
120+
repr=False,
121+
)
122+
123+
oauth_config_url: str | None = Field(
124+
default=None,
125+
description="OAuth provider configuration URL (for OIDC, this is the .well-known URL)",
126+
)
127+
128+
oauth_base_url: str | None = Field(
129+
default=None,
130+
description="Base URL of this server for OAuth callbacks",
131+
)
132+
133+
oauth_redirect_path: str = Field(
134+
default="/auth/callback",
135+
description="OAuth callback redirect path",
136+
)
137+
138+
oauth_audience: str | None = Field(
139+
default=None,
140+
description="OAuth audience parameter (required by some providers like Auth0)",
141+
)
142+
143+
oauth_required_scopes: str = Field(
144+
default="",
145+
description="Comma-separated list of required OAuth scopes",
146+
)
147+
87148
@property
88149
def allowed_registries_list(self) -> List[str]:
89150
"""Parse allowed registries into a list."""
90151
return [r.strip() for r in self.allowed_registries.split(",") if r.strip()]
91152

153+
@property
154+
def oauth_required_scopes_list(self) -> List[str]:
155+
"""Parse OAuth required scopes into a list."""
156+
if not self.oauth_required_scopes:
157+
return []
158+
return [s.strip() for s in self.oauth_required_scopes.split(",") if s.strip()]
159+
92160

93161
@lru_cache
94162
def get_settings() -> Settings:

src/mcp_devbench/server.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from fastmcp import FastMCP
88
from pydantic import BaseModel
99

10+
from mcp_devbench.auth import create_auth_provider
1011
from mcp_devbench.config import get_settings
1112
from mcp_devbench.managers.container_manager import ContainerManager
1213
from mcp_devbench.managers.exec_manager import ExecManager
@@ -71,7 +72,8 @@ class HealthCheckResponse(BaseModel):
7172
version: str = "0.1.0"
7273

7374

74-
# Initialize FastMCP server
75+
# Initialize FastMCP server without authentication initially
76+
# Auth provider will be set in main() after settings are loaded
7577
mcp = FastMCP("MCP DevBench")
7678
logger = get_logger(__name__)
7779

@@ -988,18 +990,48 @@ def main() -> None:
988990
"""Main entry point for the MCP DevBench server."""
989991
settings = get_settings()
990992

993+
# Setup logging first so auth initialization can be properly logged
994+
setup_logging(log_level=settings.log_level, log_format=settings.log_format)
995+
996+
# Initialize authentication provider after settings and logging are configured
997+
auth_provider = create_auth_provider()
998+
999+
# Set the auth provider on the FastMCP server
1000+
mcp.auth = auth_provider
1001+
9911002
logger.info(
9921003
"Starting server",
9931004
extra={
994-
"host": settings.host,
995-
"port": settings.port,
1005+
"transport": settings.transport_mode,
1006+
"auth_mode": settings.auth_mode,
1007+
"host": settings.host if settings.transport_mode != "stdio" else "N/A",
1008+
"port": settings.port if settings.transport_mode != "stdio" else "N/A",
9961009
"allowed_registries": settings.allowed_registries_list,
9971010
},
9981011
)
9991012

10001013
try:
1001-
# Run the FastMCP server with streamable HTTP transport
1002-
mcp.run(transport="streamable", host=settings.host, port=settings.port)
1014+
# Map config transport mode to FastMCP transport parameter
1015+
# FastMCP expects "streamable" for streamable-http mode
1016+
transport_map = {
1017+
"stdio": "stdio",
1018+
"sse": "sse",
1019+
"streamable-http": "streamable",
1020+
}
1021+
1022+
transport = transport_map[settings.transport_mode]
1023+
1024+
# Prepare run kwargs based on transport mode
1025+
run_kwargs = {"transport": transport}
1026+
1027+
# Add HTTP-specific settings for HTTP-based transports
1028+
if settings.transport_mode in ("sse", "streamable-http"):
1029+
run_kwargs["host"] = settings.host
1030+
run_kwargs["port"] = settings.port
1031+
run_kwargs["path"] = settings.path
1032+
1033+
# Run the FastMCP server with configured transport
1034+
mcp.run(**run_kwargs)
10031035
except KeyboardInterrupt:
10041036
logger.info("Received keyboard interrupt, shutting down")
10051037
sys.exit(0)

0 commit comments

Comments
 (0)