Skip to content

About

Runs SQL on real PostgreSQL and SQLite in memory: your schema, the database's own errors, results.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

SQL Check

CI npm downloads OpenSSF Scorecard License: MIT

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: both runs the SQL on PostgreSQL and SQLite and lists where they disagree, including the quiet differences: SQLite fills an INTEGER PRIMARY KEY by itself and does not enforce foreign keys unless asked; PostgreSQL does neither and does.

No key needed.

Built and maintained by Arhan Canli.

Install

Install in Cursor Install in VS Code Install in Goose

Needs Node.js 20 or newer. No account or key.

Claude Code

claude mcp add sql-check -- npx -y sql-check-mcp

Claude 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-mcp

Hosted (Streamable HTTP): node src/server.mjs --http serves stateless MCP at POST /mcp (port from PORT, default 3000).

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 ('[email protected]'), ('[email protected]');\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": [
            [
              "[email protected]",
              "13.50"
            ],
            [
              "[email protected]",
              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
check_sql 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.allowHosts is 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_MS to 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_rows rows 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.

About

Runs SQL on real PostgreSQL and SQLite in memory: your schema, the database's own errors, results.

Topics

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages