Skip to main content
Glama
AbuBaker91-hub

SafeSQL

SafeSQL — chat-to-SQL with read-only guardrails and an MCP server

The model proposes, the database disposes.

Ask questions in plain language over a real Postgres database. The LLM only ever proposes SQL; deterministic code parses it, validates it, rewrites it, and runs it as a SELECT-only role with limits, timeouts and a full audit trail.

1. Problem

Teams want "just let people ask the database questions", but handing an LLM a database connection is how you lose a table. Text-to-SQL demos that trust the model are one hallucinated DROP away from an incident. The fix is boring and structural: let the model write SQL, and let code that cannot hallucinate decide what actually runs.

Related MCP server: PostgreSQL MCP Server for Claude Desktop

2. What it does

  • Chat UI: ask a question, get a one-sentence summary, a results table, and a collapsible "SQL that ran" block with the model's rationale and every rewrite the guard applied.

  • guard() — a pure function, no DB, no LLM — parses each proposal with sqlglot and blocks anything that is not one clean SELECT on allow-listed tables (exact rules below).

  • Execution runs as Postgres role readonly_app inside a READ ONLY transaction with a 5s statement timeout: three independent layers under the guard.

  • One repair attempt: on a database error, the error message is fed back to the model once; a second failure is an honest failed answer showing both attempts.

  • Every question is logged to a queries table, and every stage (generate, guard, execute, summarize) writes an audit row.

  • MCP server (mcp_server.py): tools describe_schema and query_readonly reuse the exact same guard and executor, so Claude Desktop / Claude Code can query the demo DB safely.

  • Demo data: the Chinook music store (artists, albums, tracks, invoices, customers).

▶ Watch the 30-second demo video · Try the live demo

Ask a question — results table plus a one-sentence summary built from the rows:

Good question: results table and summary

Expand "SQL that ran" — the model's rationale and every rewrite the guard applied:

SQL block with rationale and the LIMIT 200 rewrite

Ask for something destructive — blocked with the exact reason, and the attempted SQL shown:

Blocked DROP TABLE with reason "not a SELECT"

Every question lands in the history with its status and latency:

History panel with ok and blocked queries

3. Architecture

flowchart LR
  UI[static UI /] --> API[FastAPI /ask]
  MCP[Claude via MCP] --> G
  API --> R[aiforge-core Router]
  R --> P1[Gemini 2.5 Flash]
  R --> P2[Groq llama-3.3-70b]
  R -->|SqlProposal| G[guard.py sqlglot]
  G -->|blocked + reason| API
  G -->|rewritten SELECT| X[execute.py readonly_app READ ONLY 5s]
  X --> DB[(Postgres: chinook tables, queries, audit_log)]
  X --> S[summarize.py]
  S --> API

The model never sees the database. It sees a compact schema summary (GET /schema): table names, columns with types, foreign keys, three sample rows per table. Those table names are also the guard's allow-list.

Routes: POST /ask {question}{summary, columns, rows, sql, rationale, rewrites, status, block_reason, attempts} · GET /queries history · GET /schema transparency · GET /health.

MCP server for Claude

Add to claude_desktop_config.json (or .mcp.json for Claude Code) — adjust the paths:

{
  "mcpServers": {
    "safesql": {
      "command": "/path/to/safesql/.venv/bin/python",
      "args": ["/path/to/safesql/mcp_server.py"],
      "env": { "DATABASE_URL": "postgresql://app:app@localhost:5432/app",
               "READONLY_DATABASE_URL": "postgresql://readonly_app:readonly@localhost:5432/app" }
    }
  }
}

Then ask Claude to "list the tables and count the invoices" — its queries go through the same guard and show up in the history with their status.

4. Guardrails

All deterministic, all tested, all enforced by parsing (sqlglot, postgres dialect) — never by regex on strings:

  • Unparseable SQL → blocked (unparseable); more than one statement → blocked (multi_statement).

  • The single statement must be a SELECT; CTEs are fine, data-modifying CTEs and any DML/DDL node anywhere in the tree are not (not_select).

  • No comments (comment), no SELECT ... INTO (select_into).

  • Denied functions: pg_sleep, pg_read_file, lo_import, dblink (denied_function).

  • No pg_catalog, information_schema, or pg_* relations (system_table).

  • Only allow-listed tables from the schema summary, compared on sqlglot-normalized identifiers, i.e. case-insensitively and quote-aware (unknown_table).

  • Missing LIMITLIMIT 200 injected; LIMIT > 200 → lowered. Every rewrite is returned and shown in the UI.

  • Even past the guard: SELECT-only role, READ ONLY transaction, 5000ms statement timeout, per-IP rate limit.

5. Limits

  • One database, one schema (public); no cross-schema or multi-DB routing.

  • The guard's allow-list check is table-level, not column-level.

  • Case-insensitive allow-list matching can accept a quoted spelling (e.g. "Artist") that Postgres itself will then reject; that surfaces as a normal execution error and repair attempt, never a security hole.

  • One repair attempt, then an honest failure — no agentic retry loops.

  • When READONLY_DATABASE_URL is not set (managed Postgres with a single connection string, e.g. Neon), execution connects with DATABASE_URL and drops privileges with SET LOCAL ROLE readonly_app inside the READ ONLY transaction — the SELECT-only role, read-only transaction and 5s timeout all still apply; there is just no separate login.

  • The offline demo (LLM_PROVIDER_ORDER=mock) uses canned proposals, so the answers rotate through a fixed set; real questions need a free Gemini or Groq key.

  • Chinook sample database © Luis Rocha, committed as db/chinook.sql (v1.4.5, lowercase identifiers) under its license.

6. Run

cp .env.example .env      # add GEMINI_API_KEY, or set LLM_PROVIDER_ORDER=mock for keyless
make setup                # venv, deps, postgres via docker, migrations + chinook + roles
make demo                 # http://localhost:8000

Or fully containerized: docker compose up --build (defaults to the keyless mock provider).

Deploy (Vercel + Neon, free tier)

The repo is serverless-ready: api/index.py exposes the ASGI app, vercel.json routes everything to it, and requirements.txt holds the runtime deps. Cold starts only run idempotent migrations and build the schema summary (a handful of catalog queries) — the 600 KB Chinook dump is never loaded at cold start.

  1. Neon: create a free project, copy the pooled DATABASE_URL.

  2. Seed the remote database once from your machine (migrations + Chinook + readonly role):

    # bash:        DATABASE_URL="postgres://<neon-owner-url>" .venv/bin/python -m app.seed
    # PowerShell:  $env:DATABASE_URL="postgres://<neon-owner-url>"; .venv\Scripts\python.exe -m app.seed
  3. Vercel: import the repo, set env vars DATABASE_URL and LLM_PROVIDER_ORDER=mock (keyless demo) — no READONLY_DATABASE_URL needed; execution falls back to SET LOCAL ROLE readonly_app (see Limits).

  4. Confirm https://<app>.vercel.app/health returns {"ok": true}.

Deploy (Render + Neon, free tier)

  1. Neon: create a free project, enable the vector extension, copy DATABASE_URL; run db/roles.sql once and set READONLY_DATABASE_URL with the readonly_app credentials.

  2. Render: new Web Service from this repo, Docker runtime, add the env vars, health check /health.

  3. First boot runs migrations and loads Chinook automatically (idempotent).

  4. Confirm https://<app>.onrender.com/health returns {"ok": true}.

  5. Rate limit stays on; the free Gemini quota is enough for demo traffic.

7. Tests

make test — no API keys, no network; guard tests need no database at all.

  • test_guard_dml — DELETE/UPDATE/INSERT/DROP and data-modifying CTEs are blocked as not_select.

  • test_guard_multiSELECT 1; DELETE … is blocked as multi_statement.

  • test_guard_limit — missing LIMIT injected, LIMIT 5000 lowered to 200, small limits untouched.

  • test_guard_systempg_catalog, information_schema, bare pg_* views blocked as system_table.

  • test_guard_denylistpg_sleep, pg_read_file, lo_import, dblink blocked as denied_function.

  • test_guard_allowlist — unknown tables blocked as unknown_table; quoted/cased spellings of allowed tables and CTE names pass.

  • test_guard_misc — unparseable input, comments, SELECT INTO, UNION handling.

  • test_repair_loop — a DB error triggers exactly one regenerate (the error is in the second prompt); a second error ends failed with both attempts in the response, the queries row, and the audit log; the happy path audits generate → guard → execute → summarize exactly once.

  • test_readonly_role — an INSERT as readonly_app raises InsufficientPrivilege against a real Postgres.

8. Keywords

chat to SQL, natural language to SQL, text-to-SQL guardrails, read-only database tool, SQL validation, MCP server, AI analytics assistant, PostgreSQL, sqlglot, FastAPI.

Available Tools

2 tools
describe_schemaA

Describe the demo database: tables, columns, foreign keys, sample rows.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the burden. It does disclose useful output content (sample rows) and the verb 'describe' suggests a read-only operation, but it does not explicitly state safety, side effects, or any limitations. The behavior is simple enough that this is adequate but not rich.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence that names the action and resource immediately and enumerates the included components without any filler. Every word contributes value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter introspection tool, the description is largely complete. It mentions sample rows beyond the schema basics, and an output schema exists to cover return structure. The only missing piece is explicit usage context relative to query_readonly, which is minor given the tool's simplicity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so per the baseline rule a score of 4 applies. There are no parameter semantics to clarify, and the description appropriately omits any parameter details.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Describe') and a specific resource ('the demo database'), and enumerates the exact content covered (tables, columns, foreign keys, sample rows). This clearly distinguishes it from the sibling query_readonly, which implies running queries rather than describing schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The tool's purpose is clear, but there is no explicit statement of when to use describe_schema versus the sibling query_readonly. Usage is implied by the description and the sibling's name, but the description does not explicitly say 'to run queries, use query_readonly instead'.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

query_readonlyA

Run one SELECT through the SafeSQL guard (read-only role, LIMIT capped at 200).

Anything that is not a single clean SELECT on allow-listed tables is blocked.

ParametersJSON Schema
NameRequiredDescriptionDefault
sqlYes

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully carries the burden of behavioral disclosure. It states the tool is read-only, enforces a LIMIT cap of 200, restricts to allow-listed tables, and blocks non-SELECT queries. This is substantial, though it does not cover error behavior or return format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is only two sentences, with the core purpose and key restrictions front-loaded. Every sentence adds essential information, and there is no redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has a single parameter and no output schema, the description covers the main usage constraints (read-only, LIMIT, allow-listed tables). It does not explicitly describe the response format or error handling, which could be important for an agent, but for a simple SELECT runner the provided information is fairly complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0% description coverage, so the description must compensate. It adds meaning to the 'sql' parameter by explaining it must be a single clean SELECT on allow-listed tables with a LIMIT cap, which is exactly the kind of semantic detail missing from the schema. It does not provide syntax examples, but the constraint knowledge is valuable.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool executes a SELECT query through a safety guard, and specifies concrete constraints (LIMIT 200, allow-listed tables). This distinguishes it from the sibling describe_schema, which is about schema metadata, not query execution.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies this tool is for running read-only SELECT queries, but it does not explicitly guide when to use it over describe_schema or mention any alternatives. The blocking of non-SELECT statements gives indirect usage hints, but no explicit when-to-use or when-not-to-use guidance is provided.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 2 tool updatesv0.1.0
    • First observeddescribe_schema
    • First observedquery_readonly

TDQS

A4.1/5.0

Scored across 2 tools

Disambiguation5/5

describe_schema and query_readonly have clearly distinct roles: one returns database metadata, the other executes read-only queries. No overlap exists, so an agent can select the intended tool without ambiguity.

Naming Consistency4/5

Both names use snake_case and a verb-first style, so the set is readable and predictable. However, query_readonly uses an adjective modifier rather than a noun object, which is a minor deviation from a strict verb_noun pattern.

Tool Count3/5

Two tools sit at the low end of a credible server surface, especially for anything beyond a simple demonstration. They are well-chosen for a minimal read-only SQL demo, but the set feels thin compared with most servers.

Completeness4/5

For a deliberately read-only SQL guard, schema description plus SELECT execution covers the core workflow and has no dead ends. Minor gaps include no explicit tool for inspecting allowed-table details or query errors, but the stated scope makes those optional.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables Claude Desktop to interact with MySQL, PostgreSQL, and Redis databases using natural language for data querying and schema analysis. It provides a secure interface with a default read-only mode to prevent unauthorized database modifications.
    48 npm
    929
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables Claude Desktop to interact with PostgreSQL databases through natural language for schema exploration, data analysis, and query execution. Users can search schemas, describe tables, and perform read or write operations without needing to write manual SQL.
    -
  • A
    license
    A
    quality
    C
    maintenance
    Provides Claude with read-only access to local development databases (Postgres, MySQL, SQLite) to inspect schemas, run SELECT queries, and explain query plans without leaving the conversation.
    5
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables Claude Code to securely interact with PostgreSQL, MySQL, SQLite, and SQL Server databases, featuring read-only mode, query validation, SSH tunneling, and field redaction for production-safe data access.
    2
    MIT