Skip to content

Architecture

ComponentTechnologyWhy
Web frameworkFastAPI 0.115Async-first, automatic OpenAPI, high performance
ORMSQLAlchemy 2.0 (async)Async sessions, declarative models
Database driverasyncpg 0.30Native async PostgreSQL — no thread pool overhead
ValidationPydantic v2Fast, strict, automatic API docs
Hashingbcrypt 4.x + hashlibIndustry-standard password hashing with SHA-256 pre-hash
ServerUvicornASGI server with uvloop
DatabasePostgreSQL 16ACID-compliant, excellent indexing
RuntimePython 3.12Latest stable, pre-built wheels for all dependencies
ContainerDocker (python:3.12-slim)Minimal attack surface
OrchestrationHelm (OCI)Kubernetes-native deployment

mcpgate/
├── app/
│ ├── main.py # FastAPI app, lifespan (table creation), CORS, router
│ ├── api/
│ │ └── routes.py # All 5 endpoints: create, list, delete, verify, (health in main)
│ ├── core/
│ │ ├── config.py # Settings via pydantic-settings + .env
│ │ ├── database.py # SQLAlchemy engine, session factory, NullPool for tests
│ │ └── security.py # Key generation, SHA-256 pre-hash, bcrypt hash/verify
│ ├── models/
│ │ └── api_key.py # ApiKey SQLAlchemy ORM model
│ └── schemas/
│ └── api_key.py # Pydantic request/response schemas
├── tests/
│ ├── conftest.py # Fixtures: client, admin_headers, DB setup/teardown
│ ├── test_health.py # Health endpoint
│ ├── test_keys.py # Create, list, revoke (with/without auth)
│ └── test_verify.py # Valid, invalid, expired, revoked, tampered keys
├── deployments/
│ ├── docker-compose/
│ │ └── docker-compose.yml # Build-from-source compose (make app-up)
│ └── helm/mcpgate/ # Helm chart for Kubernetes
├── docs/ # This documentation site (Astro/Starlight)
├── Dockerfile # Multi-stage: production + test targets
├── docker-compose.yml # End-user zero-config deploy (pulls from GHCR)
├── docker-compose.test.yml # Test runner (make test-run)
├── Makefile # app-*, docs-*, test-* targets
├── requirements.txt # Runtime dependencies
├── requirements-test.txt # Test-only dependencies
└── pytest.ini # asyncio_mode = auto

CREATE TABLE api_keys (
id TEXT PRIMARY KEY, -- UUID string
public_id TEXT NOT NULL UNIQUE, -- Indexed — O(1) lookup
key_hash TEXT NOT NULL, -- bcrypt(sha256(plain_key))
prefix TEXT NOT NULL, -- mcp_sk_<id>_...<last4> — safe to display
user_id TEXT NOT NULL, -- Indexed — list by user
name TEXT,
scopes JSONB,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT now(),
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ
);
CREATE INDEX ix_api_keys_public_id ON api_keys (public_id);
CREATE INDEX ix_api_keys_user_id ON api_keys (user_id);

Tables are created automatically on startup via SQLAlchemy’s create_all.


POST /api/v1/keys
│
▼
verify_admin_key() check Authorization: Bearer header
│
▼
generate_api_key(prefix) token_hex(8) + token_hex(32)
│ sha256(plain_key) → bcrypt → key_hash
▼
INSERT INTO api_keys (...) store public_id + hash, never plain key
│
▼
return ApiKeyResponseWithSecret key shown once only
POST /api/v1/verify
│
▼
extract public_id key[len(prefix):].split("_")[0]
│
▼
SELECT WHERE public_id = ? single indexed read — O(1)
AND is_active = TRUE
│
▼
bcrypt.checkpw( constant-time comparison
sha256(incoming_key),
stored_hash
)
│
▼
check expires_at < now()
│
▼
UPDATE last_used_at = now()
│
▼
return VerifyResponse

┌─────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
Your Backend ────▶│ Service (ClusterIP :8000) │
MCP Servers ────▶│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Pod 1 │ │ Pod 2 │ │ Pod N │ │
│ │mcpgate │ │mcpgate │ │mcpgate │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ └─────┬──────┘ │ │
│ ▼ │ │
│ PostgreSQL ◀──────────────┘ │
│ │
│ HPA: 1–10 replicas (70% CPU / 80% mem) │
│ PDB: max 1 unavailable │
└─────────────────────────────────────────┘

The Dockerfile has two targets:

TargetUsed byContents
productiondocker-compose.yml, Helmrequirements.txt + app/ only. Non-root user, minimal image.
testdocker-compose.test.ymlrequirements.txt + requirements-test.txt + app/ + tests/ + pytest.ini. Runs pytest.

The test image is never pushed to GHCR — it’s only used locally and in CI via docker compose -f docker-compose.test.yml.