SQL Check
Runs the agent's SQL on a real in-memory PostgreSQL 18 engine (PGlite, compiled to WebAssembly) after applying a supplied schema (DDL, migrations, sample INSERTs). Each statement gets PostgreSQL's own verdict: errors with line, column, SQLSTATE code and hints, constraint violations with the failing row, or the rows returned. MySQL/SQL Server syntax that PostgreSQL rejects is flagged with the accepted PostgreSQL form, and dialect comparison against SQLite highlights behavioral differences.
Runs the agent's SQL on a real in-memory SQLite 3.49 engine (sql.js) after applying a supplied schema. Returns each statement's outcome — errors with line and column, constraint violations, or result rows — and in dialect: both mode compares SQLite's behavior against PostgreSQL, surfacing quiet differences such as self-filling INTEGER PRIMARY KEY and unenforced foreign keys.
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., "@SQL Checkcheck this on postgres and sqlite: SELECT * FROM users LIMIT 0, 10"
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.
SQL Check
Agents write SQL for databases they cannot reach, so they guess: whether PostgreSQL accepts a
GROUP BY, what a column is called, whether DATE_SUB exists, what round(2.5) returns. SQL Check
stops the guessing. It runs your SQL on the real engines, in memory: PostgreSQL 18 (PGlite, the
PostgreSQL server compiled to WebAssembly) and SQLite 3.49 (sql.js). No database server, no network.
Your schema first: CREATE TABLE statements, migrations, sample rows. Then each statement gets the engine's own verdict: the error with its line and column and PostgreSQL's hint ("Perhaps you meant to reference the column users.email"), the constraint it violates with the failing row, or the rows it returns.
Every statement is checked, not just the first: each runs in its own savepoint inside one transaction that is rolled back, so an error does not hide the next one and nothing persists.
SQL from other databases: when MySQL or SQL Server syntax fails (backticks,
DATE_SUB,LIMIT 0, 10,AUTO_INCREMENT,TOP n), the error says what the engine accepts instead.Portability:
dialect: bothruns the SQL on PostgreSQL and SQLite and lists where they disagree, including the quiet differences: SQLite fills anINTEGER PRIMARY KEYby itself and does not enforce foreign keys unless asked; PostgreSQL does neither and does.
No key needed.
Built and maintained by Arhan Canli.
Install
Needs Node.js 20 or newer. No account or key.
Claude Code
claude mcp add sql-check -- npx -y sql-check-mcpClaude Desktop: download sql-check-mcp-<version>.mcpb from the latest release and open it. The bundle is signed; verify it with gh attestation verify <file> --repo arhancanli/sql-check-mcp.
Any other client (Windsurf, Zed, Cline, Continue and others), in its MCP config file:
{
"mcpServers": {
"sql-check": {
"command": "npx",
"args": [
"-y",
"sql-check-mcp"
]
}
}
}Docker
docker build -t sql-check-mcp https://github.com/arhancanli/sql-check-mcp.git && docker run -i --rm sql-check-mcpHosted (Streamable HTTP): node src/server.mjs --http serves stateless MCP at POST /mcp (port from PORT, default 3000).
Related MCP server: SQLike
Example
An agent calls check_sql with:
{
"schema": "CREATE TABLE users (\n id serial PRIMARY KEY,\n email text NOT NULL UNIQUE,\n created_at timestamptz DEFAULT now()\n);\nCREATE TABLE orders (\n id serial PRIMARY KEY,\n user_id int NOT NULL REFERENCES users(id),\n total numeric(10,2) NOT NULL,\n status text NOT NULL CHECK (status IN ('open', 'paid'))\n);\nINSERT INTO users (email) VALUES ('ana@example.com'), ('ben@example.com');\nINSERT INTO orders (user_id, total, status) VALUES (1, 10.50, 'open'), (1, 3.00, 'paid');\n",
"sql": "-- spend per user\nSELECT u.email, sum(o.total) AS spent\nFROM users u LEFT JOIN orders o ON o.user_id = u.id\nGROUP BY u.email\nORDER BY u.email;\nSELECT u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.email;\nSELECT emial FROM users;\nSELECT * FROM orders WHERE placed_at > DATE_SUB(NOW(), INTERVAL 7 DAY);\nINSERT INTO orders (user_id, total, status) VALUES (1, 5.00, 'shipped');\nINSERT INTO orders (user_id, total, status) VALUES (9, 5.00, 'open');\nUPDATE orders SET status = 'paid' WHERE status = 'open' RETURNING id, status;\n"
}and gets back (recorded from the live server on 2026-09-27):
{
"results": [
{
"dialect": "postgres",
"version": "PostgreSQL 18.3",
"counts": {
"ok": 2,
"error": 5
},
"statements": [
{
"line": 2,
"sql": "SELECT u.email, sum(o.total) AS spent FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP B...",
"status": "ok",
"command": "SELECT",
"columns": [
"email text",
"spent numeric"
],
"row_count": 2,
"rows": [
[
"ana@example.com",
"13.50"
],
[
"ben@example.com",
null
]
]
},
{
"line": 6,
"sql": "SELECT u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id GROUP BY u.email",
"status": "error",
"column": 17,
"error": "column \"o.total\" must appear in the GROUP BY clause or be used in an aggregate function",
"code": "42803"
},
{
"line": 7,
"sql": "SELECT emial FROM users",
"status": "error",
"column": 8,
"error": "column \"emial\" does not exist",
"hint": "Perhaps you meant to reference the column \"users.email\".",
"code": "42703"
},
{
"line": 8,
"sql": "SELECT * FROM orders WHERE placed_at > DATE_SUB(NOW(), INTERVAL 7 DAY)",
"status": "error",
"column": 65,
"error": "syntax error at or near \"7\"",
"code": "42601",
"advice": "DATE_SUB/DATE_ADD are MySQL; PostgreSQL writes now() - interval '1 day'"
},
{
"line": 9,
"sql": "INSERT INTO orders (user_id, total, status) VALUES (1, 5.00, 'shipped')",
... (35 more lines)Tools
Tool | What it does |
| Runs SQL on real PostgreSQL 18 or SQLite 3.49 in memory, after your schema (DDL, migrations, sample INSERTs). Each statement gets the engine's verdict: error with line, column and hint, constraint violations, or the rows it returns (up to max_rows). All inside a rolled-back transaction. dialect both compares the two. |
How it behaves
Nothing leaves your machine: the engines run inside this process, in memory, with in-memory file systems; there is no network access at all (
factory.allowHostsis empty).Each call starts from an empty database. PostgreSQL runs every statement in its own savepoint inside one transaction that is rolled back, then discards the session; SQLite gets a new database per call. Statements that would end that transaction (
BEGIN,COMMIT,ROLLBACK) are skipped and say so.The engines run in a worker thread with a time limit (10 s per call,
SQL_CHECK_TIMEOUT_MSto change it): a statement that never ends is stopped, reported with its number, and the engine is replaced. They start in the background when the server starts, so the first call does not wait for PostgreSQL to boot.Results are compact JSON with a matching output schema: up to
max_rowsrows per statement (default 20, with the full count), long values cut at 200 characters, bigints as strings.
Benchmark
Not yet measured.
Performance
Measured 2026-09-27 from Dubai, home connection against the live upstream, Node 24.19.0 (bench/perf.json, scripts/perf.mjs in the factory).
Call | First call | Repeat | Result size |
check_sql: eight statements against a schema with sample rows, on PostgreSQL | 827 ms | 7.7 ms | 1,855 chars |
check_sql: the same SQL on PostgreSQL and SQLite, compared | 825 ms | 4.1 ms | 1,918 chars |
check_sql: a migration step that cannot run inside a transaction | 892 ms | 2.9 ms | 692 chars |
First call: a fresh server process, including the TLS connection and the upstream's own time. Repeat: the same call again, answered from the in-process cache, so it shows this server's own overhead.
Tool definitions the model reads on every turn (name, description, input schema): 800 characters. The full tool list, with the output schemas and annotations clients use to validate results, is 1,357 characters.
More MCP servers by Arhan Canli
Actions Check: Checks GitHub Actions workflows: outdated actions, old Node runtimes, retired runners, injection.
Config Check: Validates config files against their official schemas: tsconfig, compose, workflows, 1,400+ more.
Cron Check: Explains cron expressions, lists next run times in any time zone, converts between cron dialects.
Dockerfile Check: Checks Dockerfiles: build-breaking mistakes, base image tags that exist, digests, platforms, EOL.
Domain Health: Email and domain checks: SPF lookup limits, DKIM keys, DMARC, DNS records, registration expiry.
End of Life: Is this version still supported? EOL dates, latest patch and upgrade target for 470+ products.
Internet Standards: RFC sections, status, obsoleted-by chains, errata and IANA registries for coding agents.
Kube Check: Checks Kubernetes manifests for your version: removed APIs, unknown fields, Pod Security, risks.
The whole collection, 12 more
License
MIT, Copyright (c) 2026 Arhan Canli.
Available Tools
1 toolcheck_sqlRun SQL on real PostgreSQL or SQLiteARead-onlyIdempotent
Runs SQL on real PostgreSQL 18 or SQLite 3.49 in memory, after your schema (DDL, migrations, sample INSERTs). Each statement gets the engine's verdict: error with line, column and hint, constraint violations, or the rows it returns (up to max_rows). All inside a rolled-back transaction. dialect both compares the two.
| Name | Required | Description | Default |
|---|---|---|---|
| sql | Yes | the statements to check | |
| schema | No | CREATE TABLE statements, migrations and sample rows, run first | |
| dialect | No | default postgres | |
| max_rows | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| results | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly/idempotent/non-destructive, and the description adds real context beyond them: everything runs 'inside a rolled-back transaction' (no persistent side effects), the engines run in memory, and each statement returns a verdict with line/column/hint or constraint violations. Only the max_rows truncation behavior is mentioned without a default.
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?
Two dense sentences with zero filler. The primary behavior (what it runs on, on what engines) is front-loaded, followed by execution semantics and the dialect comparison.
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?
With annotations and an output schema present, the description needn't explain return values, and it covers scope, transaction safety, and error reporting. It omits only secondary details like the max_rows default and dialect default, which the schema partially covers.
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?
Schema coverage is 75% and the schema already documents dialect's enum and max_rows' bounds. The description nonetheless adds meaning: 'schema' is described as DDL/migrations/sample rows run first, and 'dialect both compares the two' explains the third enum value's semantics beyond the raw enum list.
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?
Specific verb plus resource with concrete engine versions: 'Runs SQL on real PostgreSQL 18 or SQLite 3.49 in memory.' It immediately conveys the validation/execution purpose and that the schema is applied first, so an agent knows exactly what the tool does.
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?
Usage is implied rather than stated: the description frames the tool as running statements after a schema (DDL/migrations/sample INSERTs) and hints that 'dialect both compares the two.' There are no siblings to disambiguate against and no explicit when-not guidance, so this sits at implied-usage level.
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 tool update
v0.1.0- First observed
check_sql
TDQS
Scored across 1 tool
There is only one tool, so there is no possibility of selecting the wrong one. Its purpose (validating/running SQL statements against real engines) is unambiguous.
The single name check_sql follows a clear verb_noun convention. No mixed conventions are present since there is only one tool.
A single tool is thin against the typical 3-15 range, though the server's scope (execute and report on SQL) is narrow enough that one entry point is defensible. It packs multiple modes (dialect comparison, max_rows, rollback) into one surface.
For a stateless SQL execution/validation service the core operation is covered, including error reporting, constraints, and dialect comparison. Minor extras like EXPLAIN/plan output or schema-diff tooling are absent but not essential.
Maintenance
Related MCP Connectors
Executes SQL in a real ephemeral database: rows, typed errors with suggestions, plans, diffs.
Generate, fix, explain and run read-only SQL on PostgreSQL, MySQL and SQL Server
Query PostgreSQL databases in plain English — LLM-generated, safety-validated SQL.
Deterministic safety, correctness & cost gate that vets Postgres SQL before your AI agent runs it.
Related MCP Servers
- AlicenseNot gradedqualityNot gradedmaintenanceAnalyzes SQL queries for performance issues, provides optimization suggestions with automated rewriting, and recommends indexes across multiple database dialects (PostgreSQL, MySQL, Oracle, SQL Server).1MIT
- AlicenseNot gradedqualityAmaintenanceDeterministic SQL static analysis and query-equivalence checking for Postgres, MySQL, SQLite, and SQL Server. Tokenizes locally before forwarding to the API.6Apache 2.0
- AlicenseNot gradedqualityBmaintenanceGives AI coding assistants, IDEs, and CI full PostgreSQL schema intelligence from an offline snapshot, enabling linting, query validation, migration safety analysis, and foreign key graph exploration without ever exposing database credentials.35BSD 2-Clause "Simplified"
- AlicenseAqualityDmaintenanceEnables AI agents to format SQL, explain queries in plain English, analyze schemas, build queries from natural language, and generate migrations, all without requiring a database connection.529 npmMIT