mcp-guard
README.md
# mcp-guard
A small **Model Context Protocol (MCP) server** that shows how to give AI agents access to tools *safely*:
- **OAuth 2.1 resource server**: validates bearer JWTs from an external authorization server (signature via JWKS, `iss`, `aud`/RFC 8707 resource, `exp`) and publishes **RFC 9728 Protected Resource Metadata** so MCP clients can discover where to authenticate.
- **Admin policy controls**: a YAML policy that IT can review and version decides which tools each agent can **see** (`tools/list` is filtered) and **call** (`tools/call` is enforced). Rules use scopes plus roles, it denies by default, and it includes a per-client **kill switch** and **rate limit**.
- **Audit and metrics**: one JSON audit line per decision (argument *names* only, never values) and Prometheus metrics (`mcp_tool_calls_total`, `mcp_tool_denials_total`, `mcp_tool_call_seconds`).
- **Tests**: unit tests for the token verifier and policy engine, plus end-to-end tests over real Streamable HTTP using the official MCP Python SDK client.
Built on the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) (v2).
## Architecture
```
MCP client / agent ──Bearer JWT──▶ Streamable HTTP /mcp
│
SDK bearer auth ──┤ 401 + WWW-Authenticate (resource_metadata) if missing/invalid
JWTTokenVerifier │ iss / aud / exp / signature (JWKS or HS256 in dev)
▼
PolicyMiddleware
tools/list ─▶ filter by policy
tools/call ─▶ blocked? scopes? roles? rate limit?
│ └─ deny ─▶ JSON-RPC -32003 + audit + metric
▼
tool handlers ─▶ audit + latency histogram
```
## Quick start
```bash
pip install -e ".[dev]"
export MCP_GUARD_HS256_SECRET=dev-secret-at-least-32-bytes-long!!
python -m mcp_guard.server # serves http://127.0.0.1:8000/mcp
# mint dev tokens with different permissions
python -m mcp_guard.devtoken --client reader --scope "tickets:read"
python -m mcp_guard.devtoken --client ops --scope "tickets:read tickets:write" --role admin
```
Discovery and ops endpoints:
| Path | Purpose |
|---|---|
| `/.well-known/oauth-protected-resource/mcp` | RFC 9728 metadata (authorization server, resource) |
| `/metrics` | Prometheus metrics |
| `/healthz` | Liveness |
## Policy
```yaml
tools:
search_tickets: { scopes: [tickets:read] }
create_ticket: { scopes: [tickets:write] }
close_ticket: { scopes: [tickets:write], roles: [admin] }
clients:
blocked: [rogue-agent]
rate_limit_per_minute: 120
```
| Token | Sees | `close_ticket` |
|---|---|---|
| `tickets:read` | `search_tickets` | hidden, `missing_scope` |
| `tickets:read tickets:write` | search + create | `Forbidden: missing_role` |
| `... + role admin` | all three | allowed |
| `client_id: rogue-agent` | nothing | `Forbidden: client_blocked` |
## Production notes
- Set `MCP_GUARD_JWKS_URL` (RS256/ES256 keys from your IdP) and do **not** set the HS256 secret.
- `MCP_GUARD_RESOURCE_URL` must be this server's canonical URL. Tokens whose `aud` doesn't match are rejected, which prevents token replay across servers (the confused-deputy problem).
- Rate-limit buckets are in-memory per process. Behind multiple replicas, move them to Redis.
- Suggested SLOs: 99.9% availability of `/mcp`, p95 policy-check overhead under 5 ms, measured from `mcp_tool_call_seconds`.
## Tests
```bash
pytest -q
```
## License
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues