Skip to main content
Glama
shozy786

sqlgate-mcp

by shozy786

sqlgate-mcp

Read-only SQL access to your database for AI agents, over the Model Context Protocol, with guardrails you can read and an audit log you can check.

Giving an agent a database connection string is easy. Knowing what it can do with it is not: pg_read_file('/etc/passwd'), a DELETE hidden in a CTE, SELECT * on the users table with its password hashes, or a cross join that runs for an hour. sqlgate sits between the agent and the database and only lets through what your policy allows.

agent ──MCP──▶ sqlgate ──▶ guard (sqlglot AST) ──▶ database (read-only role, read-only txn, timeout)
                  │                                     │
                  └── audit log ◀───────────────────────┴── masking ◀── results

Defence in depth

No single layer is trusted to be perfect:

  1. The guard parses every query with sqlglot in the database's dialect and refuses anything that isn't exactly one read-only query:

    • no INSERT, UPDATE, DELETE, DDL or COPY anywhere in the tree, including data-modifying CTEs; no SELECT ... INTO and no FOR UPDATE / FOR SHARE locks

    • no functions that read files or the network, sleep, run SQL from strings or change state (pg_read_file, dblink, query_to_xml, nextval, DuckDB's read_csv, getenv, ...)

    • only tables your policy allows, never the system catalogs

    • denied columns can't be selected, filtered on, or reached through SELECT * or a whole-row reference like row_to_json(u)

    • an outer LIMIT is added or tightened

  2. The database enforces it again. Postgres queries run in a READ ONLY transaction with a statement_timeout and are always rolled back, optionally after an EXPLAIN cost check. BigQuery queries are dry-run against a bytes limit and carry maximum_bytes_billed. DuckDB files are opened read-only with external access switched off and a query timeout.

  3. Masking transforms sensitive values on the way out. Lineage is followed through expressions, CTEs and subqueries, so lower(email) AS e is masked like email. Responses are also kept within a byte budget, so one wide row can't flood an agent's context.

  4. The audit log records every query, what actually ran, and why anything was refused.

You should still connect with a role that can only SELECT what the agent is meant to see.

Related MCP server: MCP Warehouse Server

Install

uv tool install 'sqlgate-mcp[postgres]'   # or [bigquery], or both

Python 3.11+, MCP Python SDK 2.x. DuckDB support is built in.

Configure

# sqlgate.yaml
database:
  type: postgres
  dsn: ${ANALYTICS_RO_DSN}        # from the environment, never the file
  statement_timeout_ms: 15000
  max_cost: 500000                # planner cost; EXPLAIN a few heavy queries to pick yours

policy:
  max_rows: 500
  allow_tables: ["public.*", "reporting.*"]
  deny_tables: ["public.*_audit"]
  deny_columns: ["*.password_hash", "users.mfa_secret"]
  mask_columns:
    "*.email": partial            # s•••@example.com
    "customers.phone": partial    # •••4567
    "customers.full_name": hash   # h:3f9a…, stable, so it still groups and joins
  deny_functions: [expensive_udf]

masking:
  key: ${SQLGATE_MASK_KEY}         # HMAC key for hash masking

audit:
  path: /var/log/sqlgate/audit.jsonl

For BigQuery:

database:
  type: bigquery
  project: acme-analytics
  datasets: [reporting, marketing]   # unqualified tables resolve to the first
  location: EU
  max_gib_billed: 5

For a DuckDB file: type: duckdb, path: warehouse.duckdb, timeout_s: 30.

Table patterns are globs over schema.table (BigQuery: project.dataset.table); column patterns over table.column or schema.table.column. Unqualified tables resolve to public for Postgres and main for DuckDB unless policy.default_schema says otherwise.

Names are compared the way the database compares them. In Postgres "Orders" (quoted) is a different table from orders, so allow patterns match it exactly; deny patterns and masks ignore case everywhere, so any ambiguity ends in a refusal.

Check what the guard does with a query before an agent ever sends it:

$ sqlgate-mcp check -c sqlgate.yaml "select email, count(*) from customers group by 1"
SELECT email, COUNT(*) FROM customers GROUP BY 1 LIMIT 501
masked output columns: #1 (partial)

$ sqlgate-mcp check -c sqlgate.yaml "with gone as (delete from orders returning *) select * from gone"
refused: DELETE is not allowed anywhere in the query, including inside WITH

Connect a client

Claude Code:

claude mcp add analytics -- sqlgate-mcp serve -c /path/to/sqlgate.yaml

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "analytics": {
      "command": "sqlgate-mcp",
      "args": ["serve", "-c", "/path/to/sqlgate.yaml"],
      "env": { "ANALYTICS_RO_DSN": "postgresql://agent_ro@db/analytics" }
    }
  }
}

Or with Docker, where the container only ever sees the config and the secrets you pass it:

docker build -t sqlgate-mcp .
claude mcp add analytics -- docker run -i --rm \
  -v /path/to/sqlgate.yaml:/config/sqlgate.yaml:ro -e ANALYTICS_RO_DSN sqlgate-mcp

Over HTTP

One server for a team, instead of one per laptop:

SQLGATE_TOKEN=$(openssl rand -hex 32) sqlgate-mcp serve -c sqlgate.yaml --transport http --host 0.0.0.0 --port 8000
claude mcp add --transport http analytics https://sqlgate.internal/mcp --header "Authorization: Bearer $TOKEN"

On 127.0.0.1 (the default) the SDK's DNS-rebinding protection is on; any other address requires SQLGATE_TOKEN, and the server won't start without it. Put TLS in front.

Tools

tool

list_tables

Tables the policy allows, with row estimates and descriptions

describe_table

Columns, types and comments of one table (denied columns left out)

run_query

One read-only SELECT; returns columns, rows, truncated and which columns were masked

explain_query

What would run, what would be masked, and the cost estimate, without running it

sample_rows

Up to 20 rows of a table through the same guard and masking, to see what values look like

All five are annotated readOnlyHint and declare output schemas, so clients get structured_content as well as text and can skip confirmation prompts. The visible schema is also available as the schema://overview resource, for clients that attach context.

Refusals come back as tool errors that say what to change ("table hr.salaries is not available; use list_tables..."), so agents recover instead of retrying blindly. Table and column comments are passed through, so a well-commented schema is the best prompt you can give an agent.

A read-only Postgres role

CREATE ROLE agent_ro LOGIN PASSWORD '...' NOSUPERUSER NOCREATEDB NOCREATEROLE;
ALTER ROLE agent_ro SET default_transaction_read_only = on;
GRANT USAGE ON SCHEMA public, reporting TO agent_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public, reporting TO agent_ro;
REVOKE SELECT ON public.users FROM agent_ro;
GRANT SELECT (id, email, created_at) ON public.users TO agent_ro;

Audit log

One JSON object per line:

{"ts": "2025-12-28T07:25:11.402+00:00", "tool": "run_query", "sql": "select count(*) from orders",
 "executed": "SELECT COUNT(*) FROM orders LIMIT 501", "outcome": "ok", "rows": 1, "truncated": false,
 "elapsed_ms": 3.8, "estimated_cost": 1834.5}

outcome is ok, refused (guard or cost check) or failed (database error). It's written per line and survives logrotate.

Development

uv sync --all-extras
uv run pytest                     # Postgres tests run when SQLGATE_TEST_PG_DSN is set
uv run ruff check && uv run ruff format --check

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to query a Postgres data warehouse through a governed, read-only SQL interface with policy enforcement, row limits, schema-level PII isolation, and a full audit trail.
    1
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to safely query PostgreSQL by intercepting and auditing every generated SQL statement before it reaches the database, blocking destructive commands like DROP/TRUNCATE, unbounded DELETE/UPDATE statements, stacked queries, and SQL injection probes. It exposes tools for validating query safety and enforcing a strict read-only (SELECT/EXPLAIN) policy.
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Enables AI agents to query PostgreSQL databases securely with read-only defaults, PII masking, rate limiting, and human-approved, expiring mutation tokens.
    9
    1
    Apache 2.0