Skip to main content

08 โ€” Configuration

Shapeโ€‹

A single YAML file. Everything operational lives here. No per-environment forks of the binary; one binary, many configs.

# config.yaml โ€” illustrative, not final schema

gateway:
bind: 0.0.0.0:8443
external_url: https://db.internal.acme.com
env: production # rejects inline secrets when 'production'
state_db:
url: ${ENV:STATE_DB_URL} # gateway's own Postgres
pool_size: 10

auth:
oidc:
issuer: https://acme.okta.com
client_id: ${ENV:OIDC_CLIENT_ID}
client_secret: ${ENV:OIDC_CLIENT_SECRET}
groups_claim: groups # or 'scim' / 'directory_api'
session_ttl_hours: 8

servers:
- name: prod
kind: postgres
host: prod-rw.db.internal
port: 5432
tls: required
databases:
- name: app
role: mcp_gateway_prod_app_ro
password: vault:secret/prod/db/app_ro_password
description: "Main customer-facing app DB"
sql_capture: redacted
pool:
max_connections: 5

- name: billing
role: mcp_gateway_prod_billing_ro
password: vault:secret/prod/db/billing_ro_password
sql_capture: metadata_only
pool: { max_connections: 3 }

- name: staging
kind: postgres
host: staging.db.internal
port: 5432
tls: required
databases:
- name: app
role: mcp_gateway_staging_app_ro
password: ${FILE:/run/secrets/staging-app-ro-password}

permissions:
- group: backend-engineers
grants:
- { server: staging, database: "*", action: query_read }
- { server: prod, database: "*", action: schema_read }

- group: oncall
grants:
- server: prod
database: "*"
action: query_read
constraints:
require_reason: true
statement_timeout_ms: 5000
row_limit: 1000

# Optional. Absent โ‡’ /admin/v1/* returns 404, YAML-only permissions path.
admin:
enabled: true
group: db-admins # SSO group claim authorising admin calls

# Optional. Absent โ‡’ pg (state DB) backs users/databases/grants.
permissions_store:
driver: pg # or 'mysql' โ€” see boot-gate below

logging:
hot_retention_days: 90
archive:
kind: s3
bucket: acme-db-mcp-audit
prefix: gateway/
region: us-east-1
stream:
- kind: otlp
endpoint: https://otel.internal:4317

Resolution orderโ€‹

  1. File at --config flag, else $DB_MCP_GATEWAY_CONFIG, else /etc/gateway/config.yml.
  2. ${ENV:NAME} placeholders expanded from process env; ${FILE:/path} placeholders read from disk (trailing newline stripped). Unresolved or empty refs abort boot.
  3. vault:, aws-sm:, gcp-sm: references resolved from the named backend (when backend integrations land โ€” until then, recognised refs also abort boot rather than silently failing at first DB connect).
  4. Final config validated against the schema. Errors at this stage refuse to start the process โ€” no half-loaded state.

Validation rules (non-exhaustive)โ€‹

  • env: production โ‡’ inline literal passwords rejected (must be reference).
  • Every permissions[*].group must be a syntactically valid group name; gateway warns (not errors) if no IdP user is in the group โ€” groups can be defined ahead of population.
  • Every permissions[*].grants[*].server / database must exist in servers, or be the literal "*".
  • Every server's tls must be required when env: production unless explicitly tls: insecure (which logs a warning every minute).
  • Role names must match ^[a-zA-Z_][a-zA-Z0-9_]*$. Catch typos before they become connection failures.
  • Every server's kind must have a query adapter wired (today postgres and mongo). A mysql/mssql target is rejected at boot rather than parsing clean and failing every query at runtime โ€” these kinds are reserved for the roadmap adapters and only become valid when their adapters land.
  • admin.enabled defaults to false โ€” absent or false leaves /admin/v1/* unmounted (404). When enabled: true, admin.group is required and must be non-empty/non-whitespace, else boot aborts (every authenticated caller would otherwise be an admin). Full surface in 12-dynamic-permissions.md.
  • permissions_store.driver: mysql combined with admin.enabled: true is rejected at boot โ€” admin handlers are pg-only today. Use driver: pg (the default when the block is absent) for the admin path, or mysql with YAML grants only.

Hot reloadโ€‹

SIGHUP re-reads the file. On success, swaps live config atomically. On failure, keeps the old config and logs the error โ€” never half-applies. Pools for removed databases drain; new pools for added databases come up lazily.

What is not in configโ€‹

  • Anything user-facing. No UI strings, no branding, no email templates (this isn't that kind of product).
  • Per-user permissions. Users get permissions through groups, period.
  • DB schemas. The gateway reads the DB's own schema; it doesn't maintain a separate model.