Skip to content

Commit 42f95ae

Browse files
authored
feat: add roles and scopes to restrict tools (#12)
1 parent 42e0024 commit 42f95ae

13 files changed

Lines changed: 440 additions & 70 deletions

File tree

CONTRIBUTING.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ Types:
8787
src/mcp_app/
8888
├── main.py # Application entry point
8989
├── config.py # Configuration models
90-
├── handlers/ # OAuth handlers
90+
├── context.py # JWT context management
91+
├── handlers/ # OAuth endpoints handlers
9192
├── middlewares/ # Custom middlewares
92-
└── tools/ # MCP tools
93+
└── tools/ # MCP tools and router
9394
9495
tests/ # Test files
9596
chart/ # Kubernetes Helm chart
@@ -99,16 +100,15 @@ chart/ # Kubernetes Helm chart
99100

100101
To add a new MCP tool:
101102

102-
1. Create the tool function in `src/mcp_app/tools/router.py`
103-
2. Register it in the `register_tools()` function
103+
1. Create the tool function in a new file under `src/mcp_app/tools/` (e.g., `my_tool.py`)
104+
2. Import and register it in `src/mcp_app/tools/router.py`
104105
3. Add tests in `tests/test_tools.py`
105106
4. Update documentation
106107

107108
Example tool:
108109

109110
```python
110-
@mcp.tool()
111-
async def my_tool(param: str) -> str:
111+
def my_tool(param: str) -> str:
112112
"""Tool description.
113113
114114
Args:
@@ -120,6 +120,16 @@ async def my_tool(param: str) -> str:
120120
return f"Processed: {param}"
121121
```
122122

123+
Then register it in `router.py`:
124+
125+
```python
126+
from mcp_app.tools.my_tool import my_tool
127+
128+
def register_tools(mcp: FastMCP) -> None:
129+
mcp.tool()(my_tool)
130+
# ... other tools
131+
```
132+
123133
## Pull Request Process
124134

125135
1. **Fork** the repository

DEVELOPMENT.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,64 @@ Example in `config.toml`:
129129
jwt_exposed_claims = ["user_id", "roles"]
130130
```
131131

132+
### Roles and Scopes for Tool Authorization
133+
134+
This project implements a basic role-based access control (RBAC) system using OAuth scopes to restrict tool execution. Scopes are issued by the identity provider (e.g., Keycloak, Auth0, or other OAuth providers) and validated in tools at runtime.
135+
136+
#### Defining Your Own Scopes and Roles
137+
138+
You can define custom scopes and roles based on your application's needs. Scopes control access to specific tools or features, while roles group permissions.
139+
140+
**Example Scopes** (define in your OAuth provider's API/client configuration):
141+
- `tool:read`: Access to read-only tools.
142+
- `tool:write`: Access to tools that modify data.
143+
- `tool:admin`: Administrative access to sensitive tools.
144+
145+
**Example Roles** (define in your OAuth provider's user/role management):
146+
- `user`: Basic user role; grants `tool:read` scope.
147+
- `editor`: Editor role; grants `tool:read` and `tool:write` scopes.
148+
- `admin`: Admin role; grants all scopes.
149+
150+
Scopes and roles are included in JWT claims (`scope` and `roles`) via your OAuth provider's rules/actions/policies. Configure your provider to map roles to scopes and include them in tokens.
151+
152+
#### Checking Scopes in Tools
153+
154+
To restrict a tool based on scopes, add authorization checks inside the tool function. Use `get_jwt_payload()` from `mcp_app.context` to access claims.
155+
156+
Example for a tool requiring `tool:user` scope:
157+
158+
```python
159+
from mcp_app.context import get_jwt_payload
160+
161+
def my_tool(param: str) -> str:
162+
"""My tool description. Requires tool:user scope.
163+
164+
Args:
165+
param: Parameter description.
166+
167+
Returns:
168+
Result description.
169+
170+
Raises:
171+
PermissionError: If user lacks required scope.
172+
"""
173+
payload = get_jwt_payload()
174+
if payload: # Only check in HTTP mode (with JWT)
175+
scope = payload.get("scope", "")
176+
scopes = scope.split() if isinstance(scope, str) else scope or []
177+
if "tool:user" not in scopes:
178+
raise PermissionError("Insufficient permissions: tool:user scope required")
179+
180+
# Tool logic here
181+
return f"Processed: {param}"
182+
```
183+
184+
- **In HTTP mode**: Validates scopes from JWT; raises `PermissionError` if unauthorized.
185+
- **In stdio mode**: Allows execution without checks (for development).
186+
- **Always include scope checks** for sensitive tools to prevent unauthorized access.
187+
188+
Configure your OAuth provider to issue tokens with the appropriate scopes based on user roles.
189+
132190
## Configuration Placeholders
133191

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

README.md

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[![Contributors](https://img.shields.io/github/contributors/bercianor/mcp-forge-python)](https://github.com/bercianor/mcp-forge-python/graphs/contributors)
1010
[![Last Commit](https://img.shields.io/github/last-commit/bercianor/mcp-forge-python)](https://github.com/bercianor/mcp-forge-python)
1111

12-
A comprehensive, production-ready MCP (Model Context Protocol) server template built with Python, featuring OAuth 2.0 authentication, JWT validation, and seamless deployment options for developers building AI-powered applications.
12+
A comprehensive, production-ready MCP (Model Context Protocol) server template built with Python. This is a Python port of the original MCP Forge Go project, featuring OAuth 2.0 authentication, JWT validation, and seamless deployment options for developers building AI-powered applications.
1313

1414
## Key Features of MCP Forge Python
1515

@@ -26,27 +26,43 @@ A comprehensive, production-ready MCP (Model Context Protocol) server template b
2626

2727
## Built-in MCP Tools
2828

29-
- **hello_world**: Personalized greeting functionality
30-
- **whoami**: JWT-based user information exposure
29+
The server includes several MCP tools registered via the router:
30+
31+
- **hello_world**: Personalized greeting functionality (requires `tool:user` scope in HTTP mode)
32+
- **whoami**: JWT-based user information exposure from the JWT payload
3133

3234
## Security & Middleware
3335

3436
- **Access Logging**: Configurable request logging with header redaction
3537
- **JWT Validation**: Dual strategies for token authentication
3638
- Local validation using JWKS URI and CEL expressions
3739
- External proxy delegation (Istio-compatible)
40+
- **CORS**: Configurable Cross-Origin Resource Sharing
41+
- **JWT Context**: Secure sharing of JWT claims between middlewares and MCP tools
3842

3943
## OAuth 2.0 Integration (RFC 8414 & RFC 9728)
4044

4145
- **OAuth Authorization Server**: OpenID Connect configuration proxy
4246
- **Protected Resource Metadata**: Complete OAuth resource discovery endpoints
47+
- **OAuth Flows**: Built-in login and callback endpoints for authorization code flow
4348

4449
## Flexible Configuration
4550

4651
- TOML-based configuration system with dedicated sections for:
4752
- Server settings (name, version, transport)
48-
- Middleware configuration (logging, JWT)
53+
- Middleware configuration (logging, JWT, CORS)
4954
- OAuth integration (authorization servers, protected resources)
55+
- Auth settings for OAuth flows (client credentials, redirect URIs)
56+
- JWT claims exposure configuration
57+
58+
## Additional Endpoints
59+
60+
Beyond MCP protocol endpoints, the server provides:
61+
62+
- **GET /**: Server information
63+
- **GET /health**: Health check endpoint
64+
- **GET /login**: Initiates OAuth authorization code flow (redirects to Auth0/Keycloak)
65+
- **GET /callback**: Handles OAuth callback and exchanges code for token
5066

5167
## Production-Ready Deployment
5268

@@ -58,7 +74,7 @@ A comprehensive, production-ready MCP (Model Context Protocol) server template b
5874

5975
### External Dependencies
6076

61-
- **Python**: >= 3.10
77+
- **Python**: >= 3.11
6278
- **uv**: Dependency and virtual environment manager (install from [astral.sh/uv](https://astral.sh/uv))
6379
- **just** (optional): Simplified command runner (install from [just.systems](https://just.systems/install.sh))
6480
- **Docker** (optional): For image building
@@ -81,12 +97,25 @@ A comprehensive, production-ready MCP (Model Context Protocol) server template b
8197
- `PyJWT`: JWT handling
8298
- `requests`: Synchronous HTTP client
8399

100+
- **Production dependencies**:
101+
- `fastapi`: ASGI web framework
102+
- `uvicorn[standard]`: ASGI server with SSE support
103+
- `pydantic`: Data validation
104+
- `pydantic-settings`: Configuration from files
105+
- `tomli`: TOML parser for Python < 3.11 (uses `tomllib` for Python 3.11+)
106+
- `mcp[cli]`: MCP Python SDK
107+
- `httpx`: Asynchronous HTTP client
108+
- `PyJWT`: JWT handling
109+
- `requests`: Synchronous HTTP client
110+
- `cryptography`: Cryptographic operations
111+
84112
- **Development dependencies**:
85113
- `ruff`: Linting and formatting
86114
- `pyright`: Type checking
87115
- `pytest`: Testing framework
88116
- `pytest-asyncio`: Async support for pytest
89117
- `coverage`: Code coverage
118+
- `pytest-benchmark`: Performance benchmarking
90119

91120
## Installation & Setup
92121

@@ -95,10 +124,13 @@ A comprehensive, production-ready MCP (Model Context Protocol) server template b
95124
uv sync
96125

97126
# Install package (enables direct commands)
98-
uv pip install .
127+
uv pip install -e .
99128

100129
# Run HTTP server with SSE
101130
uv run http
131+
132+
# Alternative: Run stdio server for local AI clients
133+
uv run stdio
102134
```
103135

104136
## Development Commands
@@ -107,24 +139,23 @@ uv run http
107139
# Testing & Quality
108140
just test # Run all tests
109141
just cov # Run tests with coverage report
142+
just bench # Run benchmarks
110143
just lint # Lint and format code
111144
just typing # Type checking
112145
just check-all # Run all quality checks
113146

114147
# Lifecycle
115148
just install # Install/update dependencies
149+
just update # Update dependencies to latest versions
116150
just clean # Remove all temporary files (.venv, caches, dist)
117151
just clean-cache # Clean caches only (keep .venv)
118152
just fresh # Clean + fresh install
119153

120154
# Running
121155
just run # Run HTTP server
122156
just run-stdio # Run stdio mode
123-
124-
# Run stdio server
125-
126-
uv run stdio
127-
157+
just dev-http # Run HTTP server with MCP Inspector
158+
just dev-stdio # Run stdio server with MCP Inspector
128159
```
129160

130161
> **Note**: For development, use `uv pip install -e .` for editable installation.
@@ -143,6 +174,7 @@ The JWT middleware supports two validation strategies:
143174
- Validates JWTs directly in the MCP server.
144175
- Downloads public keys from a JWKS endpoint (configured in `jwks_uri`).
145176
- Supports configurable cache and CEL conditions for advanced permissions.
177+
- MCP tools check for required scopes (e.g., `tool:user` for hello_world).
146178
- **Requirement**: OAuth server with JWKS endpoint (e.g. Keycloak).
147179

148180
### "external" Strategy
@@ -158,23 +190,61 @@ Example in `config.toml`.
158190

159191
See `config.toml` for configuration example.
160192

161-
**Security Note**: By default, the server runs on `127.0.0.1` to avoid unwanted exposures. Change to `0.0.0.0` only if necessary and with appropriate security measures.
193+
### Auth Configuration
194+
195+
For OAuth flows, configure the auth section:
196+
197+
```toml
198+
[auth]
199+
client_id = "your-client-id"
200+
client_secret = "your-client-secret"
201+
redirect_uri = "http://localhost:8080/callback"
202+
```
203+
204+
### CORS Configuration
205+
206+
Configure Cross-Origin Resource Sharing:
207+
208+
```toml
209+
[middleware.cors]
210+
allow_origins = ["https://yourdomain.com", "http://localhost:3000"]
211+
allow_credentials = true
212+
allow_methods = ["GET", "POST", "PUT", "DELETE"]
213+
allow_headers = ["*"]
214+
```
215+
216+
### JWT Claims Exposure
217+
218+
Control which JWT claims are accessible to MCP tools:
219+
220+
```toml
221+
# Expose all claims (not recommended for production)
222+
jwt_exposed_claims = "all"
223+
224+
# Or expose only specific claims
225+
jwt_exposed_claims = ["user_id", "email", "roles"]
226+
```
227+
228+
Note: `roles` and `scope` claims are always exposed for authorization purposes.
229+
230+
**Security Note**: By default, the server runs on `127.0.0.1` to avoid unwanted exposures. Configure the host and port in `config.toml` under `[server.transport.http]`. Change to `0.0.0.0` only if necessary and with appropriate security measures.
162231

163232
## Security Considerations
164233

165234
This template implements several security measures to protect against common vulnerabilities. As a template, it's designed to be configurable for different deployment scenarios.
166235

167236
### JWT Claims Exposure
168237

169-
To minimize data exposure, configure which JWT claims are accessible in the context:
238+
To minimize data exposure, configure which JWT claims are accessible to MCP tools:
170239

171240
```toml
172-
[context]
173241
jwt_exposed_claims = ["user_id", "roles"] # Only expose specific claims
174242
# or
175243
jwt_exposed_claims = "all" # Expose all claims (not recommended for production)
176244
```
177245

246+
Note: `roles` and `scope` claims are always exposed for authorization purposes, regardless of this configuration.
247+
178248
### Access Logging
179249

180250
Sensitive headers are automatically redacted in logs:
@@ -212,6 +282,27 @@ Dependencies are regularly updated to address known vulnerabilities. Run `uv loc
212282
- [Development Guide](DEVELOPMENT.md) - How to use this as a template.
213283
- [Contributing](CONTRIBUTING.md) - Guidelines for contributors.
214284

285+
## Project Architecture
286+
287+
```
288+
src/mcp_app/
289+
├── main.py # Application entry point and FastAPI setup
290+
├── config.py # Pydantic configuration models
291+
├── context.py # JWT context management for secure claim sharing
292+
├── handlers/ # OAuth endpoints handlers (RFC 8414 & RFC 9728)
293+
├── middlewares/ # Custom middlewares (JWT, access logs, CORS)
294+
└── tools/ # MCP tools and registration router
295+
```
296+
297+
### Core Components
298+
299+
- **main.py**: Initializes FastMCP server, FastAPI app, middlewares, and OAuth endpoints
300+
- **config.py**: TOML-based configuration with Pydantic models
301+
- **context.py**: Async-safe JWT context sharing between middlewares and tools
302+
- **handlers/**: OAuth authorization server and protected resource metadata endpoints
303+
- **middlewares/**: JWT validation, access logging, and CORS handling
304+
- **tools/**: MCP tool implementations and registration system
305+
215306
## Development
216307

217308
For detailed development instructions, including how to use this project as a template for your own MCP servers, see [DEVELOPMENT.md](DEVELOPMENT.md).
@@ -230,4 +321,4 @@ This project is licensed under the Unlicense - see the [LICENSE](LICENSE) file f
230321

231322
## Credits
232323

233-
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.
324+
This is a Python port of the [MCP Forge](https://github.com/achetronic/mcp-forge) project (Go), extended with additional OAuth flow endpoints and Python-specific implementations while maintaining security standards.

0 commit comments

Comments
 (0)