db-mcp-gateway
CLAUDE.md5 starsChanged 10 days agoMITView on GitHub
Self-hosted MCP gateway. AI agents get audited, SSO-gated, read-only DB access. Never holds a DB credential on the client side. Spec: website/docs/initial-idea/. Behavior change → update spec in the same PR. Secrets and local config: copy .env.example → .env. .env is gitignored. One reason to change per layer. Don't blur.
- Reads credentials
In this repository4 other files it gives its agents.
- AGENTS.mdAGENTS.md
- stackSkill
- state-migrationsSkill
- verify-changeSkill
The file
We split this CLAUDE.md into the rules it gives an agent
4 rules. About 0 are common, 4 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 8
Every DB call traces to an SSO-verified identity → audit row.
- Distinctive: line 9
Read-only by default. Writes require explicit per-grant opt-in AND a target-DB role with write privileges.
- Distinctive: line 164
NOT building: BI tool, query builder, credential vault, SaaS, LLM, agent runtime.
- Distinctive: line 165
No git worktrees — work directly in this checkout.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Self-hosted MCP gateway. AI agents get audited, SSO-gated, read-only DB access. Never holds a DB credential on the client side. Spec: website/docs/initial-idea/. Behavior change → update spec in the same PR.
Non-negotiables (MUST)
- DB credentials never leave the gateway. Not in responses, logs, errors, admin endpoints.
- Every DB call traces to an SSO-verified identity → audit row.
- Read-only by default. Writes require explicit per-grant opt-in AND a target-DB role with write privileges. Gateway will NOT provision write grants.
- Audit is synchronous. Audit write fails → request fails. No best-effort audit.
- Permissions in YAML (reviewed by PR) or via SSO-gated admin API (writes audited synchronously to
permissions_audit). No in-band admin UI. See 12-dynamic-permissions.
Response Rules
| Rule | Meaning |
| Execute | No preamble, no "I'll start by…", no restating the task. Lead with the action or the answer; reasoning after, only if non-obvious. Close with a 1–2 sentence summary, nothing else. |
| Parallel / read first | Batch independent tool calls. Never speculate about a file you have not read. |
| Disagree | Say when the requester is wrong; state the correction. |
| Terse | Fragments OK. Drop articles, filler, hedging. Code, commands and paths: verbatim — only prose gets compressed. |
Stack
| Concern | Choice |
| Lang | Rust 2024 — toolchain channel 1.96.0 (rust-toolchain.toml: rustfmt + clippy), MSRV 1.88 (Cargo.toml); deps pinned to latest stable at implementation time |
| Runtime / HTTP | tokio; axum (+ axum-server, rustls) |
| DB driver | sqlx 0.8 — Postgres + MySQL; mongodb 3.7 for the Mongo target |
| MCP | hand-rolled JSON-RPC over HTTP+SSE → swap for SDK when one stabilizes |
| Config | serde + serde-saphyr (not serde_yaml), validated at boot |
| State store | Postgres (co-deployed): sessions, audit log, denylist. Permissions store pg (src/state/permissions/pg.rs) or mysql (src/state/permissions/mysql/) |
| Error tracking | GlitchTip (Sentry protocol), credential-scrubbed by src/sentry_scrub.rs |
| Metrics | metrics + Prometheus exporter on /metrics |
| Distribution | ghcr.io/developerz-ai/db-mcp-gateway and registry.digitalocean.com/developerz-ai/db-mcp-gateway — .github/workflows/ci.yml (build + merge) pushes per-arch images and the multi-arch manifest to both registries on pushes to main; .github/workflows/release.yml pushes the v* tag builds (semver + :latest + GitHub release) |
Commands
| Task | Command |
| One-command setup (toolchain, deps, dev stack) | bin/setup |
| Dev stack up / down / reset / logs / container status | bin/dev up / bin/dev down / bin/dev reset / bin/dev logs / bin/dev ps |
| psql — state DB / target DB | bin/dev psql state / bin/dev psql target |
| mongosh — target Mongo | bin/dev mongosh |
| mysql — permissions store | bin/dev mysql |
| Full test (unit + integration) | bin/test |
| Single test by pattern | bin/test --test <name> or cargo test <pattern> |
| Format | bin/fmt |
| Lint (fmt-check + clippy -D warnings) | bin/lint |
| Reproduce CI locally / one-command check | bin/ci / bin/check |
| Assert scrubber regex features in the shipping graph | bin/check-scrub-features |
| Mint a static service bearer token | bin/mint-service-token <name> |
| Run the binary | cargo run -- --config config/example.yaml |
| Build release image locally | docker build -t db-mcp-gateway:dev . |
Secrets and local config: copy .env.example → .env. .env is gitignored.
Layers
One reason to change per layer. Don't blur.
| Layer | Owns | Module |
| Transport | MCP framing, JSON-RPC, HTTP+SSE, admin API | src/transport/ |
| Auth | OIDC, JWT, session cache, service tokens | src/auth/ |
| Authz | (user, groups, server, db, action) → allow + constraints | src/authz/ |
| Tools | MCP tool dispatch | src/tools/ |
| Exec | Per-DB pool, statement timeout, row cap, cancellation | src/exec/ |
| Audit | Append-only writes, retention, archive, sinks | src/audit/ |
| Config | YAML parse + validate + secrets resolve + hot reload | src/config/ |
| State DB | Sessions, audit log, denylist, permissions store | src/state/ |
Conventions
The bar: idiomatic, boring, readable Rust. No spaghetti, no premature abstraction. A function reads top to bottom without chasing state. Equally-correct options → pick the one easier to delete. clippy -D warnings is the floor, not the ceiling.
| Rule | Meaning |
| Typed errors | thiserror for domain; anyhow only at the src/main.rs boundary. Every client error has a stable code (see website/docs/initial-idea/03-mcp-tools.md). |
| No panics | No unwrap/expect outside src/main.rs and tests — a panic in a hot path crashes every user. Propagate with ?; branch with match/if let/let ... else. |
| Newtypes | Newtype over bare primitives when a value has meaning (RequestId(String), not String). Make illegal states unrepresentable — enum over contradictory bool+Option. Validate input into a type once at the edge; don't re-validate downstream. |
| Derive | Derive, don't hand-roll (Debug, Clone, serde). Every public type derives Debug at minimum. Keep the pub surface minimal. |
| Borrow by default | &str over String, &[T] over Vec<T>. .clone() only when ownership must move — a non-obvious clone gets a one-line // why. Arc<T> for shared read-only; add a lock only when you mutate shared state, keep the critical section tiny. |
| One thing per function | Need "and" to describe it → split it. No premature abstraction: concrete first, introduce a trait when the second impl arrives (e.g. DbAdapter). Iterator chains over manual index loops where they read clearer. |
| Tracing | tracing, not println. Every tool dispatch is a span with request_id, user, server, database. |
| Audit before response | Audit write commits before the success response goes out. Two-phase commit with the state DB. |
| Per-DB pools | Sized by config. Never one global pool. Never share a pool across (server, database). |
| Async end-to-end | No std::sync::Mutex on the request path — use tokio::sync. No block_on, no blocking I/O in async fns (offload with spawn_blocking). Never hold a std::sync guard across .await. A slow query on DB A must never block DB B. One noisy user must not starve others. |
| Cancellation safety | Agent disconnect → tokio task dropped → pg_cancel_backend → audit row outcome: cancelled. Test it; easy to write code that holds the conn until the query finishes anyway. |
| Size | Files ≤300 LOC. Split by responsibility. No backwards-compat shims pre-v1. |
| Comments | Comment the non-obvious why, never the what. Rename until code doesn't need the what. /// on public items whose contract isn't obvious from the signature. Architecture decisions go in website/docs/initial-idea/, not code comments. |
Testing
| Kind | Rule |
| Unit + property | Pure logic (authz evaluation, config validation, SQL rewriting), no real DB. Authz constraint-merge (proptest): most restrictive value wins, always. |
| Integration | Gateway booted against real Postgres (CI service container). Real MCP requests over HTTP. |
| Audit | Every integration test that hits a tool asserts an audit row exists. Tool failed authz → audit row with outcome: forbidden. |
CI
GitHub Actions on GitHub-hosted runners — ubuntu-latest default (ubuntu-24.04-arm for arm64), with Postgres, target Postgres, MySQL and Mongo as services. Workflows in .github/workflows/; bin/ci reproduces them locally.
| Job | Does |
fmt | cargo fmt --check; every action pinned to a 40-hex SHA |
clippy | clippy -D warnings (all-targets + release cfg) + bin/check-scrub-features |
test | cargo test --all-targets --all-features against the service containers |
audit | cargo audit --ignore RUSTSEC-2023-0071 |
secret-scan | advisory — git grep -nE over every tracked file for dbmcp_svc_[0-9a-f]{64}; exempt paths: tests/**, website/**, config/example.yaml, .github/**, src/config/yaml.rs |
build + merge | on main: per-arch image → multi-arch manifest, pushed to GHCR and DOCR |
Every workflow declares a concurrency group with cancel-in-progress, and every job sets timeout-minutes. ci.yml cancels superseded PR runs only — pushes to main never cancel, because the Image Updater pins the short SHA, so cancelling the first of two closely-landed merges would leave that commit with no image at its tag (main is a moving convenience tag). The release workflow is hard cancel-in-progress: false. Public repo — org rule: private on Ubicloud, public on GitHub-hosted.
Security review required
Touching any of these requires a (security review required) label on the PR: src/auth/, src/authz/, credential loading or storage, the audit write path, any new MCP tool, SQL execution (timeouts, row caps, cancellation). Reviewers look for: credentials in errors/logs, missing audit rows, authz bypass via constraint-merge edges, SQL injection / timeout bypass.
Layout
src/
main.rs # entry, signals, graceful shutdown
lib.rs # library root the integration tests drive
startup.rs # background tasks
sentry_scrub.rs # credential scrubbing for GlitchTip
config/ # YAML schema, validation, secrets, hot reload
transport/ # MCP over HTTP+SSE, admin API
auth/ # OIDC, JWT, sessions, service tokens
authz/ # grant evaluation, constraint merge
tools/ # MCP tool impls
exec/ # per-DB pools, query exec, timeouts, row caps (pg/, mongo/)
audit/ # writer, pruner, archive, sinks
state/ # state DB queries, permissions store (pg.rs, permissions/mysql/); no central errors.rs — each layer owns its typed error
migrations/ # state-DB schema (sqlx, Postgres); migrations-mysql/ = MySQL permissions store
tests/ # integration tests — real DBs, never a mocked target DB
benchmarks/ # load harness (feature-gated, never shipped)
config/ # example.yaml (commented), permissions.yml (deployable)
docs/ # internal notes (security QA)
website/docs/ # initial-idea/ = spec (00-seed.md = original framing), usage/ = developer (additions are public-API changes), deployment/ = operator
bin/ # setup/dev/test/fmt/lint/ci/check helpers
docker-compose.dev.ymlNEVER
| Never | Because |
| Return a connection string, password, or role secret in any response, error, or log. | The client side never holds a DB credential (MUST #1). |
| Add an admin web UI for permissions. | Permissions change by reviewed YAML or the audited admin API only (MUST #5). |
| Mock the target DB in query-path tests. | A mock cannot prove timeouts, row caps or cancellation. |
| Skip the audit write for performance. | Audit is synchronous; a failed audit write fails the request (MUST #4). |
| Add a "service account" auth path without a permissions group + audit identity. | The shipped path is service_accounts: (spec 14, src/auth/service_token.rs): a static dbmcp_svc_ bearer mapped to exactly one permissions: group, audited as service:<name>, boot-gated off the admin group, with no in-band mint/revoke. A boot-validated service identity is the one explicit exception to the per-call SSO-verification rule (rule 2): its identity is fixed in YAML at boot, so the audit row attributes the dispatch to service:<name> with no fresh IdP round-trip. The exception preserves four properties — the audit contract (synchronous write, service:<name> identity, user_email synthesized), the YAML-only configuration contract (no in-band grant edits), the single non-admin permissions group, and the no-in-band-mint/revoke rule. Any future service-auth mechanism must carry the same four and land with a spec update; spec 14 and the auth/audit tests enforce them. |
| Promote a request from one user's session to another. | The session identity is the audit subject (MUST #2). |
| Run as DB superuser. Per-DB read-only roles, full stop. | The per-DB role is the blast radius. |
| Serve a write path without an explicit per-grant opt-in and a target-DB role holding write privileges. | Read-only by default; the gateway will NOT provision write grants (MUST #3). |
Force-push main. | History is auditable; a force-push rewrites what reviewers read. |
--no-verify on commits — fix the hook. | The hook is the gate. |
Context (not in code)
- Single-tenant. Two orgs sharing one install is unsupported.
- Distribution is GHCR + DOCR (public, free, no Docker Hub rate limits). Helm chart on roadmap.
- NOT building: BI tool, query builder, credential vault, SaaS, LLM, agent runtime.
- No git worktrees — work directly in this checkout. A task big enough to need subagents runs them as a team in this same checkout: split the work into disjoint pieces so no two agents touch the same files.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

