A secure Model Context Protocol (MCP) server demonstrating enterprise-grade access control patterns. Built with Python and FastMCP v2, this prototype implements JWT-based authentication and scope-based tool authorization, designed for deployment on Google Kubernetes Engine.
- Token-based authentication: Every MCP request requires a valid JWT Bearer token
- Scope-based authorization: Token scopes determine which tools a client can see and call
- Defense in depth: Tool list filtering AND tool call validation (two independent checks)
- Structured audit logging: Every auth decision logged as JSON for cloud logging systems
- 12-factor configuration: All settings via environment variables
- Kubernetes-ready: Health and readiness probe endpoints
Client (Claude Code, MCP client)
β
β Authorization: Bearer <jwt>
βΌ
βββββββββββββββββββββββββββββββββββ
β FastMCP Server (port 8080) β
β β
β βββββββββββββββββββββββββββββ β
β β AuthMiddleware β β
β β 1. Extract Bearer token β β
β β 2. Validate JWT (sig+exp)β β
β β 3. Filter tools by scope β β
β β 4. Block unauthorized β β
β βββββββββββββββββββββββββββββ β
β β
β βββββββββββββ βββββββββββββββ β
β β get_publicβ βget_confiden-β β
β β _info β βtial_info β β
β β β β β β
β β scope: β β scope: β β
β β public: β β confidenti- β β
β β read β β al:read β β
β βββββββββββββ βββββββββββββββ β
β β
β /health /ready /mcp β
βββββββββββββββββββββββββββββββββββ
| Token Scopes | Visible Tools | Can Call |
|---|---|---|
["public:read"] |
get_public_info only |
get_public_info only |
["public:read", "confidential:read"] |
Both tools | Both tools |
[] |
None | None |
| No token / expired / invalid | Rejected (AuthError) | Rejected (AuthError) |
- Python 3.11+
- uv package manager
# Install dependencies
uv sync
# Start the server
uv run python -m src.serverThe server starts on http://localhost:8080 with:
- MCP endpoint:
POST /mcp(Streamable HTTP transport) - Health check:
GET /health - Readiness check:
GET /ready
# Public access only
uv run python -m scripts.generate_token --sub alice --scope public:read
# Full access
uv run python -m scripts.generate_token --sub bob --scope public:read confidential:read
# Expired token (for testing rejection)
uv run python -m scripts.generate_token --sub charlie --scope public:read --exp-hours -1Note: The token must be signed with the same secret the server uses. By default, both use
dev-secret-change-me. If you run the server with a custom secret (e.g.,MCP_JWT_SECRET_KEY=my-secret), you must generate tokens with the matching--secretflag:uv run python -m scripts.generate_token --sub alice --scope public:read --secret my-secret
# Generate a token
TOKEN=$(uv run python -m scripts.generate_token --sub myuser --scope public:read confidential:read 2>&1 | grep "^Token:" | cut -d' ' -f2)
# Add the MCP server to Claude Code
claude mcp add --transport http mcp-auth-prototype http://localhost:8080/mcp \
--header "Authorization: Bearer $TOKEN"# Generate a token
TOKEN=$(uv run python -m scripts.generate_token --sub alice --scope public:read 2>&1 | grep "^Token:" | cut -d' ' -f2)
# Initialize MCP session
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'# Run tests
uv run pytest -v
# Lint
uv run ruff check .Build and test the container image locally before deploying to Kubernetes.
# Build the Docker image
docker build -t mcp-auth-prototype:local .The multi-stage build creates a minimal ~150MB image containing only the runtime dependencies.
# Run with a custom JWT secret (required for production)
docker run -p 8080:8080 -e MCP_JWT_SECRET_KEY=my-secret mcp-auth-prototype:local
# Run with debug logging
docker run -p 8080:8080 \
-e MCP_JWT_SECRET_KEY=my-secret \
-e MCP_LOG_LEVEL=debug \
mcp-auth-prototype:local# Verify health endpoint
curl http://localhost:8080/health
# Verify readiness endpoint
curl http://localhost:8080/ready
# Generate a token (must use --secret matching the container's MCP_JWT_SECRET_KEY)
TOKEN=$(uv run python -m scripts.generate_token --sub alice --scope public:read --secret my-secret 2>&1 | grep "^Token:" | cut -d' ' -f2)
# Test MCP initialization against the container
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer $TOKEN" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'mcp-auth-prototype/
βββ src/
β βββ server.py # MCP server, auth middleware, health endpoints
β βββ auth.py # JWT validation and scope extraction
β βββ tools.py # Tool-to-scope mapping registry
β βββ config.py # Environment-based configuration (pydantic-settings)
βββ documents/
β βββ public.md # Sample public company document
β βββ confidential.md # Sample confidential strategy document
βββ scripts/
β βββ generate_token.py # CLI utility to mint JWT tokens
βββ tests/
β βββ conftest.py # Shared test fixtures (token factories)
β βββ test_auth.py # Unit tests for JWT validation (16 tests)
β βββ test_tools.py # Integration tests for tool authorization (6 tests)
βββ helm/
β βββ mcp-server/ # Helm chart for Kubernetes deployment
β βββ Chart.yaml # Chart metadata (name, version)
β βββ values.yaml # Default configuration values
β βββ values-dev.yaml # Dev environment overrides
β βββ templates/
β βββ _helpers.tpl # Reusable Go template helpers
β βββ deployment.yaml # Deployment (2 replicas, probes, env vars)
β βββ service.yaml # ClusterIP Service on port 8080
β βββ configmap.yaml # Document content (public.md, confidential.md)
β βββ serviceaccount.yaml # K8s ServiceAccounts with Workload Identity
β βββ secretstore.yaml # ESO connection to GCP Secret Manager
β βββ externalsecret.yaml # Syncs JWT key from GCP to K8s Secret
βββ terraform/ # Infrastructure as Code
β βββ main.tf # Provider and backend configuration
β βββ variables.tf # Input variables
β βββ outputs.tf # Output values
β βββ gke.tf # GKE cluster definition
β βββ artifact-registry.tf # Container registry
β βββ secret-manager.tf # Secret Manager resources
β βββ iam.tf # Service accounts and IAM bindings
β βββ github-wif.tf # Workload Identity Federation for GitHub Actions
βββ .github/
β βββ workflows/
β βββ ci.yaml # CI pipeline (lint, test, build, push, update Helm)
βββ argocd/
β βββ application.yaml # ArgoCD Application (GitOps auto-sync)
βββ pyproject.toml # Dependencies and tool configuration
βββ uv.lock # Locked dependency versions
Every push to main triggers an automated pipeline:
git push βββΆ GitHub Actions βββΆ ArgoCD βββΆ GKE Cluster
β β
ββ Lint (ruff) ββ Detects values.yaml change
ββ Test (pytest) ββ Renders Helm chart
ββ Build image ββ Rolling update (zero downtime)
ββ Push to Artifact Registry (git SHA tag)
ββ Update helm/mcp-server/values.yaml
- No stored credentials: GitHub Actions authenticates to GCP via Workload Identity Federation (OIDC token exchange)
- Immutable image tags: Docker images are tagged with the git commit SHA (e.g.,
a1b2c3d), notlatest - GitOps: ArgoCD continuously reconciles the cluster state with what's in Git β including self-healing if someone manually modifies the cluster
In this prototype, document content (public.md, confidential.md) is inlined directly in a Kubernetes ConfigMap within the Helm chart. This is appropriate here because:
- We have only 2 small, static documents (~1KB total)
- It keeps the Helm chart self-contained and easy to understand
- Helm's
.Files.Getfunction can't read files outside the chart directory
This approach does NOT scale. ConfigMaps are limited to 1MB, document changes require a full Helm upgrade (which triggers a pod rolling update), and there's no versioning or independent lifecycle management.
Production alternatives for document-heavy systems:
| Pattern | When to Use | How It Works |
|---|---|---|
| Object Storage (GCS/S3) | Most common. Independent document lifecycle, many documents | App fetches from a cloud bucket at runtime via Workload Identity. Supports versioning, CDN, fine-grained IAM. |
| Database (PostgreSQL/Firestore) | Documents need metadata, search, relationships | App queries a database per request. Full CRUD, indexing, transactions. |
| Git repo + sidecar | GitOps-heavy orgs, docs-as-code | A sidecar/init container clones a separate docs repo. Version history from Git, PRs for review. |
| Content API microservice | Large-scale, many consumers | Dedicated service manages documents. MCP server becomes a thin orchestration layer. |
The key principle: decouple document lifecycle from application lifecycle. The MCP server should be deployable independently from content updates.
All settings are read from environment variables with the MCP_ prefix:
| Variable | Default | Description |
|---|---|---|
MCP_HOST |
0.0.0.0 |
Network interface to bind to |
MCP_PORT |
8080 |
Server port |
MCP_LOG_LEVEL |
info |
Logging verbosity (debug, info, warning, error) |
MCP_JWT_SECRET_KEY |
dev-secret-change-me |
JWT signing key (override in production) |
MCP_JWT_ALGORITHM |
HS256 |
JWT signing algorithm |
MCP_DOCUMENTS_DIR |
documents |
Path to document files |
You can also set these in a .env file (gitignored).
| Component | Technology | Purpose |
|---|---|---|
| MCP Server | FastMCP v2 | MCP protocol with middleware hooks |
| Authentication | PyJWT | JWT token validation |
| Configuration | pydantic-settings | Typed env var config |
| HTTP Server | Uvicorn | ASGI server |
| Testing | pytest + httpx | Unit and integration tests |
| Linting | Ruff | Fast Python linter |
| Package Manager | uv | Fast Python package manager |
| Infrastructure | Terraform | Infrastructure as Code for GCP resources |
| Container Registry | GCP Artifact Registry | Docker image storage |
| Orchestration | Google Kubernetes Engine | Container orchestration |
| Secrets | GCP Secret Manager + ESO | Secure secret management |
| CI | GitHub Actions | Automated lint, test, build, push |
| CD | ArgoCD | GitOps continuous deployment |
| CIβGCP Auth | Workload Identity Federation | OIDC-based auth, no stored keys |
See IMPLEMENTATION_ROADMAP.md for the full build plan. Current status:
- Phase 0: Project scaffolding
- Phase 1: MCP server with tools
- Phase 2: Authentication and authorization
- Phase 3: Tests
- Phase 4: Dockerize
- Phase 5: GCP Infrastructure + Terraform + GKE
- Phase 6: Helm chart
- Phase 7: GitHub Actions CI pipeline
- Phase 8: ArgoCD
- Phase 9: End-to-end verification
- Phase 10: TLS Ingress (HTTPS) β Ingress controller, cert-manager, Let's Encrypt, encrypted external access
- Phase 11: OAuth2 Token Service β Production token issuance via Google OAuth2, developer CLI, Claude Code integration
- Phase 12: Autoscaling & Resilience β HPA, Cluster Autoscaler, PDB, load balancing, load testing with Locust