Skip to content

refactor: reorg samples #36

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

Merged
merged 1 commit into from
Aug 6, 2025
Merged
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
89 changes: 89 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# MCP Auth sample servers

This sample server folder contains sample servers that demonstrate how to use the MCP Auth Python SDK in various scenarios.

See [the documentation](https://mcp-auth.dev/docs) for the full guide.

## Prerequisites

### Install dependencies

First, install the required dependencies:

```bash
# Install production dependencies
pip install -e .

# Install development dependencies (optional, for development and testing)
pip install -e ".[dev]"
```

### Environment setup

Set up the required environment variable:

```bash
# Set the auth issuer URL
export MCP_AUTH_ISSUER=<your_auth_issuer_url>
```

## Directory Structure

- `current/`: Latest sample implementations (MCP server as resource server)
- `v0_1_1/`: Legacy sample implementations (MCP server as authorization server)

## Get started

### Todo Manager MCP server (current)

The primary example demonstrating how to implement an MCP server as a resource server. This server validates tokens issued by an external authorization server and provides the following tools with scope-based access control:

- `create-todo`: Create a new todo (requires `create:todos` scope)
- `get-todos`: List todos (requires `read:todos` scope for all todos)
- `delete-todo`: Delete a todo (requires `delete:todos` scope for others' todos)

To run the Todo Manager server:

```bash
# Make sure you are in the samples directory first
cd samples

# Start the Todo Manager server
uvicorn current.todo-manager.server:app --host 0.0.0.0 --port 3001
```

## Legacy examples (v0.1.1)

These examples demonstrate the legacy approach where the MCP server acts as an authorization server.

### WhoAmI MCP server (legacy)

A simple server that demonstrates basic authentication. It provides a single tool:

- `whoami`: Returns the authenticated user's information

To run the WhoAmI server:
```bash
# Make sure you are in the samples directory first
cd samples

# Start the WhoAmI server
uvicorn v0_1_1.whoami:app --host 0.0.0.0 --port 3001
```

### Todo Manager MCP server (legacy)

Legacy version of the todo manager that acts as both authorization and resource server. It provides the following tools:

- `create-todo`: Create a new todo (requires `create:todos` scope)
- `get-todos`: List todos (requires `read:todos` scope for all todos)
- `delete-todo`: Delete a todo (requires `delete:todos` scope for others' todos)

To run the legacy Todo Manager server:
```bash
# Make sure you are in the samples directory first
cd samples

# Start the legacy Todo Manager server
uvicorn v0_1_1.todo-manager.server:app --host 0.0.0.0 --port 3001
```
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
)

auth_server_config = fetch_server_config(auth_issuer, AuthServerType.OIDC)
resource_id = "https://todo-manager.mcp-auth.com/resource1"
resource_id = "http://localhost:3001"
mcp_auth = MCPAuth(
protected_resources=[
ResourceServerConfig(
Expand Down
44 changes: 0 additions & 44 deletions samples/server/README.md

This file was deleted.

132 changes: 132 additions & 0 deletions samples/v0_1_1/todo-manager/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
"""
An FastMCP server that provides Todo management tools with authentication and authorization.

This server demonstrates more complex authentication scenarios with different permission scopes:
- create-todo: Create a new todo (requires 'create:todos' scope)
- get-todos: List todos (requires 'read:todos' scope for all todos, otherwise only own todos)
- delete-todo: Delete a todo (requires 'delete:todos' scope for others' todos)

This server is compatible with OpenID Connect (OIDC) providers and uses the `mcpauth` library
to handle authorization. Please check https://mcp-auth.dev/docs/tutorials/todo-manager for more
information on how to use this server.
"""

import os
from typing import Any, List, Optional
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.middleware import Middleware

from mcpauth import MCPAuth
from mcpauth.config import AuthServerType
from mcpauth.exceptions import (
MCPAuthBearerAuthException,
BearerAuthExceptionCode,
)
from mcpauth.types import AuthInfo, ResourceServerConfig, ResourceServerMetadata
from mcpauth.utils import fetch_server_config
from .service import TodoService

# Initialize the FastMCP server
mcp = FastMCP("Todo Manager")

# Initialize the todo service
todo_service = TodoService()

# Authorization server configuration
issuer_placeholder = "https://replace-with-your-issuer-url.com"
auth_issuer = os.getenv("MCP_AUTH_ISSUER", issuer_placeholder)

if auth_issuer == issuer_placeholder:
raise ValueError(
"MCP_AUTH_ISSUER environment variable is not set. Please set it to your authorization server's issuer URL."
)

auth_server_config = fetch_server_config(auth_issuer, AuthServerType.OIDC)
mcp_auth = MCPAuth(server=auth_server_config)

def assert_user_id(auth_info: Optional[AuthInfo]) -> str:
"""Assert that auth_info contains a valid user ID and return it."""
if not auth_info or not auth_info.subject:
raise Exception("Invalid auth info")
return auth_info.subject


def has_required_scopes(user_scopes: List[str], required_scopes: List[str]) -> bool:
"""Check if user has all required scopes."""
return all(scope in user_scopes for scope in required_scopes)


@mcp.tool()
def create_todo(content: str) -> dict[str, Any]:
"""Create a new todo. Requires 'create:todos' scope."""
auth_info = mcp_auth.auth_info
user_id = assert_user_id(auth_info)

# Only users with 'create:todos' scope can create todos
user_scopes = auth_info.scopes if auth_info else []
if not has_required_scopes(user_scopes, ["create:todos"]):
raise MCPAuthBearerAuthException(BearerAuthExceptionCode.MISSING_REQUIRED_SCOPES)

created_todo = todo_service.create_todo(content=content, owner_id=user_id)
return created_todo


@mcp.tool()
def get_todos() -> dict[str, Any]:
"""
List todos. Users with 'read:todos' scope can see all todos,
otherwise they can only see their own todos.
"""
auth_info = mcp_auth.auth_info
user_id = assert_user_id(auth_info)

# If user has 'read:todos' scope, they can access all todos
# If user doesn't have 'read:todos' scope, they can only access their own todos
user_scopes = auth_info.scopes if auth_info else []
todo_owner_id = None if has_required_scopes(user_scopes, ["read:todos"]) else user_id

todos = todo_service.get_all_todos(todo_owner_id)
return {"todos": todos}


@mcp.tool()
def delete_todo(id: str) -> dict[str, Any]:
"""
Delete a todo by id. Users can delete their own todos.
Users with 'delete:todos' scope can delete any todo.
"""
auth_info = mcp_auth.auth_info
user_id = assert_user_id(auth_info)

todo = todo_service.get_todo_by_id(id)

if not todo:
return {"error": "Failed to delete todo"}

# Users can only delete their own todos
# Users with 'delete:todos' scope can delete any todo
user_scopes = auth_info.scopes if auth_info else []
if todo.owner_id != user_id and not has_required_scopes(user_scopes, ["delete:todos"]):
return {"error": "Failed to delete todo"}

deleted_todo = todo_service.delete_todo(id)

if deleted_todo:
return {
"message": f"Todo {id} deleted",
"details": deleted_todo
}
else:
return {"error": "Failed to delete todo"}

# Create the middleware and app
bearer_auth = Middleware(mcp_auth.bearer_auth_middleware('jwt'))
app = Starlette(
routes=[
# Add the metadata route (`/.well-known/oauth-authorization-server`)
mcp_auth.metadata_route(), # pyright: ignore[reportDeprecated]
Mount("/", app=mcp.sse_app(), middleware=[bearer_auth]),
],
)
104 changes: 104 additions & 0 deletions samples/v0_1_1/todo-manager/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
A simple Todo service for demonstration purposes.
Uses an in-memory list to store todos.
"""

from datetime import datetime
from typing import List, Optional, Dict, Any
import random
import string

class Todo:
"""Represents a todo item."""

def __init__(self, id: str, content: str, owner_id: str, created_at: str):
self.id = id
self.content = content
self.owner_id = owner_id
self.created_at = created_at

def to_dict(self) -> Dict[str, Any]:
"""Convert todo to dictionary for JSON serialization."""
return {
"id": self.id,
"content": self.content,
"ownerId": self.owner_id,
"createdAt": self.created_at
}


class TodoService:
"""A simple Todo service for demonstration purposes."""

def __init__(self):
self._todos: List[Todo] = []

def get_all_todos(self, owner_id: Optional[str] = None) -> List[Dict[str, Any]]:
"""
Get all todos, optionally filtered by owner_id.

Args:
owner_id: If provided, only return todos owned by this user

Returns:
List of todo dictionaries
"""
if owner_id:
filtered_todos = [todo for todo in self._todos if todo.owner_id == owner_id]
return [todo.to_dict() for todo in filtered_todos]
return [todo.to_dict() for todo in self._todos]

def get_todo_by_id(self, todo_id: str) -> Optional[Todo]:
"""
Get a todo by its ID.

Args:
todo_id: The ID of the todo to retrieve

Returns:
Todo object if found, None otherwise
"""
for todo in self._todos:
if todo.id == todo_id:
return todo
return None

def create_todo(self, content: str, owner_id: str) -> Dict[str, Any]:
"""
Create a new todo.

Args:
content: The content of the todo
owner_id: The ID of the user who owns this todo

Returns:
Dictionary representation of the created todo
"""
todo = Todo(
id=self._generate_id(),
content=content,
owner_id=owner_id,
created_at=datetime.now().isoformat()
)
self._todos.append(todo)
return todo.to_dict()

def delete_todo(self, todo_id: str) -> Optional[Dict[str, Any]]:
"""
Delete a todo by its ID.

Args:
todo_id: The ID of the todo to delete

Returns:
Dictionary representation of the deleted todo if found, None otherwise
"""
for i, todo in enumerate(self._todos):
if todo.id == todo_id:
deleted_todo = self._todos.pop(i)
return deleted_todo.to_dict()
return None

def _generate_id(self) -> str:
"""Generate a random ID for a todo."""
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
File renamed without changes.