Skip to content

Commit b5e49cf

Browse files
authored
refactor: reorg samples (#36)
1 parent 84d9ff1 commit b5e49cf

File tree

7 files changed

+326
-45
lines changed

7 files changed

+326
-45
lines changed

samples/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# MCP Auth sample servers
2+
3+
This sample server folder contains sample servers that demonstrate how to use the MCP Auth Python SDK in various scenarios.
4+
5+
See [the documentation](https://mcp-auth.dev/docs) for the full guide.
6+
7+
## Prerequisites
8+
9+
### Install dependencies
10+
11+
First, install the required dependencies:
12+
13+
```bash
14+
# Install production dependencies
15+
pip install -e .
16+
17+
# Install development dependencies (optional, for development and testing)
18+
pip install -e ".[dev]"
19+
```
20+
21+
### Environment setup
22+
23+
Set up the required environment variable:
24+
25+
```bash
26+
# Set the auth issuer URL
27+
export MCP_AUTH_ISSUER=<your_auth_issuer_url>
28+
```
29+
30+
## Directory Structure
31+
32+
- `current/`: Latest sample implementations (MCP server as resource server)
33+
- `v0_1_1/`: Legacy sample implementations (MCP server as authorization server)
34+
35+
## Get started
36+
37+
### Todo Manager MCP server (current)
38+
39+
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:
40+
41+
- `create-todo`: Create a new todo (requires `create:todos` scope)
42+
- `get-todos`: List todos (requires `read:todos` scope for all todos)
43+
- `delete-todo`: Delete a todo (requires `delete:todos` scope for others' todos)
44+
45+
To run the Todo Manager server:
46+
47+
```bash
48+
# Make sure you are in the samples directory first
49+
cd samples
50+
51+
# Start the Todo Manager server
52+
uvicorn current.todo-manager.server:app --host 0.0.0.0 --port 3001
53+
```
54+
55+
## Legacy examples (v0.1.1)
56+
57+
These examples demonstrate the legacy approach where the MCP server acts as an authorization server.
58+
59+
### WhoAmI MCP server (legacy)
60+
61+
A simple server that demonstrates basic authentication. It provides a single tool:
62+
63+
- `whoami`: Returns the authenticated user's information
64+
65+
To run the WhoAmI server:
66+
```bash
67+
# Make sure you are in the samples directory first
68+
cd samples
69+
70+
# Start the WhoAmI server
71+
uvicorn v0_1_1.whoami:app --host 0.0.0.0 --port 3001
72+
```
73+
74+
### Todo Manager MCP server (legacy)
75+
76+
Legacy version of the todo manager that acts as both authorization and resource server. It provides the following tools:
77+
78+
- `create-todo`: Create a new todo (requires `create:todos` scope)
79+
- `get-todos`: List todos (requires `read:todos` scope for all todos)
80+
- `delete-todo`: Delete a todo (requires `delete:todos` scope for others' todos)
81+
82+
To run the legacy Todo Manager server:
83+
```bash
84+
# Make sure you are in the samples directory first
85+
cd samples
86+
87+
# Start the legacy Todo Manager server
88+
uvicorn v0_1_1.todo-manager.server:app --host 0.0.0.0 --port 3001
89+
```

samples/server/todo-manager/server.py renamed to samples/current/todo-manager/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
)
4545

4646
auth_server_config = fetch_server_config(auth_issuer, AuthServerType.OIDC)
47-
resource_id = "https://todo-manager.mcp-auth.com/resource1"
47+
resource_id = "http://localhost:3001"
4848
mcp_auth = MCPAuth(
4949
protected_resources=[
5050
ResourceServerConfig(

samples/server/README.md

Lines changed: 0 additions & 44 deletions
This file was deleted.

samples/v0_1_1/todo-manager/server.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""
2+
An FastMCP server that provides Todo management tools with authentication and authorization.
3+
4+
This server demonstrates more complex authentication scenarios with different permission scopes:
5+
- create-todo: Create a new todo (requires 'create:todos' scope)
6+
- get-todos: List todos (requires 'read:todos' scope for all todos, otherwise only own todos)
7+
- delete-todo: Delete a todo (requires 'delete:todos' scope for others' todos)
8+
9+
This server is compatible with OpenID Connect (OIDC) providers and uses the `mcpauth` library
10+
to handle authorization. Please check https://mcp-auth.dev/docs/tutorials/todo-manager for more
11+
information on how to use this server.
12+
"""
13+
14+
import os
15+
from typing import Any, List, Optional
16+
from mcp.server.fastmcp import FastMCP
17+
from starlette.applications import Starlette
18+
from starlette.routing import Mount
19+
from starlette.middleware import Middleware
20+
21+
from mcpauth import MCPAuth
22+
from mcpauth.config import AuthServerType
23+
from mcpauth.exceptions import (
24+
MCPAuthBearerAuthException,
25+
BearerAuthExceptionCode,
26+
)
27+
from mcpauth.types import AuthInfo, ResourceServerConfig, ResourceServerMetadata
28+
from mcpauth.utils import fetch_server_config
29+
from .service import TodoService
30+
31+
# Initialize the FastMCP server
32+
mcp = FastMCP("Todo Manager")
33+
34+
# Initialize the todo service
35+
todo_service = TodoService()
36+
37+
# Authorization server configuration
38+
issuer_placeholder = "https://replace-with-your-issuer-url.com"
39+
auth_issuer = os.getenv("MCP_AUTH_ISSUER", issuer_placeholder)
40+
41+
if auth_issuer == issuer_placeholder:
42+
raise ValueError(
43+
"MCP_AUTH_ISSUER environment variable is not set. Please set it to your authorization server's issuer URL."
44+
)
45+
46+
auth_server_config = fetch_server_config(auth_issuer, AuthServerType.OIDC)
47+
mcp_auth = MCPAuth(server=auth_server_config)
48+
49+
def assert_user_id(auth_info: Optional[AuthInfo]) -> str:
50+
"""Assert that auth_info contains a valid user ID and return it."""
51+
if not auth_info or not auth_info.subject:
52+
raise Exception("Invalid auth info")
53+
return auth_info.subject
54+
55+
56+
def has_required_scopes(user_scopes: List[str], required_scopes: List[str]) -> bool:
57+
"""Check if user has all required scopes."""
58+
return all(scope in user_scopes for scope in required_scopes)
59+
60+
61+
@mcp.tool()
62+
def create_todo(content: str) -> dict[str, Any]:
63+
"""Create a new todo. Requires 'create:todos' scope."""
64+
auth_info = mcp_auth.auth_info
65+
user_id = assert_user_id(auth_info)
66+
67+
# Only users with 'create:todos' scope can create todos
68+
user_scopes = auth_info.scopes if auth_info else []
69+
if not has_required_scopes(user_scopes, ["create:todos"]):
70+
raise MCPAuthBearerAuthException(BearerAuthExceptionCode.MISSING_REQUIRED_SCOPES)
71+
72+
created_todo = todo_service.create_todo(content=content, owner_id=user_id)
73+
return created_todo
74+
75+
76+
@mcp.tool()
77+
def get_todos() -> dict[str, Any]:
78+
"""
79+
List todos. Users with 'read:todos' scope can see all todos,
80+
otherwise they can only see their own todos.
81+
"""
82+
auth_info = mcp_auth.auth_info
83+
user_id = assert_user_id(auth_info)
84+
85+
# If user has 'read:todos' scope, they can access all todos
86+
# If user doesn't have 'read:todos' scope, they can only access their own todos
87+
user_scopes = auth_info.scopes if auth_info else []
88+
todo_owner_id = None if has_required_scopes(user_scopes, ["read:todos"]) else user_id
89+
90+
todos = todo_service.get_all_todos(todo_owner_id)
91+
return {"todos": todos}
92+
93+
94+
@mcp.tool()
95+
def delete_todo(id: str) -> dict[str, Any]:
96+
"""
97+
Delete a todo by id. Users can delete their own todos.
98+
Users with 'delete:todos' scope can delete any todo.
99+
"""
100+
auth_info = mcp_auth.auth_info
101+
user_id = assert_user_id(auth_info)
102+
103+
todo = todo_service.get_todo_by_id(id)
104+
105+
if not todo:
106+
return {"error": "Failed to delete todo"}
107+
108+
# Users can only delete their own todos
109+
# Users with 'delete:todos' scope can delete any todo
110+
user_scopes = auth_info.scopes if auth_info else []
111+
if todo.owner_id != user_id and not has_required_scopes(user_scopes, ["delete:todos"]):
112+
return {"error": "Failed to delete todo"}
113+
114+
deleted_todo = todo_service.delete_todo(id)
115+
116+
if deleted_todo:
117+
return {
118+
"message": f"Todo {id} deleted",
119+
"details": deleted_todo
120+
}
121+
else:
122+
return {"error": "Failed to delete todo"}
123+
124+
# Create the middleware and app
125+
bearer_auth = Middleware(mcp_auth.bearer_auth_middleware('jwt'))
126+
app = Starlette(
127+
routes=[
128+
# Add the metadata route (`/.well-known/oauth-authorization-server`)
129+
mcp_auth.metadata_route(), # pyright: ignore[reportDeprecated]
130+
Mount("/", app=mcp.sse_app(), middleware=[bearer_auth]),
131+
],
132+
)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
A simple Todo service for demonstration purposes.
3+
Uses an in-memory list to store todos.
4+
"""
5+
6+
from datetime import datetime
7+
from typing import List, Optional, Dict, Any
8+
import random
9+
import string
10+
11+
class Todo:
12+
"""Represents a todo item."""
13+
14+
def __init__(self, id: str, content: str, owner_id: str, created_at: str):
15+
self.id = id
16+
self.content = content
17+
self.owner_id = owner_id
18+
self.created_at = created_at
19+
20+
def to_dict(self) -> Dict[str, Any]:
21+
"""Convert todo to dictionary for JSON serialization."""
22+
return {
23+
"id": self.id,
24+
"content": self.content,
25+
"ownerId": self.owner_id,
26+
"createdAt": self.created_at
27+
}
28+
29+
30+
class TodoService:
31+
"""A simple Todo service for demonstration purposes."""
32+
33+
def __init__(self):
34+
self._todos: List[Todo] = []
35+
36+
def get_all_todos(self, owner_id: Optional[str] = None) -> List[Dict[str, Any]]:
37+
"""
38+
Get all todos, optionally filtered by owner_id.
39+
40+
Args:
41+
owner_id: If provided, only return todos owned by this user
42+
43+
Returns:
44+
List of todo dictionaries
45+
"""
46+
if owner_id:
47+
filtered_todos = [todo for todo in self._todos if todo.owner_id == owner_id]
48+
return [todo.to_dict() for todo in filtered_todos]
49+
return [todo.to_dict() for todo in self._todos]
50+
51+
def get_todo_by_id(self, todo_id: str) -> Optional[Todo]:
52+
"""
53+
Get a todo by its ID.
54+
55+
Args:
56+
todo_id: The ID of the todo to retrieve
57+
58+
Returns:
59+
Todo object if found, None otherwise
60+
"""
61+
for todo in self._todos:
62+
if todo.id == todo_id:
63+
return todo
64+
return None
65+
66+
def create_todo(self, content: str, owner_id: str) -> Dict[str, Any]:
67+
"""
68+
Create a new todo.
69+
70+
Args:
71+
content: The content of the todo
72+
owner_id: The ID of the user who owns this todo
73+
74+
Returns:
75+
Dictionary representation of the created todo
76+
"""
77+
todo = Todo(
78+
id=self._generate_id(),
79+
content=content,
80+
owner_id=owner_id,
81+
created_at=datetime.now().isoformat()
82+
)
83+
self._todos.append(todo)
84+
return todo.to_dict()
85+
86+
def delete_todo(self, todo_id: str) -> Optional[Dict[str, Any]]:
87+
"""
88+
Delete a todo by its ID.
89+
90+
Args:
91+
todo_id: The ID of the todo to delete
92+
93+
Returns:
94+
Dictionary representation of the deleted todo if found, None otherwise
95+
"""
96+
for i, todo in enumerate(self._todos):
97+
if todo.id == todo_id:
98+
deleted_todo = self._todos.pop(i)
99+
return deleted_todo.to_dict()
100+
return None
101+
102+
def _generate_id(self) -> str:
103+
"""Generate a random ID for a todo."""
104+
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
File renamed without changes.

0 commit comments

Comments
 (0)