Skip to content
Closed
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ dist
docs

#testfile
server.py
setup.py
test.py
test-script.py
Expand Down
7 changes: 7 additions & 0 deletions examples/example-fastmcp-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Auth0 Configuration
Copy link
Copy Markdown
Contributor

@kishore7snehil kishore7snehil Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add comments to .env.example explaining each variable. Something like this:

# Auth0 tenant domain (e.g., your-tenant.us.auth0.com)
AUTH0_DOMAIN=your-tenant.auth0.com

# Auth0 API Identifier - must match the audience in your Auth0 API configuration
AUTH0_AUDIENCE=https://api.example.com

# URL where this MCP server is accessible (used for OAuth metadata)
MCP_SERVER_URL=http://localhost:3001

# Port the server will listen on
PORT=3001

# Enable debug mode for detailed logging
DEBUG=false

# CORS origins - comma-separated list of allowed origins (* for all)
CORS_ORIGINS=*

AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://api.example.com
MCP_SERVER_URL=http://localhost:3001
PORT=3001
DEBUG=false
CORS_ORIGINS=*
43 changes: 43 additions & 0 deletions examples/example-fastmcp-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Example FastMCP MCP Server with Auth0 Integration

This example demonstrates how to create a FastMCP MCP server that uses Auth0 for authentication using the `auth0-api-python` library.

## Install dependencies

```
poetry install
```

## Auth0 Tenant Setup

For detailed instructions on setting up your Auth0 tenant for MCP server integration, please refer to the [Auth0 Tenant Setup guide](https://github.com/auth0/auth0-auth-js/blob/main/examples/example-fastmcp-mcp/README.md#auth0-tenant-setup).
Copy link
Copy Markdown
Contributor

@kishore7snehil kishore7snehil Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add a detailed Auth0 setup section to this README or create a separate docs/AUTH0_SETUP.md file with step-by-step instructions rather than asking the user to visit any other repo.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree to this, redirecting users to a different repo is a sub-optimal developer experience.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is temporary and we are working on a separate doc for setting up tenants for mcp


## Configuration

Rename `.env.example` to `.env` and configure the domain and audience:

```
# Auth0 tenant domain
AUTH0_DOMAIN=example-tenant.us.auth0.com

# Auth0 API Identifier
AUTH0_AUDIENCE=http://localhost:3001
```

With the configuration in place, the example can be started by running:

```bash
poetry run python -m src.server
```

## Testing
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The testing instructions are incomplete. After starting the MCP Inspector, users need to know:

  1. How to configure the Authorization header in the Inspector
  2. How to obtain a test access token
  3. What tools are available and how to call them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think documenting how to use a specific MCP client like MCP inspector is outside the scope of this example. This example is meant to be client agnostic


Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively:

```bash
npx @modelcontextprotocol/inspector
```

The server will start up and the UI will be accessible at http://localhost:6274.

In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL.
1,213 changes: 1,213 additions & 0 deletions examples/example-fastmcp-mcp/poetry.lock

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions examples/example-fastmcp-mcp/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[tool.poetry]
name = "example-fastmcp-mcp"
version = "0.1.0"
description = ""
authors = ["Auth0 <support@auth0.com>"]
readme = "README.md"
package-mode = false

[tool.poetry.dependencies]
python = "^3.10"
python-dotenv = "^1.1.1"
mcp = "^1.14.1"
auth0-api-python = {path = "../..", develop = true}
starlette = "^0.48.0"
uvicorn = "^0.36.0"

[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[dependency-groups]
dev = [
"ruff (>=0.13.1,<0.14.0)"
]
Empty file.
134 changes: 134 additions & 0 deletions examples/example-fastmcp-mcp/src/auth0/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Auth0 integration for MCP server.

This module provides Auth0 authentication and authorization for MCP servers,
including token verification, middleware, and scoped tool decorators.
"""

from __future__ import annotations

import logging
from collections.abc import Callable

from mcp.server.auth.routes import create_protected_resource_routes
from mcp.server.fastmcp import FastMCP
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from starlette.routing import Route, Router

from .errors import AuthenticationRequired, InsufficientScope, MalformedAuthorizationRequest
from .middleware import Auth0Middleware

logger = logging.getLogger(__name__)


class Auth0Mcp:
"""
Auth0 integration for FastMCP servers.

Provides authentication middleware, authorization decorators,
and OAuth2 Protected Resource metadata endpoints for MCP servers.

Args:
name: Human-readable name for the MCP server
audience: Auth0 API identifier (OAuth2 audience claim)
domain: Auth0 tenant domain (e.g., 'tenant.us.auth0.com')

Raises:
RuntimeError: If audience or domain are not provided
"""
def __init__(self, name: str, audience: str, domain: str, mcp_server_url: str | None = None):
self.name = name
self.audience = audience
self.domain = domain
self.mcp_server_url = mcp_server_url
if not self.audience or not self.domain:
raise RuntimeError("audience and domain must be provided")
self.mcp = FastMCP(
name=self.name,
stateless_http=True,
)
self._scopes_supported = {
"openid",
"profile",
"email"
}

def auth_metadata_router(self) -> Router:
"""
Returns a router that serves the OAuth Protected Resource Metadata
at the standard endpoint: /.well-known/oauth-protected-resource
"""
routes: list[Route] = create_protected_resource_routes(
resource_url=self.audience,
authorization_servers=[f"https://{self.domain}"],
scopes_supported=list(self._scopes_supported),
resource_name=self.name,
)

return Router(routes=routes)

def auth_middleware(self) -> list[Middleware]:
return [Middleware(Auth0Middleware, domain=self.domain, audience=self.audience)]

def register_scopes(self, scopes: list[str]) -> None:
"""
Register scopes that tools require.

Args:
scopes: List of scopes to register (e.g., ["tool:greet", "tool:whoami"])
"""
if scopes:
self._scopes_supported.update(scopes)

def exception_handlers(self) -> dict[int | type[Exception], Callable[[Request, Exception], Response]]:
return {
AuthenticationRequired: self._auth_error_handler,
InsufficientScope: self._auth_error_handler,
MalformedAuthorizationRequest: self._auth_error_handler,
# Generic fallback for any other exceptions
Exception: self._generic_exception_handler,
}

def _auth_error_handler(self, request: Request, exc: Exception) -> JSONResponse:
"""
Handle auth errors: malformed authorization requests, missing auth, invalid tokens, and insufficient scopes.
"""
# Include resource metadata parameter for 401 responses per RFC 9728 Section 5.1
include_resource_metadata = exc.status_code == 401

return JSONResponse(
{
"error": exc.error_code,
"error_description": exc.description
},
status_code=exc.status_code,
headers={"WWW-Authenticate": self._build_www_authenticate_header(exc.error_code, exc.description, include_resource_metadata)},
)

def _generic_exception_handler(self, request: Request, exc: Exception) -> JSONResponse:
"""
Fallback handler for all other exceptions.
"""
logger.error(f"Unexpected error in: {exc}", exc_info=exc)

# Return standard HTTP 500 error
return JSONResponse(
{
"error": "internal_server_error",
"error_description": "An unexpected error occurred"
},
status_code=500,
)

def _build_www_authenticate_header(self, error_code: str, description: str, include_resource_metadata: bool = False) -> str:
"""
Build WWW-Authenticate header according to RFC 9728 Section 5.1.
"""
www_auth_params = [f'error="{error_code}"', f'error_description="{description}"']
if include_resource_metadata and self.mcp_server_url:
metadata_url = self.mcp_server_url.rstrip("/") + "/.well-known/oauth-protected-resource"
www_auth_params.append(f'resource_metadata="{metadata_url}"')

return f"Bearer {', '.join(www_auth_params)}"
54 changes: 54 additions & 0 deletions examples/example-fastmcp-mcp/src/auth0/authz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from __future__ import annotations

from collections.abc import Iterable
from functools import wraps

from mcp.server.fastmcp import Context

from . import Auth0Mcp
from .errors import AuthenticationRequired, InsufficientScope

# Collect required scopes from all decorated functions
_scopes_required: set[str] = set()

def require_scopes(required_scopes: Iterable[str]):
"""
Decorator that requires scopes on MCP tools.

Example:
@mcp.tool(...)
@require_scopes(["tool:greet", "tool:whoami"])
async def my_tool(name: str, ctx: Context) -> str:
return f"Hello {name}!"
"""
required_scopes_list = list(required_scopes)

# Collect scopes when decorator is applied
_scopes_required.update(required_scopes_list)

def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
# ctx is passed in either kw or positional
ctx: Context | None = (kwargs.get("ctx") if isinstance(kwargs.get("ctx"), Context) else None) or next((arg for arg in args if isinstance(arg, Context)), None)
if ctx is None:
raise TypeError("ctx: Context is required")

auth = getattr(ctx.request_context.request.state, "auth", {})
if not auth:
raise AuthenticationRequired("Authentication required")

user_scopes = set(auth.get("scopes", []))
missing_scopes = [s for s in required_scopes_list if s not in user_scopes]
if missing_scopes:
raise InsufficientScope(f"Missing required scopes: {missing_scopes}")

return await func(*args, **kwargs)
return wrapper
return decorator

def register_required_scopes(auth0_mcp: Auth0Mcp) -> None:
"""Register all scopes that were collected from @require_scopes decorators."""
if _scopes_required:
auth0_mcp.register_scopes(list(_scopes_required))
_scopes_required.clear()
50 changes: 50 additions & 0 deletions examples/example-fastmcp-mcp/src/auth0/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations


class AuthenticationRequired(Exception):
"""
Raised when authentication is required but missing.

This maps to HTTP 401 Unauthorized status.
Indicates the request lacks valid authentication credentials.
"""
status_code = 401
error_code = "invalid_token"
default_description = "Authentication required"

def __init__(self, message: str | None = None):
self.description = message or self.default_description
super().__init__(self.description)


class InsufficientScope(Exception):
"""
Raised when user lacks required OAuth scopes.

This maps to HTTP 403 Forbidden status.
Indicates the user is authenticated but doesn't have permission
to access the requested resource due to insufficient scopes.
"""
status_code = 403
error_code = "insufficient_scope"
default_description = "Insufficient scope"

def __init__(self, message: str | None = None):
self.description = message or self.default_description
super().__init__(self.description)


class MalformedAuthorizationRequest(Exception):
"""
Raised when authorization request is malformed.

This maps to HTTP 400 Bad Request status.
Indicates the authorization header or token format is invalid.
"""
status_code = 400
error_code = "invalid_request"
default_description = "Malformed authorization request"

def __init__(self, message: str | None = None):
self.description = message or self.default_description
super().__init__(self.description)
Loading