reldir mcp serves a database to AI agents over the
Model Context Protocol. Every tool runs a
reldir command as the command line runs it, through the same validation, and
every change is planned before it is made. To connect a client, see
How to give an AI agent access to a database.
reldir [GLOBAL FLAGS] mcp
The server speaks MCP on stdin and stdout, protocol version 2025-11-25, and
names itself reldir with the binary’s version. It offers tools, resources
with subscriptions, and prompts, and its instructions tell the agent how to
read and how to change the database.
The database is the one --db names, or the one found from the working
directory, as for every command (see CLI).
Global flags apply to every call of the session:
--readonly: the session writes nothing at all – no rows, no history, no
derived state – and every change tool answers READ_ONLY;--timeout, --max-result-rows and the other
limits bound every query.Every tool answers with the machine protocol’s
result document as structured content, and one line of text saying how the
call ended – the summary, or the code and message of a failure. Each tool
declares that document as its output schema. A command that fails is a tool
result with isError set, which the agent reads like any other; a call the
server cannot run at all – an unknown tool, arguments that do not fit – is a
protocol error.
Each tool’s input schema is closed: an argument it does not name, or one of the
wrong type, is refused before anything runs, as an invalid_params error that
lists every fault. A misspelt dry_run or limt is never silently ignored.
| Tool | Arguments | Does |
|---|---|---|
status |
validity, unrecorded changes, the revision | |
tables |
the tables, row counts and keys | |
describe |
table |
a table’s columns, key, and references in and out |
schema_show |
table |
a table’s schema document |
get_row |
table, key |
one row; a composite key is an array of values |
list_rows |
table, where, order, limit |
a table’s rows, filtered and ordered (-column for descending); 200 by default |
query |
sql, params, limit, cursor, allow_invalid |
one read-only statement, a page at a time |
explain |
sql, params |
SQLite’s plan for a statement |
check |
strict, deep |
every fault, with file, line and JSON Pointer |
lint |
table |
how the schemas could say more |
infer |
tables |
the schemas reldir would infer; writes nothing |
doctor_plan |
only |
every repair doctor could make |
doctor_explain |
fix |
what one repair does |
diff |
table, from and to (together), git, schema_only |
what changed since the last revision, between two, or against a git revision |
log |
limit |
recorded revisions, newest first |
show_revision |
revision |
one revision and what it changed |
snapshot_list |
the named snapshots |
Read tools are annotated read-only and idempotent.
query answers a page of rows: 200 unless limit says otherwise, at most
10,000. When more remain, data.more is true and data.next_cursor continues
from where the page ended:
{"sql": "SELECT id, title FROM posts ORDER BY id", "limit": 2}
{"sql": "SELECT id, title FROM posts ORDER BY id", "limit": 2, "cursor": "c1.2.<state>.<query>"}
A cursor belongs to one query – its statement and values – and to the state
the database was in when the first page was read. Passed with another query it
is refused (USAGE); once the database has changed, the next page could not
continue the same answer, so it is refused with CURSOR_STALE, and the agent
starts again from the first page. A page never mixes two states.
A tool that changes the database takes effect in two calls.
Plan. Called without plan_id, the tool validates the change against
the database as it is and reports everything it would do – every file it
would write or delete, every referential action, the rows it would write –
and writes nothing. Its result’s data holds the plan:
| Member | Holds |
|---|---|
plan_id |
the plan’s identifier, to commit it with |
state |
the state of the database the plan was made against |
removes |
what it removes, one entry each: deletes <path> for a file it deletes, <action> <path> (<constraint>) for a referential action |
overwrites |
how many existing files it rewrites |
confirmation_required |
whether removes is not empty |
The summary names the plan_id and says how to commit it.
Commit. Called again with the same arguments and the plan_id, the tool
makes exactly that change – provided the database is still in the state the
plan was made against. The check is made under the writer lock, so nothing
can slip in between. If anything changed, the commit is refused with
PLAN_CHANGED and nothing is written; the agent plans again.
A plan commits once, only with the arguments it was made with; other arguments,
a plan already committed, or one from before the server started are
PLAN_CHANGED. The server keeps the 64 most recent plans.
A plan that removes data – deletes a row or file, or runs a referential
action – says so in data.removes, and committing it needs the user’s
confirmation. When the client supports elicitation, the server asks the user
directly, naming what the plan removes; declining is DECISION_REQUIRED and
nothing is written. When it does not, the agent must pass confirm: true,
which it should do only with the user’s agreement. Rewrites of existing rows
are reported in data.overwrites; every rewritten row stays in history.
What a plan removes is decided from the plan itself, not from how the request
is spelt: a DELETE hidden in a WITH clause, or a cascade three tables away,
is a removal like any other.
| Tool | Arguments | Does |
|---|---|---|
mutate |
sql, params |
one INSERT, UPDATE or DELETE |
transaction |
statements |
several statements as one change; each sees what the ones before it did |
insert_row |
table, row |
one row, or an array of rows |
update_row |
table, key, patch |
set columns of one row |
delete_row |
table, key |
delete one row, with its referrers’ actions |
import |
table, path, format, mode |
import a file on the server’s machine (insert, upsert or replace) |
apply_fixes |
only, allow_data |
apply doctor’s repairs |
migrate |
operations |
a migration: operations that reshape schemas and rows together |
schema_new |
table |
declare a table |
schema_pin |
table |
make an inferred schema the table’s declaration |
schema_widen |
target, null, optional, types, any |
widen a table’s contract |
snapshot_restore |
name |
make the database what a snapshot holds |
restore_rows |
paths, at |
write row files back as history recorded them: a deleted row, or a row as it was |
Every change tool also takes plan_id and confirm. A transaction’s
statements are the transaction: BEGIN, COMMIT and ROLLBACK in them are
TRANSACTION_STATEMENT. Tools that can remove or rewrite data are annotated
destructive; none is open-world.
Two tools write beside the database’s rows, in one call:
| Tool | Arguments | Does |
|---|---|---|
snapshot_create |
name |
keep the database as it is now under a name |
export |
table, path, format, where, order |
write a table’s rows to a new file – JSON, JSON lines, CSV or Parquet – which must not exist |
| URI | Is |
|---|---|
reldir://dialect |
the JSON Schema dialect every table schema is written in |
reldir://protocol |
the JSON Schema of every tool result |
reldir://docs/<topic> |
a page of this documentation, as Markdown |
reldir://schema/{table} |
a table’s schema document (a template) |
reldir://row/{table}/{key} |
one row, by its key (a template) |
A resource that does not exist – an unknown table, a row no key names – is a
resource not found error.
A client may subscribe to resources: with resources/subscribe, as every
client that opens with initialize (protocol 2025-11-25) does, or with a
subscriptions/listen stream, as later protocol revisions do. When the server commits a change, it notifies each
subscriber of every subscribed resource the change touched: a row resource for
a change to its table, a schema resource for a change to its schema. A change
to a schema also changes the list of resources, which subscribers of the list
hear. Changes made outside the server – by another process, or by hand – are
seen at the server’s next call.
| Prompt | Arguments | Sets the agent to |
|---|---|---|
explore-database |
learn the tables, how they relate, and what is wrong, changing nothing | |
design-schema |
table (required), purpose |
design or change a table through schema_new and migrate, showing each plan |
repair-database |
find every fault with check, repair with doctor_plan and apply_fixes, confirming removals with the user |
A client may cancel a call in flight. A query being run stops at its next progress check, and the call ends without an answer. A change that has reached its commit is not interrupted: it is made, or not made, whole.
Each call opens the database afresh, so it answers from the folder as it is –
including an edit another program made a moment ago – and holds no lock
between calls. With reldir serve running, an
open reads only what changed since the last one. Commands run off the
protocol’s thread, so a slow query does not stop the server from hearing a
cancellation.