Skip to main content
Glama
tahasiddiquii

mcp-sql-guard

mcp-sql-guard

Give an agent read access to your warehouse without the write risk, injection, or data exposure. A Model Context Protocol server that parses every statement to an abstract syntax tree and clears it only if it is a single read-only, allow-listed SELECT. It enforces a LIMIT, masks PII columns unless the caller's role is entitled to them, and records every decision in a tamper-evident audit log. Runs fully offline on a bundled DuckDB warehouse.

ci python license offline

Handing an agent a database connection is the fastest way to turn a helpful tool into a data breach: a generated DROP, a read_csv('/etc/passwd'), a UNION into system tables, or a plain SELECT email that leaks customer PII to whoever is asking. String matching does not stop these; the query has to be understood. This server validates on the AST and governs at the column level, built from my text-to-SQL and guardrails work.

What this demonstrates

Governance control

Where

Single read-only SELECT, verified on the AST

validator.py

Table allowlist, CTE-aware so CTE names are not mistaken for tables

validator.py

File and system functions blocked (read_csv, copy, attach, ...)

config.py

LIMIT injected when absent, capped when too large

validator.py

Column-level PII masking, alias and SELECT * aware

validator.py · masking.py

Role entitlement for PII (read_pii)

config.py

Tamper-evident audit log

audit.py

Governance gate in CI

evals.py

Related MCP server: mcp-read-only-sql

Architecture

flowchart LR
    SQL[agent SQL + role] --> V{{AST validator}}
    POL[(governance policy)] --> V
    V -->|not a read-only SELECT| X[refuse + audit]
    V -->|ok| L[enforce LIMIT]
    L --> E[execute on warehouse]
    E --> M[mask PII by role]
    M --> A[(hash-chained audit)]
    M --> SQL

Quickstart

make dev            # venv + install -e ".[dev]"

sqlguard schema     # the queryable schema, PII marked
sqlguard demo       # benign, masked, and blocked queries
sqlguard query "SELECT name, email FROM customers" --role analyst   # email masked
sqlguard eval       # the governance gate
sqlguard serve --role analyst                                       # live MCP server over stdio

No keys, no network. The warehouse is a bundled in-memory DuckDB. Point at a real database in production by swapping the connection in engine.py; the validation and masking logic is unchanged.

The gate that matters

sqlguard eval replays governed and adversarial queries (report):

metric

value

gate

unsafe_executed

0

= 0

pii_exposed

0

= 0

privileged_pii_visible

True

true

execution_accuracy

1.000

>= 0.90

false_block_rate

0.000

<= 0.10

The two zeros are the contract. unsafe_executed counts any write, multi-statement, file-access, or non-allow-listed query that ran; it must be zero. pii_exposed counts any raw email or phone number that reached a role without read_pii; it must be zero. privileged_pii_visible confirms masking is a role decision, not a blanket blackout: a privacy officer still sees the data. Recall and false-block confirm ordinary analytics still work. CI fails if any gate slips.

What it catches

sqlguard demo over sample traffic:

  • Writes and DDL. DROP, INSERT, UPDATE, and anything that is not a SELECT is refused before it reaches the database.

  • Multi-statement smuggling. SELECT ...; DROP TABLE ... is rejected as more than one statement.

  • File and system access. read_csv, copy, attach, and friends are blocked, so the query cannot escape the warehouse.

  • Metadata exfiltration. A UNION into system tables is refused because unions and non-allow-listed tables are not permitted.

  • PII exposure. An analyst selecting email, email AS contact, or SELECT * gets the column masked; a privacy officer with read_pii gets the value.

Design decisions

  • Validate on the AST, not the string. A query is understood, not pattern matched, so obfuscation and aliasing do not get past the checks.

  • Mask cannot be renamed away. PII masking is computed from the projection, including alias resolution and SELECT * expansion, so email AS x is still masked.

  • Least privilege by default. PII is masked unless a role is explicitly granted read_pii. The policy YAML is written to be read in review.

  • Fail closed. A parse error, an unknown table, or an execution error is a clean denial, never an uncaught path.

Layout

src/mcp_sql_guard/  config · schema · validator · masking · engine · audit · guard · server · evals · cli
data/  policy.example.yaml · eval_cases.jsonl
reports/  governance_report_example.md

Part of a portfolio on production ML and LLM engineering:

License

MIT (c) 2026 Taha Siddiqui

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    A
    maintenance
    Secure SQL proxy for AI agents. Translates natural language to safe SQL via Claude, validates at the AST level (SELECT-only, no DDL/DML), enforces per-agent row-level security, and audit-logs every query.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides secure read-only SQL access to PostgreSQL and ClickHouse databases with built-in safety features like read-only enforcement, timeouts, and managed result files.
    19 PyPI
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables SQL query sanitization by rewriting queries at the AST level to prevent PII/PHI exposure, acting as a proxy between AI agents and PostgreSQL databases.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enforces safety and governance for SQL queries executed by AI agents, providing read-only enforcement, cost estimation, and audit trails.
    Apache 2.0