Skip to content

Commit e06ed87

Browse files
authored
fix: documentation and tools (#8)
- correct documentation info - separate tools in files - add mcp inspector - add context class to share jwt - upgrade development doc
1 parent 5c88ebf commit e06ed87

15 files changed

Lines changed: 161 additions & 251 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323

2424
### Technical Details
2525

26-
- Python 3.10+ support
26+
- Python 3.11+ support
2727
- FastAPI-based HTTP server with SSE
2828
- JWT validation with local/external strategies
2929
- TOML configuration

DEVELOPMENT.md

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,46 +32,12 @@ config.toml # Configuration example
3232

3333
The current tools (`hello_world`, `whoami`) are examples. Here's how to replace them:
3434

35-
### 1. Modify Tools
36-
37-
Edit `src/mcp_app/tools/router.py`:
38-
39-
```python
40-
from mcp import Tool
41-
from mcp.server import Server
42-
43-
# Remove demo imports
44-
# from mcp_app.tools.hello_world import hello_world_tool
45-
# from mcp_app.tools.whoami import whoami_tool
46-
47-
# Add your tools
48-
from my_app.tools.my_tool import my_tool_function
49-
50-
def register_tools(server: Server) -> None:
51-
"""Register MCP tools with the server."""
52-
53-
# Remove demo tools
54-
# server.register_tool(hello_world_tool)
55-
# server.register_tool(whoami_tool)
56-
57-
# Register your tools
58-
server.register_tool(my_tool_function)
59-
```
60-
61-
### 2. Create Your Tools
35+
### 1. Create Your Tools
6236

6337
Create `src/mcp_app/tools/my_tools.py`:
6438

6539
```python
66-
from mcp import Tool
67-
from mcp.server import Server
68-
from mcp_app.config import Configuration
69-
70-
# Get config if needed
71-
config = None # Will be set during app startup
72-
73-
@server.tool()
74-
async def my_business_logic_tool(param1: str, param2: int) -> dict:
40+
def my_business_logic_tool(param1: str, param2: int) -> dict:
7541
"""Execute your business logic.
7642
7743
Args:
@@ -89,6 +55,31 @@ async def my_business_logic_tool(param1: str, param2: int) -> dict:
8955
return result
9056
```
9157

58+
#### Register Your Tools
59+
60+
Edit `src/mcp_app/tools/router.py`:
61+
62+
```python
63+
from mcp.server import FastMCP
64+
65+
# Remove demo imports
66+
# from mcp_app.tools.hello_world import hello_world
67+
# from mcp_app.tools.whoami import whoami
68+
69+
# Add your tools
70+
from my_app.tools.my_tools import my_business_logic_tool
71+
72+
def register_tools(mcp: FastMCP) -> None:
73+
"""Register MCP tools with the server."""
74+
75+
# Remove demo tools
76+
# mcp.tool()(hello_world)
77+
# mcp.tool()(whoami)
78+
79+
# Register your tools
80+
mcp.tool()(my_business_logic_tool)
81+
```
82+
9283
### 3. Update Tests
9384

9485
Modify `tests/test_tools.py` to test your new tools instead of the demo ones.
@@ -97,6 +88,33 @@ Modify `tests/test_tools.py` to test your new tools instead of the demo ones.
9788

9889
Update README.md and DEVELOPMENT.md to document your tools instead of the demo ones.
9990

91+
## JWT Validation Configuration
92+
93+
The project includes JWT validation middleware for securing tools. By default, it's configured for local validation using a JWKS endpoint.
94+
95+
### Using Keycloak
96+
97+
To enable JWT validation with Keycloak:
98+
99+
1. **Run Keycloak locally** (e.g., via Docker: `docker run -p 8080:8080 quay.io/keycloak/keycloak:latest start-dev`).
100+
2. **Create a realm and client** in Keycloak admin console.
101+
3. **Update `config.toml`**:
102+
- Set `jwks_uri = "http://localhost:8080/realms/your-realm/protocol/openid-connect/certs"`
103+
- Adjust `allow_conditions` to match your email domain, e.g., `payload.email.endswith("@yourdomain.com")`
104+
4. **Enable OAuth endpoints** if needed by setting `oauth_authorization_server.enabled = true` and `oauth_protected_resource.enabled = true`, updating issuer_uri and auth_servers accordingly.
105+
106+
### Using Auth0
107+
108+
To use Auth0 as your identity provider:
109+
110+
1. **Get your Auth0 tenant details** (tenant name, client ID, etc.).
111+
2. **Update `config.toml`**:
112+
- Set `jwks_uri = "https://your-tenant.auth0.com/.well-known/jwks.json"`
113+
- Set `allow_conditions` to validate claims, e.g., `payload.iss == "https://your-tenant.auth0.com/" and payload.aud == "your-client-id"`
114+
3. **Ensure Auth0 is configured** to issue JWTs with the required claims.
115+
116+
For external validation (e.g., via a proxy), set `strategy = "external"` and configure your proxy to forward validated JWTs in the `X-Validated-Jwt` header.
117+
100118
## Configuration Placeholders
101119

102120
Before using this template, you must replace all placeholders with your actual values:

Justfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,8 @@ run:
8181
[group('run')]
8282
run-stdio:
8383
uv run stdio
84+
85+
# Run MCP Inspector
86+
[group('run')]
87+
dev:
88+
bunx --yes @modelcontextprotocol/inspector uv run stdio

README-es.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,3 @@ Este proyecto está licenciado bajo Unlicense - consulta el archivo [LICENSE](LI
183183
## Créditos
184184

185185
Traducción completa a Python del proyecto [MCP Forge](https://github.com/achetronic/mcp-forge) (Go), manteniendo todas las funcionalidades y nivel de seguridad del original.
186-
187-
```
188-
189-
```

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,3 @@ This project is licensed under the Unlicense - see the [LICENSE](LICENSE) file f
185185
## Credits
186186

187187
Complete translation to Python of the [MCP Forge](https://github.com/achetronic/mcp-forge) project (Go), maintaining all functionalities and security level of the original.
188-
189-
```
190-
191-
```

docs/_config.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ plugins:
55

66
title: "MCP Forge Python - Production-Ready MCP Server with OAuth"
77
description: "A comprehensive MCP (Model Context Protocol) server template with OAuth support, JWT validation, and production-ready deployment options for Python developers."
8-
author: "Ruben"
8+
author: "bercianor"
99
url: "https://bercianor.es/mcp-forge-python"
1010
social:
1111
name: MCP Forge Python
1212
links:
1313
- https://github.com/bercianor
1414
- https://github.com/bercianor/mcp-forge-python
15-

src/mcp_app/context.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
Shared context for authentication data.
3+
4+
This module provides context variables to share JWT-related data between
5+
middlewares and MCP tools in an async-safe way.
6+
"""
7+
8+
from contextvars import ContextVar
9+
from typing import Any
10+
11+
# Context variables for JWT data
12+
jwt_token: ContextVar[str | None] = ContextVar("jwt_token", default=None)
13+
jwt_payload: ContextVar[dict[str, Any] | None] = ContextVar("jwt_payload", default=None)
14+
15+
16+
def set_jwt_context(token: str, payload: dict[str, Any]) -> None:
17+
"""
18+
Set the JWT token and its decoded payload.
19+
20+
Args:
21+
token: The validated JWT token string.
22+
payload: The decoded JWT payload.
23+
24+
"""
25+
jwt_token.set(token)
26+
jwt_payload.set(payload)
27+
28+
29+
def get_jwt_payload() -> dict[str, Any] | None:
30+
"""
31+
Get the decoded JWT payload.
32+
33+
Returns:
34+
The JWT payload as a dictionary, or None if not set or decoding failed.
35+
36+
"""
37+
return jwt_payload.get()

src/mcp_app/middlewares/jwt_validation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from starlette.middleware.base import BaseHTTPMiddleware
1616
from starlette.types import ASGIApp
1717

18+
from mcp_app.context import set_jwt_context
19+
1820
logger = logging.getLogger(__name__)
1921

2022

@@ -147,6 +149,9 @@ async def _validate_local(self, request: Request) -> None:
147149
# Set forwarded header
148150
request.headers.__dict__["_list"].append((self.forwarded_header.encode(), token.encode()))
149151

152+
# Set JWT in shared context for MCP tools
153+
set_jwt_context(token, payload)
154+
150155
def _check_condition(self, condition: str, payload: dict[str, Any]) -> bool:
151156
"""Check simple CEL-like conditions."""
152157
# Basic implementation for common patterns

src/mcp_app/tools/hello_world.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Tool to say hello."""
2+
3+
4+
def hello_world(name: str) -> str:
5+
"""
6+
Say hello to someone.
7+
8+
Args:
9+
name: Name of the person to greet.
10+
11+
Returns:
12+
Greeting message.
13+
14+
"""
15+
return f"Hello, {name}! 👋"

src/mcp_app/tools/router.py

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,8 @@
66

77
from mcp.server import FastMCP
88

9-
10-
def hello_world(name: str) -> str:
11-
"""
12-
Say hello to someone.
13-
14-
Args:
15-
name: Name of the person to greet.
16-
17-
Returns:
18-
Greeting message.
19-
20-
"""
21-
return f"Hello, {name}! 👋"
22-
23-
24-
def whoami(jwt: str | None = None) -> str:
25-
"""
26-
Expose information about the user.
27-
28-
Args:
29-
jwt: Validated JWT from middleware.
30-
31-
Returns:
32-
User information message.
33-
34-
"""
35-
if not jwt:
36-
return "JWT is empty. Information is not available"
37-
38-
return f"Success! Data are in the following JWT. You have to decode it first. JWT: {jwt}"
9+
from mcp_app.tools.hello_world import hello_world
10+
from mcp_app.tools.whoami import whoami
3911

4012

4113
def register_tools(mcp: FastMCP) -> None:

0 commit comments

Comments
 (0)