agentleFS
developerz-aideveloperz-ai/db-mcp-gateway/CLAUDE.md

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

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
  1. Distinctive:

    Every DB call traces to an SSO-verified identity → audit row.

    line 8
  2. Distinctive:

    Read-only by default. Writes require explicit per-grant opt-in AND a target-DB role with write privileges.

    line 9
  3. Distinctive:

    NOT building: BI tool, query builder, credential vault, SaaS, LLM, agent runtime.

    line 164
  4. Distinctive:

    No git worktrees — work directly in this checkout.

    line 165

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

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)

  1. DB credentials never leave the gateway. Not in responses, logs, errors, admin endpoints.
  2. Every DB call traces to an SSO-verified identity → audit row.
  3. 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.
  4. Audit is synchronous. Audit write fails → request fails. No best-effort audit.
  5. 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

RuleMeaning
ExecuteNo 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 firstBatch independent tool calls. Never speculate about a file you have not read.
DisagreeSay when the requester is wrong; state the correction.
TerseFragments OK. Drop articles, filler, hedging. Code, commands and paths: verbatim — only prose gets compressed.

Stack

ConcernChoice
LangRust 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 / HTTPtokio; axum (+ axum-server, rustls)
DB driversqlx 0.8 — Postgres + MySQL; mongodb 3.7 for the Mongo target
MCPhand-rolled JSON-RPC over HTTP+SSE → swap for SDK when one stabilizes
Configserde + serde-saphyr (not serde_yaml), validated at boot
State storePostgres (co-deployed): sessions, audit log, denylist. Permissions store pg (src/state/permissions/pg.rs) or mysql (src/state/permissions/mysql/)
Error trackingGlitchTip (Sentry protocol), credential-scrubbed by src/sentry_scrub.rs
Metricsmetrics + Prometheus exporter on /metrics
Distributionghcr.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

TaskCommand
One-command setup (toolchain, deps, dev stack)bin/setup
Dev stack up / down / reset / logs / container statusbin/dev up / bin/dev down / bin/dev reset / bin/dev logs / bin/dev ps
psql — state DB / target DBbin/dev psql state / bin/dev psql target
mongosh — target Mongobin/dev mongosh
mysql — permissions storebin/dev mysql
Full test (unit + integration)bin/test
Single test by patternbin/test --test <name> or cargo test <pattern>
Formatbin/fmt
Lint (fmt-check + clippy -D warnings)bin/lint
Reproduce CI locally / one-command checkbin/ci / bin/check
Assert scrubber regex features in the shipping graphbin/check-scrub-features
Mint a static service bearer tokenbin/mint-service-token <name>
Run the binarycargo run -- --config config/example.yaml
Build release image locallydocker 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.

LayerOwnsModule
TransportMCP framing, JSON-RPC, HTTP+SSE, admin APIsrc/transport/
AuthOIDC, JWT, session cache, service tokenssrc/auth/
Authz(user, groups, server, db, action) → allow + constraintssrc/authz/
ToolsMCP tool dispatchsrc/tools/
ExecPer-DB pool, statement timeout, row cap, cancellationsrc/exec/
AuditAppend-only writes, retention, archive, sinkssrc/audit/
ConfigYAML parse + validate + secrets resolve + hot reloadsrc/config/
State DBSessions, audit log, denylist, permissions storesrc/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.

RuleMeaning
Typed errorsthiserror 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 panicsNo 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.
NewtypesNewtype 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.
DeriveDerive, 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 functionNeed "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.
Tracingtracing, not println. Every tool dispatch is a span with request_id, user, server, database.
Audit before responseAudit write commits before the success response goes out. Two-phase commit with the state DB.
Per-DB poolsSized by config. Never one global pool. Never share a pool across (server, database).
Async end-to-endNo 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 safetyAgent 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.
SizeFiles ≤300 LOC. Split by responsibility. No backwards-compat shims pre-v1.
CommentsComment 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

KindRule
Unit + propertyPure logic (authz evaluation, config validation, SQL rewriting), no real DB. Authz constraint-merge (proptest): most restrictive value wins, always.
IntegrationGateway booted against real Postgres (CI service container). Real MCP requests over HTTP.
AuditEvery 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.

JobDoes
fmtcargo fmt --check; every action pinned to a 40-hex SHA
clippyclippy -D warnings (all-targets + release cfg) + bin/check-scrub-features
testcargo test --all-targets --all-features against the service containers
auditcargo audit --ignore RUSTSEC-2023-0071
secret-scanadvisory — 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 + mergeon 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.yml

NEVER

NeverBecause
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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.