CoraDB MCP Server
Official# CoraDB MCP Server
An [MCP](https://modelcontextprotocol.io) server for CoraDB. Any MCP client — an
AI app such as Claude, an IDE, a script or another program — can use it to read
a database's schema, run SELECTs
(relational and `SELECT … FROM MATCH` graph queries) and run other statements,
and get back exactly what the engine returns.
- **Stateless.** Every call names its database and carries its own login. The
server opens a connection for the call, runs it, commits or rolls back,
closes the connection and forgets the login. Nothing is kept between calls:
no connection, no session, no password.
- **One transaction per call.** All statements of a call succeed and are
committed together, or nothing is saved.
- **The server setting has the last word.** `CORADB_WRITE_MODE` decides
whether changes can be saved at all; a call can only be stricter.
## Quick start
Needs [Node.js](https://nodejs.org) 20.6 or later and a running CoraDB.
Nothing to install by hand: MCP clients start the server with `npx`, which
downloads [`@coradb/coradb-mcp-server`](https://www.npmjs.com/package/@coradb/coradb-mcp-server)
from npm the first time.
For the Claude desktop app: Settings → Developer → Edit Config, add this to
`mcpServers`, then quit the app completely and reopen it:
```json
"coradb": {
"command": "npx",
"args": ["-y", "@coradb/coradb-mcp-server"]
}
```
Then call a tool, for example `execute_select` with
`{ "database": "e-commerce", "sql": "SELECT COUNT(*) FROM orders" }`, or ask
the AI in plain words: *"How many orders are in e-commerce?"*
After the server is updated, restart the client **and start a new
conversation**: a conversation keeps the tool definitions it started with.
Contents: [Quick start](#quick-start) · [How a call works](#how-a-call-works) ·
[Tools](#tools) · [Transactions and write mode](#transactions-and-write-mode) ·
[Results](#results) · [Using it from an AI app](#using-it-from-an-ai-app) ·
[Calling it directly](#calling-it-directly) · [Settings](#settings) ·
[Installing and updating](#installing-and-updating) · [Security](#security) ·
[Development](#development)
## How a call works
Every database tool takes these login inputs:
| Input | Required | Default |
|---|---|---|
| `database` | **yes** | — |
| `host` | no | `CORADB_HOST` setting, else `localhost` |
| `port` | no | `CORADB_PORT` setting, else `33000` |
| `user` | no | `dba` |
| `password` | no | empty |
Only host and port can be changed in the settings. Users and passwords are
never stored by the server: a call that needs another user sends it.
For each call the server:
1. connects to the broker at `host:port` and logs in to `database` as `user`;
2. runs the statements one after the other, in one transaction (auto-commit is off);
3. commits, if the call may commit and every statement succeeded — otherwise rolls back;
4. closes the connection and drops the login.
Several databases, servers or users are simply different calls. If the
connection breaks, times out or the server stops part-way, CoraDB rolls back
whatever was not committed.
## Tools
| Tool | What it does | Changes data |
|---|---|---|
| [`connection_info`](#connection_info) | Logs in and reports the login, engine version and server settings | no |
| [`describe_schema`](#describe_schema) | Tables, views, vertex and edge tables with columns, keys, indexes | no |
| [`execute_select`](#execute_select) | Runs SELECTs and returns columns and rows | never |
| [`execute_sql`](#execute_sql) | Runs any statements; commits when all succeed, unless `dryRun` | yes |
| [`explain_query`](#explain_query) | The optimizer's plan for a SELECT, without running it | no |
Inputs are strict: an input a tool does not know is refused (`Unrecognized
key`) instead of being ignored, so a client with an outdated copy of a tool's
definition finds out instead of being misunderstood.
All tools that take SQL accept **several statements separated by `;`**. They
run in order and the first error stops the call (later statements are reported
as `skipped`). A `;` inside a string, a quoted name or a comment does not split.
If where a statement ends depends on server settings (for example a backslash
before a quote), the call is refused: send the statements one per call.
### connection_info
Logs in and reports the login used (`user@host:port/database`, never the
password), the engine version, the round-trip time, the driver in use
(version, folder, when it was loaded, how many times it was reloaded), the
server's defaults, write mode and limits. Use it to check that a database is reachable.
```json
{ "database": "e-commerce" }
```
### describe_schema
The objects of the database as the given user sees them, read from the live
catalog: tables, views, vertex (node) tables and edge tables, with columns,
types, nullability, defaults, primary and foreign keys and indexes.
| Input | |
|---|---|
| login inputs | see above |
| `object` | one table, view, vertex or edge table (case-insensitive) |
| `format` | `text` (default, CREATE-like) or `json` (the CoraDB schema document) |
| `refresh` | `true` to read the catalog again instead of the copy kept for `CORADB_SCHEMA_TTL_MS` |
```json
{ "database": "e-commerce", "object": "product" }
```
```text
VERTEX TABLE DBA.product
id INTEGER NOT NULL
name VARCHAR(64) NOT NULL
description VARCHAR(32) NOT NULL
unit_price NUMERIC(10,2) NOT NULL
units_in_stock SHORT NOT NULL
PRIMARY KEY (id)
```
A kept copy is only handed out after the call's login succeeds.
### execute_select
Runs one or more SELECTs and returns the columns and rows as the engine sends
them. Each statement is first compiled by the server and refused, before
anything runs, unless it is a SELECT. Nothing is ever committed.
| Input | |
|---|---|
| `sql` | one or more SELECT statements separated by `;` |
| login inputs | see above |
| `maxRows` | rows returned per SELECT (default `CORADB_MAX_ROWS`, 200); `totalRows` still gives the full count |
| `timeoutMs` | time limit for the whole call (default `CORADB_CALL_TIMEOUT_MS`) |
| `params` | values for `?` placeholders (single statement only) |
```json
{
"database": "e-commerce",
"sql": "SELECT c.name, COUNT(p) AS products FROM MATCH (p:product)-[:part_of]->(c:category) GROUP BY c.name ORDER BY products DESC LIMIT 3"
}
```
```json
{
"login": "dba@localhost:33000/e-commerce",
"transaction": "rolled back",
"rolledBackBecause": "execute_select only reads",
"elapsedMs": 16,
"statements": [
{"statement":1,"sql":"SELECT c.name, COUNT(p) AS products FROM MATCH …","type":"SELECT","columns":[{"name":"c.name","type":"String","nullable":false,"precision":32,"scale":0},{"name":"products","type":"Int","nullable":false,"precision":10,"scale":0}],"totalRows":3,"truncated":false,"elapsedMs":15, "rows": [
["Hoodies & Sweatshirts",390],
["Tees",385],
["Jackets",362]
]}
]
}
```
### execute_sql
Runs any statements — INSERT, UPDATE, DELETE, MERGE, CREATE, ALTER, DROP,
SELECT … — in one transaction. Not offered when `CORADB_WRITE_MODE=off`.
| Input | |
|---|---|
| `sql` | one or more statements separated by `;` |
| login inputs | see above |
| `dryRun` | `true`: run everything, then roll it back. Default `false`: commit when every statement succeeds |
| `maxRows`, `timeoutMs`, `params` | as for `execute_select` |
```json
{
"database": "e-commerce",
"sql": "UPDATE product SET units_in_stock = 40 WHERE id = 15"
}
```
```json
{
"login": "dba@localhost:33000/e-commerce",
"transaction": "committed",
"elapsedMs": 21,
"statements": [
{"statement":1,"sql":"UPDATE product SET units_in_stock = 40 WHERE id = 15","type":"UPDATE","affectedRows":1,"elapsedMs":12}
]
}
```
The same call with `"dryRun": true` returns `"transaction": "rolled back"`,
`"rolledBackBecause": "dryRun was requested"` and still reports
`"affectedRows": 1`: what would have changed. `COMMIT` and `ROLLBACK`
statements are refused — the call controls the transaction. Serial increments
(AUTO_INCREMENT, `NEXT_VALUE`) are not undone by a rollback; everything else,
DDL included, is.
### explain_query
The plan CoraDB's optimizer chooses for each SELECT of the call, without
running it (statements other than SELECT are not explained).
```json
{ "database": "e-commerce", "sql": "SELECT c.name FROM MATCH (p:product)-[:part_of]->(c:category)" }
```
## Transactions and write mode
What happens at the end of an `execute_sql` call:
| `CORADB_WRITE_MODE` | Call without `dryRun` (or `false`) | Call with `dryRun: true` |
|---|---|---|
| `write` (default) | committed if every statement succeeded, otherwise rolled back | rolled back |
| `dryrun` | rolled back; the reply says the server allows only dry runs | rolled back |
| `off` | no `execute_sql` tool | — |
A call can only be stricter than the server, never less strict. If any
statement fails, the time limit runs out or the call is cancelled, everything
is rolled back. The reply's `transaction` says `committed` or `rolled back`,
and `rolledBackBecause` says why.
## Results
`execute_select` and `execute_sql` reply with one JSON document: the login,
`transaction`, `rolledBackBecause`, `stopped` (`error`, `timeout` or
`cancelled`, when the call stopped early), `elapsedMs`, and per statement:
`type` (as the server compiled it), `sql`, `columns` (name, type, precision,
scale, nullable, table), `rows` (one array per row), `totalRows`, `truncated`,
`affectedRows`, `error` (`code` and `message` from the engine) or `skipped`.
Values: numbers, strings, `null`, booleans. DATE as `YYYY-MM-DD`, DATETIME as
`YYYY-MM-DD HH:MM:SS.mmm`, TIMESTAMP as `YYYY-MM-DD HH:MM:SS` (the stored
wall-clock time, no time-zone shift). NUMERIC values that do not fit a
JavaScript number exactly come back as text. Binary values and graph objects
the driver does not decode come back as hex. BLOB/CLOB show their type and
length only. Strings longer than `CORADB_MAX_CELL_CHARS` are cut; rows beyond
`CORADB_MAX_RESPONSE_CHARS` are left out and counted in `rowsOmittedForSize`.
A call that stops early, and any other failure, is returned as a tool error
(`isError: true`): engine errors as `CoraDB error <code>: <message>`, an
unreachable broker as `Could not work with CoraDB at host:port …`.
**Time limits and cancelling.** Every call has a time limit (`timeoutMs`, or
`CORADB_CALL_TIMEOUT_MS`, at most `CORADB_MAX_TIMEOUT_MS`), also given to
CoraDB as the statement's query timeout. When it runs out, or the client
cancels the call (MCP `notifications/cancelled`), the running statement is
cancelled on the server, the rest are skipped and everything is rolled back.
## Using it from an AI app
**Claude desktop app (and Cowork).** Settings → Developer → Edit Config, add
to `mcpServers`, then quit the app completely (also from the system tray) and
start it again. Settings go in `env`; leave out the ones you do not change:
```json
"coradb": {
"command": "npx",
"args": ["-y", "@coradb/coradb-mcp-server"],
"env": { "CORADB_HOST": "localhost", "CORADB_PORT": "33000", "CORADB_WRITE_MODE": "write" }
}
```
If the app cannot start `npx` on Windows, use
`"command": "cmd", "args": ["/c", "npx", "-y", "@coradb/coradb-mcp-server"]`.
**Claude Code:**
```bash
claude mcp add coradb --scope user -- npx -y @coradb/coradb-mcp-server
```
**Any other MCP client:** run `npx -y @coradb/coradb-mcp-server` (or
`coradb-mcp-server` after a global install) as a stdio server; settings go in
its environment.
Ask in plain words; the AI turns the request into calls. For example:
| You say | The AI calls |
|---|---|
| "How many orders were shipped in 2025?" | `execute_select` on `e-commerce` |
| "Set the stock of product 15 to 40." | `execute_sql` (committed) |
| "Check whether this update would work: …" | `execute_sql` with `dryRun: true` |
| "Is this query valid?" | `explain_query` (SELECT) or `execute_sql` with `dryRun: true` |
| "Run it on demodb as reader, password …" | the same tool with `database`, `user`, `password` |
## Calling it directly
The server speaks MCP (JSON-RPC 2.0) over stdin/stdout, one JSON message per
line. A program starts it, initializes, and calls tools:
```text
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"coradb",…},…}}
→ {"jsonrpc":"2.0","method":"notifications/initialized"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"execute_select","arguments":{"database":"e-commerce","sql":"SELECT COUNT(*) FROM orders"}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\n\"login\": …"}]}}
```
With the MCP TypeScript SDK (Node.js):
```js
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const client = new Client({ name: 'my-app', version: '1.0.0' });
await client.connect(new StdioClientTransport({
command: 'npx',
args: ['-y', '@coradb/coradb-mcp-server'],
}));
const r = await client.callTool({
name: 'execute_sql',
arguments: {
database: 'e-commerce',
user: 'dba',
password: '',
sql: 'UPDATE product SET units_in_stock = 40 WHERE id = 15',
dryRun: true,
},
});
const result = JSON.parse(r.content[0].text); // { login, transaction, statements: [...] }
console.log(result.transaction, result.statements[0].affectedRows);
await client.close();
```
`scripts/smoke.mjs` and `test/db/live.test.mjs` in the source repository are complete examples.
## Settings
Environment variables, from the `env` block of the client's configuration.
When the server runs from a source checkout, they can also come from `.env` in
its folder (`.env.example`, included in the package, lists them all); the
environment wins over `.env`, and only `CORADB_*` variables are read from it.
Restart the client after a change. `connection_info` shows which came from
`.env` (names only).
| Variable | Default | Meaning |
|---|---|---|
| `CORADB_HOST` | `localhost` | Broker host for calls that give none |
| `CORADB_PORT` | `33000` | Broker port for calls that give none |
| `CORADB_DATABASES` | `*` | Databases a call may name, comma-separated; `*` for any |
| `CORADB_WRITE_MODE` | `write` | `write`, `dryrun` or `off` (see above) |
| `CORADB_MAX_ROWS` | `200` | Rows per SELECT when the call does not say |
| `CORADB_MAX_ROWS_LIMIT` | `5000` | Most rows a call may ask for |
| `CORADB_MAX_STATEMENTS` | `50` | Most statements in one call |
| `CORADB_CALL_TIMEOUT_MS` | `120000` | A call's time limit when it gives none; `0` for none |
| `CORADB_MAX_TIMEOUT_MS` | `600000` | Longest time limit a call may ask for |
| `CORADB_QUERY_TIMEOUT_MS` | `60000` | Server-side limit per statement when the call has no limit |
| `CORADB_CONNECT_TIMEOUT_MS` | `5000` | How long to wait for the broker |
| `CORADB_MAX_CELL_CHARS` | `20000` | Longer strings in a cell are cut |
| `CORADB_MAX_RESPONSE_CHARS` | `200000` | Rows beyond this size are left out of a reply |
| `CORADB_SCHEMA_TTL_MS` | `600000` | How long a schema read from the catalog is kept |
## Installing and updating
- **With npx (recommended):** nothing to install. npx downloads the package
the first time and may keep using that copy; to always start the newest
version use `@coradb/coradb-mcp-server@latest` in `args`, and to pin one,
for example `@coradb/coradb-mcp-server@1.0.0`.
- **Global install:** `npm install -g @coradb/coradb-mcp-server`, then use
`"command": "coradb-mcp-server"` with no `args`. Update with
`npm update -g @coradb/coradb-mcp-server`.
- **The CoraDB driver** is the npm package
[`@coradb/node-coradb`](https://www.npmjs.com/package/@coradb/node-coradb),
installed with the server. When its files change (an update), the server
loads the new version before the next call, with no restart. If the new
code does not load, the previous version keeps working and
`connection_info` shows the error under `driver.reloadError`;
`connection_info` also shows the driver's version, folder, load time and
number of reloads.
## Security
- **No stored logins.** The server keeps no users or passwords, in files or in
memory between calls; each call brings its own and it is dropped when the
call ends. Replies and `connection_info` never show passwords.
- **The client holds the credentials.** Whoever calls the server sends the
password. When the caller is an AI app, the AI sees the password it sends.
- **CoraDB's rights apply.** A call can do exactly what its user may do in that
database. Use a user with only SELECT rights for reading.
- **Guards.** `execute_select` and `explain_query` let the server compile each
statement and refuse anything that is not a SELECT before it runs.
`COMMIT`/`ROLLBACK` statements are refused. `CORADB_WRITE_MODE=dryrun` or
`off` prevents saving whatever a call asks.
- **Local only.** The server talks to its client over stdio on the same
computer; it opens no network port of its own.
## Development
```bash
git clone https://github.com/apsisware/coradb-mcp-server.git && cd coradb-mcp-server
npm install # installs and builds dist/
npm test # unit tests, no database needed
npm run test:db # live tests against e-commerce on localhost:33000 as dba
```
To run a local build from a client, use `"command": "node"` with
`"args": ["<path to the checkout>/dist/index.js"]`.
| Task | What it does |
|---|---|
| `npm run build` | Compiles `src/` to `dist/` |
| `npm test` | Unit tests (`test/*.test.mjs`), no database; includes time-limit and cancel checks for every tool against a fake broker that never answers |
| `npm run test:db` | Live tests (`test/db/`) through the MCP protocol against a running CoraDB with the e-commerce dataset. Nothing is saved. Another login: `CORADB_TEST_DATABASE`, `_HOST`, `_PORT`, `_USER`, `_PASSWORD` |
| `npm run smoke` | Calls every tool once and prints the replies (`SMOKE_DATABASE` etc. to change the login; a query as argument) |
| `npm publish` | Builds and runs the unit tests first (`prepublishOnly`), then publishes `dist/`, `README.md`, `LICENSE` and `.env.example` |
### Files
| File | |
|---|---|
| `src/index.ts` | Starts the server (stdio) |
| `src/tools.ts` | The tools and their inputs |
| `src/run.ts` | Runs a call's statements: type checks, time limits, cancelling, commit or rollback, results |
| `src/db.ts` | Login resolution and the one-connection-per-call wrapper |
| `src/driver.ts` | Loads `@coradb/node-coradb`, and loads it again when its files change |
| `src/config.ts` | Settings and defaults |
| `src/dotenv.ts` | `CORADB_*` settings from `.env` |
| `src/schema.ts`, `src/coradbSchema.ts` | Reading and printing the schema |
| `src/sqlText.ts` | Splitting a script into statements |
| `src/format.ts` | Turning values into JSON |
| `test/*.test.mjs` | Unit tests (no database; `limits.test.mjs` uses a fake broker that never answers) |
| `test/db/live.test.mjs` | Live tests against e-commerce |
| `scripts/smoke.mjs` | `npm run smoke` |
TDQS
Scored across 5 tools
Each tool targets a distinct activity: connection verification, schema introspection, read-only querying, mutating/DDL execution, and plan inspection. The only potential overlap is that execute_sql can also run SELECTs, but its description explicitly frames it as the transactional read-write path while execute_select is the guarded read-only path. Boundaries are clear enough that misselection is unlikely.
Four tools follow a clean verb_noun snake_case pattern (describe_schema, execute_select, execute_sql, explain_query). connection_info breaks the pattern as a noun phrase, a minor deviation but still unambiguous and readable.
Five tools is a tight, well-scoped surface for a database MCP: connect, inspect, read, write, explain. Nothing feels redundant or missing at the count level.
The lifecycle is well covered: reachability check, live catalog introspection, read queries, transactional writes/DDL with dry-run, and plan explanation. Minor gaps exist around multi-database enumeration/switching and any explicit result-pagination or cancellation path, but agents can work around these.