Skip to content

FEAT: enables oauth proxy capability #1075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
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
101 changes: 101 additions & 0 deletions examples/servers/proxy-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# OAuth Proxy Server

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.

## Installation

```bash
# Navigate to the proxy-auth directory
cd examples/servers/proxy-auth

# Install the package in development mode
uv add -e .
```

## Configuration

The servers can be configured using either:

1. **Command-line arguments** (take precedence when provided)
2. **Environment variables** (loaded from `.env` file when present)

Example `.env` file:

```env
# Auth Server Configuration
AUTH_SERVER_HOST=localhost
AUTH_SERVER_PORT=9000
AUTH_SERVER_URL=http://localhost:9000

# Resource Server Configuration
RESOURCE_SERVER_HOST=localhost
RESOURCE_SERVER_PORT=8001
RESOURCE_SERVER_URL=http://localhost:8001

# Combo Server Configuration
COMBO_SERVER_HOST=localhost
COMBO_SERVER_PORT=8000

# OAuth Provider Configuration
UPSTREAM_AUTHORIZE=https://github.com/login/oauth/authorize
UPSTREAM_TOKEN=https://github.com/login/oauth/access_token
CLIENT_ID=your-client-id
CLIENT_SECRET=your-client-secret
DEFAULT_SCOPE=openid
```

## Running the Servers

The example consists of three server components that can be run using the project scripts defined in pyproject.toml:

### Step 1: Start Authorization Server

```bash
# Start Authorization Server on port 9000
uv run mcp-proxy-auth-as --port=9000

# Or rely on environment variables from .env file
uv run mcp-proxy-auth-as
```

**What it provides:**

- OAuth 2.0 flows (authorization, token exchange)
- Token introspection endpoint for Resource Servers (`/introspect`)
- Client registration endpoint (`/register`)

### Step 2: Start Resource Server (MCP Server)

```bash
# In another terminal, start Resource Server on port 8001
uv run mcp-proxy-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http

# Or rely on environment variables from .env file
uv run mcp-proxy-auth-rs
```

### Step 3: Alternatively, Run Combined Server

For simpler testing, you can run a combined proxy server that handles both authentication and resource access:

```bash
# Run the combined proxy server on port 8000
uv run mcp-proxy-auth-combo --port=8000 --transport=streamable-http

# Or rely on environment variables from .env file
uv run mcp-proxy-auth-combo
```

## How It Works

The proxy OAuth server acts as a transparent proxy between:

1. Client applications requesting OAuth tokens
2. Upstream OAuth providers (like GitHub, Google, etc.)

This allows MCP servers to leverage existing OAuth providers without implementing their own authentication systems.

The server code is organized in the `proxy_auth` package for better modularity.

```text
```
25 changes: 25 additions & 0 deletions examples/servers/proxy-auth/proxy_auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""OAuth Proxy Server for MCP."""

__version__ = "0.1.0"

# Import key components for easier access
from .auth_server import auth_server as auth_server
from .auth_server import main as auth_server_main
from .combo_server import combo_server as combo_server
from .combo_server import main as combo_server_main
from .resource_server import main as resource_server_main
from .resource_server import resource_server as resource_server
from .token_verifier import IntrospectionTokenVerifier

__all__ = [
"auth_server",
"resource_server",
"combo_server",
"IntrospectionTokenVerifier",
"auth_server_main",
"resource_server_main",
"combo_server_main",
]

# Aliases for the script entry points
main = combo_server_main
7 changes: 7 additions & 0 deletions examples/servers/proxy-auth/proxy_auth/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Main entry point for Combo Proxy OAuth Resource+Auth MCP server."""

import sys

from .combo_server import main

sys.exit(main()) # type: ignore[call-arg]
158 changes: 158 additions & 0 deletions examples/servers/proxy-auth/proxy_auth/auth_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# pyright: reportMissingImports=false
import argparse
import logging
import os

from dotenv import load_dotenv # type: ignore
from mcp.server.auth.providers.transparent_proxy import (
ProxySettings, # type: ignore
TransparentOAuthProxyProvider,
)
from mcp.server.auth.settings import AuthSettings
from mcp.server.fastmcp.server import FastMCP
from pydantic import AnyHttpUrl

# Load environment variables from .env if present
load_dotenv()

# Configure logging after .env so LOG_LEVEL can come from environment
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()

logging.basicConfig(
level=LOG_LEVEL,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)

# Dedicated logger for this server module
logger = logging.getLogger("proxy_auth.auth_server")

# Suppress noisy INFO messages from the FastMCP low-level server unless we are
# explicitly running in DEBUG mode. These logs (e.g. "Processing request of type
# ListToolsRequest") are helpful for debugging but clutter normal output.

_mcp_lowlevel_logger = logging.getLogger("mcp.server.lowlevel.server")
if LOG_LEVEL == "DEBUG":
# In full debug mode, allow the library to emit its detailed logs
_mcp_lowlevel_logger.setLevel(logging.DEBUG)
else:
# Otherwise, only warnings and above
_mcp_lowlevel_logger.setLevel(logging.WARNING)

# ----------------------------------------------------------------------------
# Environment configuration
# ----------------------------------------------------------------------------
# Load and validate settings from the environment (uses .env automatically)
settings = ProxySettings.load()

# Upstream endpoints (fully-qualified URLs)
UPSTREAM_AUTHORIZE: str = str(settings.upstream_authorize)
UPSTREAM_TOKEN: str = str(settings.upstream_token)
UPSTREAM_JWKS_URI = settings.jwks_uri
# Derive base URL from the authorize endpoint for convenience / tests
UPSTREAM_BASE: str = UPSTREAM_AUTHORIZE.rsplit("/", 1)[0]

# Client credentials & defaults
CLIENT_ID: str = settings.client_id or "demo-client-id"
CLIENT_SECRET = settings.client_secret
DEFAULT_SCOPE: str = settings.default_scope

# Metadata URL (only used if we need to fetch from upstream)
UPSTREAM_METADATA = f"{UPSTREAM_BASE}/.well-known/oauth-authorization-server"

## Load and validate settings from the environment (uses .env automatically)
settings = ProxySettings.load()

# Server host/port
RESOURCE_SERVER_PORT = int(os.getenv("RESOURCE_SERVER_PORT", "8000"))
RESOURCE_SERVER_HOST = os.getenv("RESOURCE_SERVER_HOST", "localhost")
RESOURCE_SERVER_URL = os.getenv(
"RESOURCE_SERVER_URL", f"http://{RESOURCE_SERVER_HOST}:{RESOURCE_SERVER_PORT}"
)

# Auth server configuration
AUTH_SERVER_PORT = int(os.getenv("AUTH_SERVER_PORT", "9000"))
AUTH_SERVER_HOST = os.getenv("AUTH_SERVER_HOST", "localhost")
AUTH_SERVER_URL = os.getenv(
"AUTH_SERVER_URL", f"http://{AUTH_SERVER_HOST}:{AUTH_SERVER_PORT}"
)

auth_settings = AuthSettings(
issuer_url=AnyHttpUrl(AUTH_SERVER_URL),
resource_server_url=AnyHttpUrl(RESOURCE_SERVER_URL),
required_scopes=["openid"],
)

# Create the OAuth provider with our settings
oauth_provider = TransparentOAuthProxyProvider(
settings=settings, auth_settings=auth_settings
)


# ----------------------------------------------------------------------------
# Auth Server using FastMCP
# ----------------------------------------------------------------------------
def create_auth_server(
host: str = AUTH_SERVER_HOST,
port: int = AUTH_SERVER_PORT,
auth_settings: AuthSettings = auth_settings,
oauth_provider: TransparentOAuthProxyProvider = oauth_provider,
):
"""Create a auth server instance with the given configuration."""

# Create FastMCP resource server instance
auth_server = FastMCP(
name="Auth Server",
host=host,
port=port,
auth_server_provider=oauth_provider,
auth=auth_settings,
)

return auth_server


# Create a default server instance
auth_server = create_auth_server()


def main():
"""Command-line entry point for the Authorization Server."""
parser = argparse.ArgumentParser(description="MCP OAuth Proxy Authorization Server")
parser.add_argument(
"--host",
default=None,
help="Host to bind to (overrides AUTH_SERVER_HOST env var)",
)
parser.add_argument(
"--port",
type=int,
default=None,
help="Port to bind to (overrides AUTH_SERVER_PORT env var)",
)
parser.add_argument(
"--transport",
default="streamable-http",
help="Transport type (streamable-http or websocket)",
)

args = parser.parse_args()

# Use command-line arguments only if provided, otherwise use environment variables
host = args.host or AUTH_SERVER_HOST
port = args.port or AUTH_SERVER_PORT

# Log the configuration being used
logger.info(f"Starting Authorization Server with host={host}, port={port}")

# Create a server with the specified configuration
auth_server = create_auth_server(
host=host, port=port, auth_settings=auth_settings, oauth_provider=oauth_provider
)

logger.info(f"🚀 MCP OAuth Authorization Server running on http://{host}:{port}")
auth_server.run(transport=args.transport)


if __name__ == "__main__":
main()
Loading
Loading