Skip to content

Commit fbcb096

Browse files
committed
Add example proxy OAuth server implementation
Signed-off-by: Jesse Sanford <[email protected]>
1 parent 85039f8 commit fbcb096

File tree

17 files changed

+1668
-22
lines changed

17 files changed

+1668
-22
lines changed

examples/servers/proxy-auth/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# OAuth Proxy Server
2+
3+
This is a minimal OAuth proxy server example for the MCP Python SDK that demonstrates how to create a transparent OAuth proxy for existing OAuth providers.
4+
5+
## Installation
6+
7+
```bash
8+
# Navigate to the proxy-auth directory
9+
cd examples/servers/proxy-auth
10+
11+
# Install the package in development mode
12+
uv add -e .
13+
```
14+
15+
## Configuration
16+
17+
The servers can be configured using either:
18+
19+
1. **Command-line arguments** (take precedence when provided)
20+
2. **Environment variables** (loaded from `.env` file when present)
21+
22+
Example `.env` file:
23+
24+
```env
25+
# Auth Server Configuration
26+
AUTH_SERVER_HOST=localhost
27+
AUTH_SERVER_PORT=9000
28+
AUTH_SERVER_URL=http://localhost:9000
29+
30+
# Resource Server Configuration
31+
RESOURCE_SERVER_HOST=localhost
32+
RESOURCE_SERVER_PORT=8001
33+
RESOURCE_SERVER_URL=http://localhost:8001
34+
35+
# Combo Server Configuration
36+
COMBO_SERVER_HOST=localhost
37+
COMBO_SERVER_PORT=8000
38+
39+
# OAuth Provider Configuration
40+
UPSTREAM_AUTHORIZE=https://github.com/login/oauth/authorize
41+
UPSTREAM_TOKEN=https://github.com/login/oauth/access_token
42+
CLIENT_ID=your-client-id
43+
CLIENT_SECRET=your-client-secret
44+
DEFAULT_SCOPE=openid
45+
```
46+
47+
## Running the Servers
48+
49+
The example consists of three server components that can be run using the project scripts defined in pyproject.toml:
50+
51+
### Step 1: Start Authorization Server
52+
53+
```bash
54+
# Start Authorization Server on port 9000
55+
uv run mcp-proxy-auth-as --port=9000
56+
57+
# Or rely on environment variables from .env file
58+
uv run mcp-proxy-auth-as
59+
```
60+
61+
**What it provides:**
62+
63+
- OAuth 2.0 flows (authorization, token exchange)
64+
- Token introspection endpoint for Resource Servers (`/introspect`)
65+
- Client registration endpoint (`/register`)
66+
67+
### Step 2: Start Resource Server (MCP Server)
68+
69+
```bash
70+
# In another terminal, start Resource Server on port 8001
71+
uv run mcp-proxy-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
72+
73+
# Or rely on environment variables from .env file
74+
uv run mcp-proxy-auth-rs
75+
```
76+
77+
### Step 3: Alternatively, Run Combined Server
78+
79+
For simpler testing, you can run a combined proxy server that handles both authentication and resource access:
80+
81+
```bash
82+
# Run the combined proxy server on port 8000
83+
uv run mcp-proxy-auth-combo --port=8000 --transport=streamable-http
84+
85+
# Or rely on environment variables from .env file
86+
uv run mcp-proxy-auth-combo
87+
```
88+
89+
## How It Works
90+
91+
The proxy OAuth server acts as a transparent proxy between:
92+
93+
1. Client applications requesting OAuth tokens
94+
2. Upstream OAuth providers (like GitHub, Google, etc.)
95+
96+
This allows MCP servers to leverage existing OAuth providers without implementing their own authentication systems.
97+
98+
The server code is organized in the `proxy_auth` package for better modularity.
99+
100+
```text
101+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""OAuth Proxy Server for MCP."""
2+
3+
__version__ = "0.1.0"
4+
5+
# Import key components for easier access
6+
from .auth_server import auth_server as auth_server
7+
from .auth_server import main as auth_server_main
8+
from .combo_server import combo_server as combo_server
9+
from .combo_server import main as combo_server_main
10+
from .resource_server import main as resource_server_main
11+
from .resource_server import resource_server as resource_server
12+
from .token_verifier import IntrospectionTokenVerifier
13+
14+
__all__ = [
15+
"auth_server",
16+
"resource_server",
17+
"combo_server",
18+
"IntrospectionTokenVerifier",
19+
"auth_server_main",
20+
"resource_server_main",
21+
"combo_server_main",
22+
]
23+
24+
# Aliases for the script entry points
25+
main = combo_server_main
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Main entry point for Combo Proxy OAuth Resource+Auth MCP server."""
2+
3+
import sys
4+
5+
from .combo_server import main
6+
7+
sys.exit(main()) # type: ignore[call-arg]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# pyright: reportMissingImports=false
2+
import argparse
3+
import logging
4+
import os
5+
6+
from dotenv import load_dotenv # type: ignore
7+
from mcp.server.auth.providers.transparent_proxy import (
8+
ProxySettings, # type: ignore
9+
TransparentOAuthProxyProvider,
10+
)
11+
from mcp.server.auth.settings import AuthSettings
12+
from mcp.server.fastmcp.server import FastMCP
13+
from pydantic import AnyHttpUrl
14+
15+
# Load environment variables from .env if present
16+
load_dotenv()
17+
18+
# Configure logging after .env so LOG_LEVEL can come from environment
19+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
20+
21+
logging.basicConfig(
22+
level=LOG_LEVEL,
23+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
24+
datefmt="%Y-%m-%d %H:%M:%S",
25+
)
26+
27+
# Dedicated logger for this server module
28+
logger = logging.getLogger("proxy_auth.auth_server")
29+
30+
# Suppress noisy INFO messages from the FastMCP low-level server unless we are
31+
# explicitly running in DEBUG mode. These logs (e.g. "Processing request of type
32+
# ListToolsRequest") are helpful for debugging but clutter normal output.
33+
34+
_mcp_lowlevel_logger = logging.getLogger("mcp.server.lowlevel.server")
35+
if LOG_LEVEL == "DEBUG":
36+
# In full debug mode, allow the library to emit its detailed logs
37+
_mcp_lowlevel_logger.setLevel(logging.DEBUG)
38+
else:
39+
# Otherwise, only warnings and above
40+
_mcp_lowlevel_logger.setLevel(logging.WARNING)
41+
42+
# ----------------------------------------------------------------------------
43+
# Environment configuration
44+
# ----------------------------------------------------------------------------
45+
# Load and validate settings from the environment (uses .env automatically)
46+
settings = ProxySettings.load()
47+
48+
# Upstream endpoints (fully-qualified URLs)
49+
UPSTREAM_AUTHORIZE: str = str(settings.upstream_authorize)
50+
UPSTREAM_TOKEN: str = str(settings.upstream_token)
51+
UPSTREAM_JWKS_URI = settings.jwks_uri
52+
# Derive base URL from the authorize endpoint for convenience / tests
53+
UPSTREAM_BASE: str = UPSTREAM_AUTHORIZE.rsplit("/", 1)[0]
54+
55+
# Client credentials & defaults
56+
CLIENT_ID: str = settings.client_id or "demo-client-id"
57+
CLIENT_SECRET = settings.client_secret
58+
DEFAULT_SCOPE: str = settings.default_scope
59+
60+
# Metadata URL (only used if we need to fetch from upstream)
61+
UPSTREAM_METADATA = f"{UPSTREAM_BASE}/.well-known/oauth-authorization-server"
62+
63+
## Load and validate settings from the environment (uses .env automatically)
64+
settings = ProxySettings.load()
65+
66+
# Server host/port
67+
RESOURCE_SERVER_PORT = int(os.getenv("RESOURCE_SERVER_PORT", "8000"))
68+
RESOURCE_SERVER_HOST = os.getenv("RESOURCE_SERVER_HOST", "localhost")
69+
RESOURCE_SERVER_URL = os.getenv(
70+
"RESOURCE_SERVER_URL", f"http://{RESOURCE_SERVER_HOST}:{RESOURCE_SERVER_PORT}"
71+
)
72+
73+
# Auth server configuration
74+
AUTH_SERVER_PORT = int(os.getenv("AUTH_SERVER_PORT", "9000"))
75+
AUTH_SERVER_HOST = os.getenv("AUTH_SERVER_HOST", "localhost")
76+
AUTH_SERVER_URL = os.getenv(
77+
"AUTH_SERVER_URL", f"http://{AUTH_SERVER_HOST}:{AUTH_SERVER_PORT}"
78+
)
79+
80+
auth_settings = AuthSettings(
81+
issuer_url=AnyHttpUrl(AUTH_SERVER_URL),
82+
resource_server_url=AnyHttpUrl(RESOURCE_SERVER_URL),
83+
required_scopes=["openid"],
84+
)
85+
86+
# Create the OAuth provider with our settings
87+
oauth_provider = TransparentOAuthProxyProvider(
88+
settings=settings, auth_settings=auth_settings
89+
)
90+
91+
92+
# ----------------------------------------------------------------------------
93+
# Auth Server using FastMCP
94+
# ----------------------------------------------------------------------------
95+
def create_auth_server(
96+
host: str = AUTH_SERVER_HOST,
97+
port: int = AUTH_SERVER_PORT,
98+
auth_settings: AuthSettings = auth_settings,
99+
oauth_provider: TransparentOAuthProxyProvider = oauth_provider,
100+
):
101+
"""Create a auth server instance with the given configuration."""
102+
103+
# Create FastMCP resource server instance
104+
auth_server = FastMCP(
105+
name="Auth Server",
106+
host=host,
107+
port=port,
108+
auth_server_provider=oauth_provider,
109+
auth=auth_settings,
110+
)
111+
112+
return auth_server
113+
114+
115+
# Create a default server instance
116+
auth_server = create_auth_server()
117+
118+
119+
def main():
120+
"""Command-line entry point for the Authorization Server."""
121+
parser = argparse.ArgumentParser(description="MCP OAuth Proxy Authorization Server")
122+
parser.add_argument(
123+
"--host",
124+
default=None,
125+
help="Host to bind to (overrides AUTH_SERVER_HOST env var)",
126+
)
127+
parser.add_argument(
128+
"--port",
129+
type=int,
130+
default=None,
131+
help="Port to bind to (overrides AUTH_SERVER_PORT env var)",
132+
)
133+
parser.add_argument(
134+
"--transport",
135+
default="streamable-http",
136+
help="Transport type (streamable-http or websocket)",
137+
)
138+
139+
args = parser.parse_args()
140+
141+
# Use command-line arguments only if provided, otherwise use environment variables
142+
host = args.host or AUTH_SERVER_HOST
143+
port = args.port or AUTH_SERVER_PORT
144+
145+
# Log the configuration being used
146+
logger.info(f"Starting Authorization Server with host={host}, port={port}")
147+
148+
# Create a server with the specified configuration
149+
auth_server = create_auth_server(
150+
host=host, port=port, auth_settings=auth_settings, oauth_provider=oauth_provider
151+
)
152+
153+
logger.info(f"🚀 MCP OAuth Authorization Server running on http://{host}:{port}")
154+
auth_server.run(transport=args.transport)
155+
156+
157+
if __name__ == "__main__":
158+
main()

0 commit comments

Comments
 (0)