Skip to main content
Glama

DbRex

Query databases from .sql files in VSCode, and let an AI work the same connections through MCP.

Deep beta. This is early, pre-release software under active development. Interfaces, the connection file format and the on-disk layout of ~/.dbrex can all change without a migration path. There is no published release yet — you build it from source. Do not point it at anything you cannot afford to have a query run against.

Connections, credentials and results live in a small local daemon (dbrexd), not in the editor. The editor is one client; a terminal is another; an AI agent over MCP is a third. Closing the window closes a display, not the machinery — an agent keeps working.

  • Ctrl+Enter runs the statement under the cursor, Ctrl+Shift+Enter the file.

  • -- @conn: name and -- @limit: n steer individual statements.

  • -- @kind: mysql and friends let a file carry a whole connection, for a database that is not worth a config entry.

  • DbRex: Copy MCP Setup Command gives you the one line that connects an agent.

  • Passwords are never accepted from an agent: the daemon routes the prompt to this window or to a terminal, and only those may answer.

MySQL, PostgreSQL, SQL Server, ClickHouse, Trino (including SSO), S3-compatible object stores, Kafka, Elasticsearch with OpenSearch, and a plain local directory.

Several engines need no connector of their own, because they already speak one of these wires. RisingWave serves the PostgreSQL protocol, so kind: postgres on port 4566 is the whole of it. Azure SQL, Synapse and Fabric are all kind: mssql. StarRocks and Apache Doris speak the MySQL protocol, so kind: mysql reaches them on port 9030. Amazon Redshift answers a PostgreSQL-derived wire on 5439 and kind: postgres connects in practice — though AWS does not document that path and steers you to its own drivers, so treat it as working rather than supported.

Buckets browse out of the box over plain HTTPS. Querying files needs DuckDB, which is a 70 MB native component, so it is not bundled — run dbrex install-duckdb once if you want it.

Install

curl -sSL https://raw.githubusercontent.com/gr4c2-2000/dbrex/main/install.sh | sh

Checks for a Node 18 or newer, builds from source, installs the dbrex command into ~/.dbrex/bin and links it into ~/.local/bin, then installs the VSCode extension if it finds an editor. --cli-only skips the extension.

The CLI half needs no editor. That is deliberate: the daemon is meant to outlive the editor, so it has to be installable without one.

From a clone:

./install.sh

Related MCP server: MCP Database Query Server

Layout

packages/core        shared types, connection specs, SQL statement splitting
packages/daemon      dbrexd — sessions, providers, secret vault, result store
packages/client      the protocol clients speak to the daemon
packages/cli         dbrex — terminal client and the MCP stdio bridge
packages/extension   the VSCode extension, which ships the daemon and the CLI
                     (its `webview/` holds the result panel front-end)

Build

npm install
npm run typecheck
npm test
npm run build

Package the extension:

cd packages/extension
npx @vscode/vsce package --no-dependencies --allow-missing-repository --out dbrex.vsix
code --install-extension dbrex.vsix

CLI

dbrex shell [conn]                 interactive session; Tab completes from the server
dbrex status                       is the daemon up, is the vault unlocked
dbrex connections                  list connections and what they are for
dbrex query <conn> <sql>           run one statement and print the rows
dbrex query [conn] -f <file.sql>   run a file; the name is optional when the
                                   file defines its own connection
dbrex browse <conn> [path...]      walk the schema tree
dbrex unlock                       unlock the secret vault for this daemon
dbrex set-password <conn>          store a password for a connection
dbrex results [n]                  recent stored results
dbrex install-duckdb               add DuckDB, needed to query object stores
dbrex mcp                          serve MCP over stdio (for AI agents)
dbrex stop                         stop the daemon

Output

A terminal gets a table sized to it, with numbers right-aligned and NULL dimmed so it cannot be mistaken for the string "NULL". A pipe gets TSV and no summary line, because the next thing in the pipe is a program. --format overrides either: table, json, csv, tsv or vertical.

dbrex query prod "SELECT ..." --format json | jq '.[0].hits'
dbrex query prod "SELECT ..." --format vertical    # one field per line

A table that does not fit shrinks its widest columns and marks what it cut. It never drops a column: a missing column reads as "the query returned no such column", which is not a thing a database tool may imply.

Verifying an installation

npm test proves the code is self-consistent. make verify proves two things it cannot, in containers that have never seen dbrex:

  • make verify-install runs install.sh on a machine with no build and nothing on PATH, then checks that dbrex answers — including after its build directory is deleted, and when it is installed a second time.

  • make verify-run queries real MySQL, PostgreSQL, ClickHouse, Redpanda and MinIO. The Kafka half reads one topic per compression codec, because that is where a reader actually breaks.

  • make verify-risingwave does the same against RisingWave, which shares the PostgreSQL provider. Opt-in, and not part of make verify, because the image is 11 GB — but it is the only thing that can prove the shared introspection still works on both engines.

They are separate because a working build is no evidence that an installation works. Requires Docker; everything they start is thrown away afterwards.

make verify

The Connections view

Named for what it holds. It lists connections and expands into whatever each one has underneath — databases and tables for a relational engine, buckets and objects for a store, topics for Kafka, indices for Elasticsearch. "Schema" was accurate for the first two engines and wrong for the rest.

The view's title bar adds a connection, searches, and refreshes. Each connection row carries three actions:

  • Add Password stores a credential in the daemon's vault

  • Change a Setting asks the provider which options it accepts and edits the one you choose, in the file the connection came from

  • Delete Connection removes the entry, after a confirmation naming the file

A connection declared inside a .sql file by its own -- @kind directives has no entry to edit or delete; the statement that defines it is the place to change it, and the view says so rather than failing.

Add Connection opens a form in its own editor tab. Everything is on screen at once: the kinds the daemon supports, that engine's own fields, where the file goes, and a password box for engines that take one. Nothing is committed until Save, and a mistake in one field does not mean starting over.

The fields come from what the daemon says each provider accepts, so a provider added later gets a working form with no UI code of its own. Scope is a choice between ~/.dbrex/connections.json and the workspace's own .dbrex/connections.json — which you can commit — and each option shows the path it writes to rather than describing it. A password typed here goes to the daemon's vault; the file records only that the credential comes from there.

It replaced a chain of eight sequential pickers. Those could not be reviewed, could not be revisited, and a wrong answer on the third question meant answering the first two again.

Workspaces

A workspace decides which connections exist. The wrong one does not fail — it makes the right connection missing, which reads as though DbRex lost a database.

DbRex: Select Workspace offers the folders this window has open together with the workspaces the daemon is already serving for other windows and agents, and says how many connections each one reaches. The choice applies at once and survives the daemon idling out; it is deliberately not remembered across a window reload, because which environment a window points at should not come back unasked.

An agent gets the same choice through list_workspaces and use_workspace, so one MCP bridge can be pointed at a different checkout mid-session instead of being restarted. .claude/skills/dbrex/SKILL.md tells it to ask rather than assume.

Two lanes in the results panel

An agent and a person share one results panel. They no longer share one slot: the panel has a Mine and an Agent tab, and a query one of them runs cannot replace what the other is reading. A result arriving in the lane you are not looking at marks its tab rather than pulling you to it. Running a query yourself does switch to your own lane, because that is what you just asked to see.

The tabs stay hidden until an agent has actually run something.

Where a result came from

Every stored result records which kind of client ran it, so one history written by three clients stays legible:

$ dbrex results
8f2a…  pin  mcp     1 203 rows  prod  SELECT day, count(*) FROM events …
41b9…       vscode     18 rows  prod  SELECT * FROM users WHERE id = 1
c7d0…       cmd           1 row  prod  SELECT version()

The same label appears in the Results view in the editor, with the client's own name in the tooltip.

Configuration

Connections live in ~/.dbrex/connections.json, or in a workspace's own .dbrex/connections.json. Passwords do not: they go to the daemon's encrypted vault, an environment variable, or a command (op read ...), whichever the connection declares.

PostgreSQL and RisingWave

One provider serves both. What that costs is written into the introspection: it stays on pg_catalog and calls no server-side function, because that is the subset both engines implement. information_schema would have been the more standard choice and is the wrong one — PostgreSQL omits materialized views from it entirely, and in RisingWave a materialized view is the main thing anyone wants to look at.

The browse tree starts at schemas rather than databases. A PostgreSQL session is bound to one database and cannot join across them, so a tree offering the others would list tables that session cannot query.

{ "name": "warehouse", "kind": "postgres",
  "host": "db.internal", "port": 5432, "user": "analyst",
  "database": "analytics", "sslmode": "verify-full",
  "secret": { "from": "command", "argv": ["op", "read", "op://work/warehouse/password"] } }

sslmode takes the libpq spellings: disable (the default, for a container on localhost), require to encrypt without judging the certificate, verify-ca to check the chain, verify-full to check the hostname as well. Through an SSH tunnel the name verified is still the real one, not the local end.

For RisingWave the only differences are the port and the database name:

{ "name": "stream", "kind": "postgres",
  "host": "localhost", "port": 4566, "user": "root", "database": "dev" }

Materialized views appear in the tree marked as such, and CREATE MATERIALIZED VIEW, CREATE SOURCE and CREATE SINK are offered by completion.

Kafka

A cluster explores like a schema: topics at the top, and expanding one shows its fields, worked out from a sample of its messages. Clicking a topic gives you a statement you can read.

{ "name": "bus", "kind": "kafka", "brokers": "kafka-1:9092,kafka-2:9092" }
SELECT *
FROM "order-events"
LIMIT 100

A topic is a table. The messages a statement needs are pulled into DuckDB and the statement then runs verbatim, so a JSON payload arrives as typed columns rather than as bytes:

SELECT _partition, count(*) AS n, max(_offset) AS latest
FROM order_events
GROUP BY _partition;

Every message carries _partition, _offset, _timestamp and _key beside its own fields. A payload field of the same name wins — it is your data.

Browsing needs nothing installed: the tree comes from KafkaJS, which is pure JavaScript. Querying needs DuckDB, the same 70 MB as the object store, and the same dbrex install-duckdb.

A topic's window is read once per session: the first query on a topic pays for the read, the next one answers from what is already there. Expanding a topic in the tree pays separately, for a smaller sample.

On a cluster of more than 64 topics the listing shows partitions but no message counts. Kafka has no bulk size call — it is one round trip per topic, and a real cluster of 515 answered 29 counts a second however hard it was asked, so counting them all cost twenty seconds of a sidebar. Below that threshold the counts are there, which is where "0 messages" is worth seeing.

What this is not is a streaming consumer. Every query reads a bounded window — by default the last 1000 messages of the topic, spread across its partitions so one hot partition does not eat the whole budget — and nothing is kept between sessions. Nothing is ever committed: offsets come from seeking to a computed position, never from a consumer group's memory.

Option

brokers

host:9092,host2:9092. Required

user + password

SASL. Omit both for an unauthenticated cluster

saslMechanism

plain, scram-sha-256, scram-sha-512

ssl

TLS. SASL_SSL is this plus a user

format

json infers the fields; text keeps the body in one column

startPosition

latest reads the tail, earliest the head

sampleMessages

How many messages a query pulls per topic. Default 1000

Compressed topics read: gzip, snappy, lz4 and zstd. KafkaJS implements only gzip, so the other three are decompressed here, in pure JavaScript — except zstd, which uses Node's own and therefore needs Node 22.15 or newer.

A tunnel does not apply to this kind, and the daemon says so rather than pretending: a cluster answers metadata with its own advertised listeners, and the client goes there next.

For a topic that has to land somewhere durable and keep up, this is the wrong tool and an engine built for it is the right one — RisingWave's CREATE TABLE ... WITH (connector = 'kafka'), reachable through the postgres provider above.

A local directory

Give it a path. It indexes the files, shows them in the Connections view, and lets you write SQL against them — the same DuckDB machinery the object-store provider uses, pointed at the folder the files are actually in while you are still working on them.

{
  "name": "files",
  "kind": "dir",
  "options": { "path": "$home/data", "include": "*.parquet" }
}

Only path is required. recursive is on by default, include filters what gets indexed by a */? glob, and maxFiles bounds the walk.

The index is queryable. The walk produces a dbrex_files view, so the folder itself is something you can ask questions about:

-- what is in here, and how big
SELECT extension, count(*) AS files, sum(size) AS bytes
FROM dbrex_files
GROUP BY extension
ORDER BY bytes DESC
-- and then read the data, with whichever reader fits
SELECT * FROM read_parquet('/data/events/day=2026-10-01/part-0.parquet')

Clicking a file in the tree hands you a statement that reads it, with the reader chosen from the extension — read_parquet, read_json_auto, or DuckDB sniffing a delimited file.

A statement cannot leave the directory. DuckDB will read any path it is given, so the root is enforced rather than merely displayed: the allowed directory is set, external access is switched off, and the configuration is then locked so a statement cannot widen it again. Reading a file outside the root, or /etc/passwd, comes back as a permission error. The order of those three settings matters — two of them achieve nothing on their own — which is why it is verified against a real DuckDB in make verify rather than assumed.

Symlinks are indexed as neither files nor directories: a link out of the root would describe files the confinement refuses to read, which is a tree that lies.

Browsing needs nothing installed. Querying needs DuckDB, which is the same dbrex install-duckdb the object store asks for.

SQL Server, Azure SQL, Synapse and Fabric

One kind: mssql for all four: the same TDS wire, the same T-SQL, the same INFORMATION_SCHEMA.

{
  "name": "warehouse",
  "kind": "mssql",
  "secret": { "from": "vault" },
  "options": {
    "host": "sql.example",
    "port": 1433,
    "user": "$user",
    "database": "analytics",
    "encrypt": true
  }
}

Fabric and Entra-only servers cannot use a password. Microsoft's own documentation says SQL authentication is unsupported there. Set auth to token and let the secret source produce one, which makes the Azure CLI the whole of the credential:

{
  "name": "fabric",
  "kind": "mssql",
  "secret": {
    "from": "command",
    "argv": ["az", "account", "get-access-token",
             "--resource", "https://database.windows.net/",
             "--query", "accessToken", "-o", "tsv"]
  },
  "options": {
    "host": "xxx.datawarehouse.fabric.microsoft.com",
    "database": "mywarehouse",
    "auth": "token"
  }
}

For a local container, trustServerCertificate: true accepts a self-signed certificate. Do not set it against a managed service — it turns off the check that the server is the one you meant.

The default row limit is appended as SELECT TOP n, not FETCH FIRST n ROWS ONLY. T-SQL only allows FETCH after an OFFSET, and OFFSET only after an ORDER BY, so the standard clause would be a syntax error on an unordered statement rather than a smaller result. A statement with UNION, EXCEPT or INTERSECT is left alone and the rows are bounded on the way out instead: a TOP would bind to one branch and quietly limit the wrong thing.

One kind: elasticsearch for both. Which fork a cluster is comes from asking it at connect, not from declaring it: OpenSearch serves its SQL on /_plugins/_sql and answers in a different shape, and a user who has to get that right by hand will sometimes get it wrong.

{
  "name": "logs",
  "kind": "elasticsearch",
  "secret": { "from": "vault" },
  "options": {
    "host": "elastic.example",
    "port": 9200,
    "protocol": "https",
    "user": "$user"
  }
}

Leave user out for a cluster with security disabled; no credential is sent and nothing prompts. Set index to pin a connection to one index — it is then the only one browsed, and a Query DSL body needs no path.

Statements come in two kinds and the right one is chosen by looking at them.

-- SQL, on any cluster whose SQL surface is enabled
SELECT kind, count(*) AS n
FROM "logs-2026.10.01"
GROUP BY kind
POST /logs-2026.10.01/_search
{ "query": { "match": { "message": "timeout" } }, "size": 50 }

Anything starting with {, GET or POST is a Query DSL request, written the way Kibana's console writes it. Everything else is SQL. The DSL path needs nothing installed, which is the path for a cluster too old for SQL or without the plugin.

On licensing. Elastic puts "Elasticsearch SQL APIs & CLI" in the free Basic tier and its JDBC and ODBC drivers behind a paid one, so DbRex reaches SQL over HTTP on clusters where a JDBC-based tool cannot. OpenSearch gates nothing: its SQL plugin is Apache-2.0 and ships in every distribution but the minimal one.

Documents become rows by taking the union of the fields a page actually has, in first-seen order, with _index, _id and _score in front. A nested object stays a value rather than being flattened into invented columns. The schema tree reads the mapping, so it works on a cluster with no SQL at all, and it offers .keyword multi-fields because text is not aggregatable and the keyword is what a GROUP BY needs.

Elasticsearch SQL is a small dialect: no joins, and one index or pattern per statement. A statement it rejects comes back with the engine's own reason.

A connection the file carries itself

A container you started this morning and will throw away this afternoon does not deserve an entry in a config file. -- @kind: says the comments above a statement describe a connection, and every other directive that is not @conn, @limit or @password is an option of that kind:

-- @kind: mysql
-- @host: 127.0.0.1
-- @port: 3306
-- @user: root
-- @password: $env:MYSQL_ROOT_PASSWORD
-- @database: app

SELECT id, email FROM users ORDER BY id DESC;

Running that works in the editor and from the terminal, where the name on the command line becomes optional:

dbrex query -f scratch.sql

The options each kind accepts are the ones dbrex connections documents for it, and a mistake is reported as a config error before anything connects, listing every bad option at once. Without -- @conn: the connection is named after where it points — mysql/127.0.0.1:3306 — which is what shows up in the results history; add -- @conn: docker to name it yourself.

Nothing about it is written anywhere. It belongs to the workspace the file is in — this window, a terminal in the same repository and an agent working on it all see it, so "run this file for me" needs no configuration on the agent's side; list_connections shows it the same as any other. It is forgotten once the last client for that workspace disconnects, and its password never reaches the vault. Editing the directives and running again reconnects; running again unchanged keeps the session it already had.

A few things worth knowing before putting a password in a file:

  • Whoever can read the .sql file can read the password, and git is very good at remembering files. $env:NAME is read from the daemon's environment and keeps the secret out of the file.

  • An agent may use a connection the workspace defined, and may define one itself, but it may never supply the password for one — the same rule as everywhere else. What it never gets is the password's value: it queries through the daemon, which holds the credential the file gave it.

  • Leave @password out and the daemon asks this window or a terminal for one, exactly as it would for a configured connection.

License

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants and IDEs to execute SQL queries on local DuckDB databases, in-memory databases, or cloud-stored databases with support for flexible connections and configurable result limits.
    1
    1
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables AI agents to query and explore databases including SQLite, PostgreSQL, MySQL, and SQL Server through a secure, read-only workflow. It provides tools for listing connections, inspecting table schemas, and executing SELECT statements directly within VS Code.
    1
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Query local CSV, Parquet, JSON and TSV files with real SQL via DuckDB. Gives your AI coding tool ground-truth data access instead of hallucinated answers.
    4
    MIT
  • F
    license
    Not graded
    quality
    A
    maintenance
    Connects AI coding assistants like Claude Code, Cursor, and Windsurf to enterprise data sources (databases, files, APIs, etc.) via a single binary, with 45+ connector categories and built-in memory tools.
    1
    -