Datanotes MCP server
io.github.davidefornelli/datanotes·v3.0.0The engine of your second brain: Markdown notes as database tables, for you and your agents.
0 stars1.8k downloads/wk
Reviews
Write oneNobody has reviewed Datanotes yet.
If you have run it, two minutes of your experience saves the next person an afternoon.
Datanotes tools (56, 14 write)
write = sends, deletes, buys or postsRead from the package source without running it. The installed server may list more.
add_columnAdd a typed column. Sparse by default (written only on rows that have a value \u2014 use for metrics, e.g. `{"name": "metric_mood", "type": "integer", "min": 1, "max": 5, "label": "Mood", "aliases": ["mood"], "description": "Mood of the day, 1 = bad, 5 = great"}`). `description` is required: what the column holds, for the agents and people who read the schema (the engine writes it for `identity` "
add_constraintALTER TABLE ADD CONSTRAINT `name` CHECK: `check` is a where filter every row must match, e.g. `{"value": {"gte": 1, "lte": 5}}`. A condition on a column the row leaves empty passes, like SQL; inside `or` / `and` / `not` an empty value is tested as it is (`{"exists": true}` there fails). A key may follow a link column to the row it names, as in queries: `{"parent.kind": "project"}` (further dots fo
adopt_rowsTurn existing notes (not rows of any table yet) into rows of `table`, keeping their body and links: `rename` keys (`{"data": "occurred_at"}`), `set` for all, `per_row` values by note path (`{"Home/x.md": {"zone": "[[Kitchen]]"}}`), `drop` keys the table does not have. Every note gets `table` and a system `id`; with `relocate` (default true, unlike move_rows: notes become rows where the table keeps
align_relationsAdd the reciprocal links missing on a table's `inverse` link columns (e.g. Alice lists Carol in `friends` but Carol does not list Alice). Run it after declaring `inverse` on existing data; check_table reports such rows (code `inverse`). Single-value links already pointing elsewhere are conflicts (nothing written). The links it adds are checked like other writes (CHECKs that follow links, `ref_wher
alter_columnChange a column's attributes (type, min/max/step, values, required, default, label, aliases, description, note, `ref_where`, `ref_by` (see list_link_targets); `null` removes an attribute, except `description`, which changes but never goes (null or "" is refused); `dense` true/false). Refused with the list of violating rows when existing values do not satisfy the new definition. A different scale m
alter_tableAlter a table: move it (`catalog`, `schema`), rename it (`name`), change `partition` / `filename` patterns, `description` (changes, never goes: "" is refused), `aliases`, `exclude` folders, `row_template` ("" removes it), `history` (true | false | "default"), `autolink_target` (true | false | "default": may automatic links link its rows? default: yes, unless its rows are named by a date or a time)
batchSeveral writes that succeed or fail together. `operations`: `[{"op": ..., ...arguments}]` where `op` is a row operation (`insert_row`, `update_row`, `update_rows`, `delete_rows`, `merge_rows`, `move_rows`, `adopt_rows`, `recompute_columns`, `align_relations`, `restore_table`) or a DDL op (`create_table`, `add_column`, `alter_column`, \u2026), with the same arguments as the operation (an unknown on
check_tableValidate every row against the table (required, types, ranges, enums, links, unknown columns, CHECK constraints, those that follow links included, filtered link targets (ref_where / ref_by), stale generated/identity values, missing inverse links, front matter that cannot be read: code `unreadable`, with no column, for a note whose `table` line names the table but whose YAML is broken, which every
clone_tablewrite actionCREATE TABLE `target` CLONE `source`: same definition plus independent copies of its rows (deep clone), optionally as of a `version` / `timestamp`. `copy_rows: false` copies the definition only (CREATE TABLE LIKE). `target`: `catalog.schema.table`, `schema.table` (in the source's catalog) or a name (source's catalog and schema). Useful for experiments, e.g. a sandbox copy in the `agent` catalog. T
comment_onCOMMENT ON CATALOG / SCHEMA: set the `description` of a catalog (`catalog` alone) or of one of its schemas (`catalog` and `schema`) in metastore.yaml: what it holds, so agents pick the right place. `description` "" removes it. An unknown catalog or schema is refused with the existing ones. describe_database and `system.information_schema.catalogs` / `schemata` show it. Undo with `undo_migration`.
configure_databaseConfigure the database (persisted in metastore.yaml `config:`): `root` (rename the database folder; links follow; revert by renaming back), `default_catalog`, `default_schema`, `default_partition` ("" = flat) and `default_filename` (date formats for new time-based tables), `archive_folder`, `timezone` (IANA; completes date-times written without offset), `history` (transaction log for every table:
create_viewwrite actionCreate (or `replace`) a view: a named select stored in metastore.yaml. Name it with the prefix `vw_` (e.g. `vw_open_tasks`): views share names and folders with tables. `query` takes the select_rows parameters (table, columns, where, order_by, limit, join, group_by, aggregates, expand; relative dates keep it current) and must run now. Or `query: {union: [{table, columns, where}, \u2026], columns, w
create_webhookwrite actionPush row changes to a URL (a service that would rather be called than poll `list_changes`): batches of up to 100 events POSTed as `{webhook, events, last}`, signed with `X-Datanotes-Signature: sha256=<HMAC of the body>`, retried with backoff until a 2xx. Events are those of `list_changes` (hand edits with their `violations`; the hand edits the service has seen are recorded before it stops). Starts
define_basewrite actionCreate or replace a base: a named set of columns (same spec as `create_table`) that tables inherit, like a parent class (`bases` in `metastore.yaml`). Tables using it get the columns with the same name and meaning; `ref: "self"` means the inheriting table (e.g. a `parent` column with `hierarchy: true`). `bases` builds on other bases. Descriptions are required, as in create_table: `description` say
delete_rowswrite actionDelete rows matching `where`. `mode`: "archive" (default, move to the table's archive folder) or "delete" (trash; content journaled for undo). `on_referenced`: "restrict" (default, refuse rows other notes link to), "set_null" (clear front matter links to them) or "ignore" (the links stay, to no row: an archived row is gone as a trashed one, so check_table reports them as `broken_link` and new link
delete_webhookwrite actionStop and remove a webhook (by `id` from list_webhooks).
describe_databaseDescribe the database: its folder (`root`), `metastore` (`metastore.yaml`: read it freely, but it is read-only for agents and changes only through the schema operations), configuration (default catalog and schema, row partition and file-name formats, archive folder, time zone, history, automatic links), journal folder, `bases` (shared column sets and the tables using them), `catalogs` \u2192 `sche
describe_historyDESCRIBE HISTORY: the table's versions, newest first \u2014 `version`, `timestamp`, `operation` (CREATE TABLE, WRITE, UPDATE, DELETE, MERGE, ALTER TABLE, ADD COLUMNS, RESTORE, UNDO, \u2026; `EXTERNAL_EDIT` = changed outside the engine: an editor, git, sync), `operation_metrics` and `migration_id` (undo). An EXTERNAL_EDIT's `operation_metrics.violations` counts the rules this table's rows break bec
describe_semantic_indexThe semantic search index: `enabled`, `provider`, `model`, `files`, `passages`, `pending`, `running`, `last_sync`, `last_error`. sync_semantic_index brings it up to date.
describe_tableDescribe a table (`catalog.schema.table` or a shorter unambiguous name): `qualified`, `name`, `catalog`, `schema`, `description`, `aliases`, `layout` (`folder`, `row_folder`: the folder pattern of new rows, `archive`), `options` (`partition`, `filename`, `archive`, `exclude`), typed `columns` with label/aliases/description and the dense flag, `untyped_fields`, `row_count`, and when set: `tags`, `b
drop_columnwrite actionRemove a column from the schema and every row. Previous values are journaled; revert with `undo_migration`. Refused while a CHECK, `ref_where` or `ref_by` of any table reads the column (directly or through a link): the conflicts name them; drop or change them first. The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).
drop_constraintwrite actionRemove the CHECK constraint `name` from `table`. The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).
drop_tablewrite actionDrop a table. Refused while it has rows unless `mode: "archive"`, which removes the definition from `metastore.yaml` and moves all rows to the table's archive folder (nothing is hard-deleted; undo restores). The transaction log is kept: `restore_table` can bring it back. Refused while a CHECK that follows links, or a `ref_where` / `ref_by`, of another table reads this table through a link (link ta
drop_viewwrite actionRemove a view from `metastore.yaml` (undo with `undo_migration`). The schema changes only through operations like this one: never edit metastore.yaml by hand (no validation, no undo).
graph_neighborsDirect neighbours of notes: what they link to (`out`) and what links to them (`in`, backlinks), with counts per edge label. `direction`: out, in or both (default). `from_nodes` selects the notes: `{"paths": [...]}` and/or `{"table": ..., "where": {...}}`. Edges are labelled with their link column (`people`, `event`, \u2026) or `link` for other links.
graph_pathShortest chain of links between two sets of notes (each `{"paths": [...]}` and/or `{"table", "where"}`): `found`, `length` and `path`: the steps (`from`, `label`, `direction`, `to`). `direction` both by default; `max_depth` default 6 (max 10).
graph_traverseBreadth-first walk over links from `from_nodes` (`{"paths": [...]}` and/or `{"table", "where"}`). - `direction`: out (default, follow links), in (follow backlinks) or both. - `edges`: edge labels to follow (link columns such as `people`, `event`, or `link`). - `tables`: return only notes of these tables; `through`: only walk on through these tables. - `min_depth` (default 1) / `max_depth` (default
infer_tableRead-only schema inference from notes' front matter. - `folder` (recursive; "" = whole vault) and/or `paths`: notes not yet in a table. Returns `columns` (type, enum `values`, link `ref` to the table most targets belong to, `required`, `unique`, `dense`, `from` when the front matter key is not a valid column name, `stats`), `proposal`, the create_table arguments (`name`, `description`, `filename`
insert_rowwrite actionInsert a row (new note). Values are validated against the columns; the note path comes from the table's `path`/`filename` patterns (e.g. an event logged today about last night lands in last night's day folder when you set `occurred_at` to last night). `body` is the note text. Generated / identity columns are computed (do not pass them); CHECK constraints are enforced. Returns `migration_id` (undo)
list_changesChange feed across all tables: numbered row changes (`seq`), oldest first, from engine writes and from hand edits (`source: external`). Each has `table`, `kind` (insert, update, delete, move, metastore, unreadable), `op` (the operation that made it), `path`, `row_id`, `changes` ({column: {before, after}}), `actor`, `migration_id`. Hand edits are compared in batches (0.7 s after the last vault chan
list_docsList the pages of the Datanotes documentation of this version (read offline, from the package): path, title and description, one per line. `prefix` keeps the pages under a folder (`guides/`, `reference/`, `start/`). Read one with read_docs(path).
list_folderThe direct children of a vault folder: `{folder, folders, files}`, each a vault path, sorted; dot-files and dot-folders are left out. `folder` "" is the vault root. A missing folder is an error.
list_link_targetsThe rows a link column may point to, for one row: what an agent or a picker offers before writing the link. A link column can filter its targets: - `ref_where`: a `where` filter every target must match (`{"kind": {"in": ["area", "project"]}}`). - `ref_by`: a column of the row picks the targets' level: `{"column": "kind", "map": {"area": null, "project": ["area"], "workstream": ["project"]}}`: a wo
list_migrationsThe operation journal, newest first: `total` (the matching operations) and `migrations`, each with `migration_id` (for undo_migration), `op`, `summary`, `table` (when it was about one), `executed_at`, `notes_changed`, `files_changed`, `undone`, `undoes`, `files_failed`, `by` (the agent), and `members` (a batch's operations) or `batch` (the batch of an operation). Filters: `op` (e.g. "update_rows")
list_tablesList every table: `name`, `catalog`, `schema`, `qualified` (`personal.metrics.mood` \u2014 use it wherever a table is expected; `metrics.mood` or `mood` also work when unambiguous), `path` and, when declared, `description`, `aliases` (other words for what it records) and `tags`. Tables are defined in `metastore.yaml` (catalog \u2192 schema \u2192 table): read it freely, but it is read-only for age
list_webhooksWebhooks of the change feed: id, url, filter (tables, ops, columns, exclude_actor), `cursor` (last change delivered), `status` (ok, failing, disabled), `last_delivery`, `last_error`. Secrets are not shown.
merge_rowsMERGE (upsert) `rows` into `table` by the key columns `on` (e.g. `["occurred_at"]` or `["id"]`): a row matching an existing one updates it (`when_matched`: "update" default or "ignore"), the others are inserted (`when_not_matched`: "insert" default or "ignore"). Date-times are compared after completion with the database time zone. A source key matching several rows, or repeated, is a conflict. One
move_rowsMove rows matching `where` (required: `{}` is every row) to another table (e.g. split a domain out of `log` into its own table): sets each row's `table` to the destination, applies `rename` (`{"metric_distance_km": "distance_km"}`), `set` and `drop`, adds the destination's missing dense fields, and validates every row against the destination first. Keys unknown to the destination are reported, nev
read_docsRead a page of the Datanotes documentation of this version as Markdown, or one section of it: `guides/read-and-write/read-and-filter`, or `guides/read-and-write/read-and-filter#compare-values-with-operators` (the anchor of its heading, as in the docs links of the errors; the datanotes.dev addresses work too). Read a section rather than a whole page: list_docs lists the pages, search_docs finds the
read_noteRead a note or another file of the vault: `{path, content, front_matter, tags, modified_at, created_at, size}`. `content` is the whole text, front matter included; `tags` are without `#`, from the text and the front matter; the times are in the database's time zone. A missing file is an error. `path`: vault-relative, forward slashes.
recompute_columnsBring generated and identity columns up to date (all rows, or only `paths`), e.g. after rows were edited by hand (an editor, git, sync) or a generated expression changed with `alter_column`. Also gives rows without an `id` a new one, and a duplicated note (same `id` as an older row) a new id. `new_ids=true` regenerates every id in scope (new ones on every call): only on the user's explicit request
rename_columnRename a column in metastore.yaml and on every row. Refused if `to_column` is already a column of the table; a row that has a stray `to_column` key with a different value is a conflict, and nothing is written. CHECKs, `ref_where` and `ref_by` that read the column (in this table, or through a link from another table: `parent.kind`) follow the new name. Views and note text that mention the old name
rename_valueRename one value of an enum column (e.g. `high` \u2192 `very high`) in metastore.yaml and every row, keeping its position, so an ordered scale is unchanged. CHECKs and `ref_where` conditions on the column (also through a link from another table) and `ref_by` levels / maps that name the value use the new one. Note text (queries, prose) is not rewritten. Refused if `to_value` already exists. Undo wi
restore_databaseRESTORE DATABASE to a `timestamp` (each table as of its last version committed at or before it) or to `before_migration` (a `migration_id` or a `batch`: every table as it was just before that operation ran \u2014 to rewind an agent's run). Every table that keeps a transaction log goes back, rows and definition: rows added since are deleted, changed rows get their old content, removed rows come bac
restore_tableRESTORE TABLE to a `version` or `timestamp`: rows and definition return to that state \u2014 rows added since are deleted, changed rows get their old content, removed rows come back (a dropped table is re-created). History is not rewritten: the restore is a new version, and `undo_migration` reverts it. Refused (rows listed, with the way out) when the rows it writes, or rows that link to them, woul
search_docsSearch the Datanotes documentation of this version for the sections on a topic, best first: each with the path to pass to read_docs (`guides/read-and-write/read-and-filter#compare-values-with-operators`), its page and heading, and a short excerpt. Words in a heading count most; a word matches in any form (`rows`, `row`) and its synonyms too (`revert` for `undo`). Syntax: a quoted phrase ("time tra
search_notesPlain-text search: every word of `query` must occur, case- and accent-insensitive, in the note's path or text; there are no operators. Returns `{query, results: [{path, score, matches: [{start, end, context}]}], total}`, best first: at most 200 notes (`total` counts them all) and 20 matches per note. `context_length`: characters of context on each side (default 100, max 1000).
select_rowsQuery rows: `{table, total, rows: [{path, ...}]}`. `table` may be a view, `system.information_schema.<catalogs|schemata|tables|columns|views|check_constraints>`, or `base:<name>` \u2014 every table that extends that base (e.g. `base:record`: all event-like tables), on the base's columns, each row tagged `_source` with its table. - `where`: as in update_rows; relative dates `"{today}"`, `"{today-7d
semantic_searchFind notes by meaning, not exact words: `{query, results: [{path, score, heading, snippet, table?}]}`, one result per note (its best passage), best first. Use it for vague questions ("the dinner where we talked about moving", "what did I decide about the car") and to find the rows to read or update; then read_note / select_rows for the details. Filters: `tables` (qualified or short names), `folder
set_table_basesSet the bases a table inherits from (`bases`, in order; `[]` removes them). Inherited columns come after `id` and before the table's own columns. An own column with the name of an inherited one is replaced by the inherited definition (rows must fit it, its `ref_where` / `ref_by` included); a table may only restyle an inherited column (label, description, aliases, note, dense) with `alter_column`.
sync_semantic_indexBring the semantic search index up to date: embed the notes changed since the last run (the engine also does it on its own about ten seconds after edits). `rebuild: true` re-embeds everything: slow, and costs money with a paid provider \u2014 ask first.
table_changesChange data feed between two versions (both included, end defaults to latest): one entry per row change with its values plus `_change_type` (insert, delete, update_preimage, update_postimage, metadata), `_commit_version`, `_commit_timestamp`, `_operation`, `path`. Use it to answer "what changed since \u2026" without diffing full tables.
undo_migrationRevert a journaled table operation by its `migration_id`. Refused (nothing written) when any touched note changed since; the conflicts are listed. Also refused when the revert would leave rows breaking a CHECK that follows links or a `ref_where` / `ref_by` (rows it writes, or rows that link to them), listed like the refusals of other writes with the way out (fix the row, or drop the rule). The rul
update_rowwrite actionUpdate exactly ONE row, addressed by its `id` (the system UUID) or its `path`; refused when the row does not exist (or id and path disagree). Same validation, journal and history as `update_rows`. - `set` / `unset`: columns to set or remove. - `body`: replace the whole note body; or edit it keeping the rest: `body_sections` `{"History": "\u2026"}` replaces the text under that heading (any level, u
update_rowswrite actionUpdate every row matching `where` (required: `{}` is every row). `where`: keys AND-ed; values are equality or operators eq, ne, gt, gte, lt, lte, in, contains, icontains (case/accent-insensitive, also inside lists \u2014 use it for names and aliases), exists, within (a hierarchy link: that row and everything below); combine with `or` / `and` / `not` (nestable); `path` is a pseudo-column. A dotted
vacuum_tablewrite actionDelete transaction-log history older than `retain_from_version` (default: keep only the latest version); time travel before it becomes impossible. Rows are not touched. `dry_run` defaults to true: it lists the log files that would go. Only on the user's explicit request.
Public scan report
scanner v0.1.10 · 2026-10-12 · same rubric, same numbers if you re-run it
- Code scan20 source files scanned25/25
- –Live reliabilityno gateway calls yet and no remote to proben/a
- –Tool poisoningtools not inspected (local package is not executed); not countedn/a
- Auth qualitylocal package, no credentials required12/15
- Maintenancelast push 0 days ago15/15
- Maintainer identityregistry namespace matches repository owner; GitHub account older than a year8/10
How Datanotes compares
Against every graded Databases MCP server, from our own scans and uptime checks. Updated after each scan run.
- Safety score
- 92/100
- higher than 90% of 803 Databases servers
- Tools
- 56
- the typical Databases server has 8
- Tools that change things
- 25%
- typical: 10%. Fewer write tools means less an agent can break
- Downloads a week
- 1,844
- more than 95% of 315 Databases servers with a package
What its tools let an agent do: 33 read · 6 create · 10 change · 6 delete · 1 send or post
Install Datanotes in Claude Code, Cursor or VS Code
Runs npx -y datanotes on your machine. Read the scan report first; the gateway never runs local packages.
claude mcp add datanotes -- npx -y datanotes
What the publisher says
From the Datanotes repository's README, as published. We do not edit it. Read it on GitHub
Datanotes
Datanotes is the open-source database engine for Markdown: it runs a folder of notes as typed, validated tables that your programs and AI agents query, write and undo over MCP or HTTP, while every row stays a plain file you own.
<!-- Hero GIF goes here: an agent writes a row over MCP, the engine refuses a bad value and explains the fix, then the note opens as plain Markdown. -->
Documentation: datanotes.dev
Why Datanotes
- Agents can't write garbage. Every write is checked against its table: types, links, unique values and CHECK constraints. A refused write changes nothing. Its answer says which value broke which rule and how to fix it.
- Every change can be undone. The engine journals every write it makes. It records who made it: the agent, its session and its token. undo_migration takes any change back.
- It's still just Markdown. A row is a note, and its front matter keys are the columns. Read it, edit it, grep it or commit it to Git. The engine reads your hand edits and checks them too.
- Local first. Datanotes is Apache-2.0 and runs on your machine. Nothing leaves it unless you turn that feature on.
When not to use it
Datanotes keeps every row as a file, so it isn't a replacement for SQLite or Postgres. Choose a classic database for:
- Large analytical datasets
- High write throughput
- Many concurrent writers on one folder
Datanotes is comfortable at 10,000 notes. At 50,000 notes, the engine takes 22 to 115 seconds to start.
Quick start
You need Node.js 22 or newer. On Windows, use Node.js 24.16 or newer, or 26.1 or newer.
Start the engine on a folder, which becomes your database. It can be empty, or hold notes already:
npx -y datanotes serve --database your_database_folderRun Datanotes on your notes walks you through your first table.
Connect your agents
Agents reach the engine through its Model Context Protocol (MCP) server, datanotes mcp. It uses the engine that runs on the folder, or starts one. Every client runs the same command:
npx -y datanotes mcp --database your_database_folderClaude Code
claude mcp add datanotes -- \
npx -y datanotes mcp --database your_database_folderClaude Desktop
Add the server to claudedesktopconfig.json:
{
"mcpServers": {
"datanotes": {
"command": "npx",
"args": [
"-y", "datanotes", "mcp", "--database", "your_database_folder"
]
}
}
}Cursor
Add the server to .cursor/mcp.json in your project, or to ~/.cursor/mcp.json:
{
"mcpServers": {
"datanotes": {
"command": "npx",
"args": [
"-y", "datanotes", "mcp", "--database", "your_database_folder"
]
}
}
}VS Code
Add the server to .vscode/mcp.json in your workspace:
{
"servers": {
"datanotes": {
"type": "stdio",
"command": "npx",
"args": [
"-y", "datanotes", "mcp", "--database", "your_database_folder"
]
}
}
}Codex
Add the server to ~/.codex/config.toml:
[mcp_servers.datanotes]
command = "npx"
args = ["-y", "datanotes", "mcp", "--database", "your_database_folder"]Windows
On Windows, clients start npx through cmd /c. For Claude Code, outside Windows Subsystem for Linux (WSL):
Shortened. The full README is on GitHub.
Nothing above is checked by us. What we check is on the safety report.
Datanotes: common questions
- Is Datanotes MCP server safe?
- Yes, by our scan: it is graded A (92/100). Read the Datanotes safety report
- How do I install Datanotes?
- It runs on your machine. Copy the Claude Code, Cursor, VS Code or Claude Desktop config from the install section.
- Does Datanotes need an API key?
- Not as far as the registry entry and our scan can tell: no credentials are declared or required.
- Is Datanotes maintained?
- The last commit was 20 days ago (2026-10-12). The latest release is v3.0.0.
- What can I use instead of Datanotes?
- Servers from other publishers that do the same job: Airtable MCP server, Microsoft Access Database MCP server and Emerald MCP server. Compare all Datanotes alternatives.
- How does Datanotes compare with other Databases MCP servers?
- safety score 92/100 (higher than 90% of 803 Databases servers); tools 56 (the typical Databases server has 8); tools that change things 25% (typical: 10%. Fewer write tools means less an agent can break). Full comparison.
Alternatives to Datanotes
Same job from other publishers: the closest match first, then the best rated.
AirtableRead and write access to Airtable database schemas, tables, and records.not reviewedEstablishedB- Microsoft Access DatabaseRead and edit Microsoft Access databases (.accdb/.mdb) - forms, VBA, tables, queries. Windows.not reviewedGrowingA
- EmeraldThe database under the chat: a local SQLite file a model asks for rows, not notes to re-read.not reviewedGrowingA
- sqlite-graph-memoryRecall over your markdown notes: vector search plus your [[wikilinks]] as a graph, in SQLite.not reviewedGrowingA
- CogniCoreMemory, reflection and safety for AI agents. Hosted or local, no vector database.5.0(1)GrowingA