SafeSQL
Provides tools to describe the schema and execute read-only SELECT queries against a PostgreSQL database, with deterministic guardrails, limits, timeouts, and audit logging.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SafeSQLlist the tables and count the invoices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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_appinside aREAD ONLYtransaction 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
failedanswer showing both attempts.Every question is logged to a
queriestable, and every stage (generate, guard, execute, summarize) writes an audit row.MCP server (
mcp_server.py): toolsdescribe_schemaandquery_readonlyreuse 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:

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

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

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

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 --> APIThe 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), noSELECT ... INTO(select_into).Denied functions:
pg_sleep,pg_read_file,lo_import,dblink(denied_function).No
pg_catalog,information_schema, orpg_*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
LIMIT→LIMIT 200injected;LIMIT > 200→ lowered. Every rewrite is returned and shown in the UI.Even past the guard: SELECT-only role,
READ ONLYtransaction, 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_URLis not set (managed Postgres with a single connection string, e.g. Neon), execution connects withDATABASE_URLand drops privileges withSET LOCAL ROLE readonly_appinside theREAD ONLYtransaction — 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:8000Or 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.
Neon: create a free project, copy the pooled
DATABASE_URL.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.seedVercel: import the repo, set env vars
DATABASE_URLandLLM_PROVIDER_ORDER=mock(keyless demo) — noREADONLY_DATABASE_URLneeded; execution falls back toSET LOCAL ROLE readonly_app(see Limits).Confirm
https://<app>.vercel.app/healthreturns{"ok": true}.
Deploy (Render + Neon, free tier)
Neon: create a free project, enable the
vectorextension, copyDATABASE_URL; rundb/roles.sqlonce and setREADONLY_DATABASE_URLwith thereadonly_appcredentials.Render: new Web Service from this repo, Docker runtime, add the env vars, health check
/health.First boot runs migrations and loads Chinook automatically (idempotent).
Confirm
https://<app>.onrender.com/healthreturns{"ok": true}.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 asnot_select.test_guard_multi—SELECT 1; DELETE …is blocked asmulti_statement.test_guard_limit— missing LIMIT injected,LIMIT 5000lowered to 200, small limits untouched.test_guard_system—pg_catalog,information_schema, barepg_*views blocked assystem_table.test_guard_denylist—pg_sleep,pg_read_file,lo_import,dblinkblocked asdenied_function.test_guard_allowlist— unknown tables blocked asunknown_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 endsfailedwith both attempts in the response, thequeriesrow, and the audit log; the happy path audits generate → guard → execute → summarize exactly once.test_readonly_role— an INSERT asreadonly_appraisesInsufficientPrivilegeagainst 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 toolsdescribe_schemaA
Describe the demo database: tables, columns, foreign keys, sample rows.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
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.
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.
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.
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.
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.
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.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes |
TDQS
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.
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.
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.
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.
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.
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.
2 tool updates
v0.1.0- First observed
describe_schema - First observed
query_readonly
TDQS
Scored across 2 tools
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.
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.
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.
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
Related MCP Connectors
Query 40 databases from Claude, ChatGPT, or Cursor — on any device. Read-only, encrypted, audited.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Query your Postgres from ChatGPT or Claude without exposing the database or handing over credentials. Run npx boltschema connect next to your database and it dials out over HTTPS — no inbound firewall rule, no open port, works with localhost and VPC-private databases. Read-only is enforced by a SQL guard, a Postgres READ ONLY transaction, and a scoped role generated for you.
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables 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 npm929MIT
- FlicenseNot gradedqualityDmaintenanceEnables 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.-
- AlicenseAqualityCmaintenanceProvides 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.5MIT
- AlicenseNot gradedqualityAmaintenanceEnables 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.2MIT