|
| 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