How agents work with your vault
An agent connected through datanotes mcp, the Model Context Protocol (MCP) server of Datanotes, works with your vault the way a careful colleague works with a shared database. It reads the descriptions before it writes, writes only through operations the engine checks, and signs every change with its name in the journal. This page follows that path, from the instructions the agent receives to undoing what it did; the prompts show it on concrete requests.
The agent starts with the server’s instructions
Section titled “The agent starts with the server’s instructions”When an MCP client connects, datanotes mcp sends instructions with its list of tools. The engine’s part is short:
Database of Markdown notes: tables defined in metastore.yaml, a row is a note whose front matter key `table` names its table, and each front matter key is a column. Use list_tables / describe_table first, select_rows to read, insert_row / update_row / update_rows / batch to write (validated, journaled, undo with undo_migration). Change the schema only with its operations (create_table, add_column, alter_column, create_view, ...); never edit metastore.yaml. Every table and column needs a `description` of what it holds, or it is refused: descriptions are how agents and people understand the schema. Fill those describe_table lists in `missing_descriptions` with alter_table / alter_column. A body you write links the notes it mentions by name (a one-word name only with a capital, inside a sentence): when a write returns ambiguous_mentions, read the candidate notes and link the right one with update_row and autolink: false, or leave the text. Read the docs of this version with search_docs and read_docs.The tools are every operation of the engine, plus three that read the docs of the installed version. To give agents rules and tools of your own, serve your own MCP server next to it. Each tool’s description says when to call it: list_tables asks the agent to call it first, and add_column asks the agent to read describe_table before it adds a column.
The agent finds tables by their descriptions
Section titled “The agent finds tables by their descriptions”Before it reads or writes, the agent matches your words against what the tables say about themselves:
list_tables: every table and view, with itsdescription,aliasesandtags, so “my watchlist” leads tofilm, whose aliases includewatchlistdescribe_table: the columns, each with its type,description,aliases, limits and, for an enum, the meaning of each value, plus the table’s row count
The meanings come from value_descriptions, which you give when you create the column. On the film diary, describe_table answers this for status:
{ "name": "status", "type": "enum", "values": ["to watch", "watched"], "value_descriptions": { "to watch": "on my watchlist, not seen yet", "watched": "I saw it at least once" }, "ordered": true, "default": "to watch"}Every table and every column has a description, because the engine refuses one without. Agents and people understand the schema through them, so write each for a reader who never saw your notes. Good aliases help too: the better both are, the less the agent guesses. You set them with create_table, add_column, alter_table, alter_column and, for catalogs and schemas, comment_on.
A table created before descriptions were required may lack some. describe_table lists them in missing_descriptions, and the agent fills them with alter_table and alter_column. Describe every table and column shows the rule in use.
The agent reads views first
Section titled “The agent reads views first”A view is a stored query with a name, a description and aliases, such as vw_to_watch: the films whose status is to watch, shortest first. list_tables returns views next to tables, and select_rows reads a view like a table and can narrow it further. When a view answers the question, the agent reads it instead of rebuilding the filter, so the answer follows your definition of “to watch” and not the agent’s. Views shows views with joins, groups and filters on the note they’re shown for.
The agent writes only through checked operations
Section titled “The agent writes only through checked operations”Rows change through insert_row, update_row, update_rows, delete_rows and batch; the schema changes through operations such as create_table, add_column and add_constraint. The engine checks every one against the table’s column types, limits, links and CHECK constraints, and writes nothing when a check fails. Three arguments make an agent’s writes safer:
dry_run: reports what would change, without writingbatch: a group of writes that succeed or fail together, and that you undo as oneexpectonupdate_row: writes only if the row still has the values the agent read, so it doesn’t overwrite an edit you made in the meantime
The agent never edits metastore.yaml: a hand edit skips validation and has no undo. Concurrent writes explains what happens when agents, programs and people write at the same time.
A refusal tells the agent what to fix
Section titled “A refusal tells the agent what to fix”A refused write names the note, the column or rule, and the values the engine accepts. It ends with a hint on how to fix it and a link to the page that shows the fix. For a film with the status seen, the engine answers:
insert_row refused — nothing was written:invalid datanotes/main/public/film/Sunrise.md: status must be one of: to watch (on my watchlist, not seen yet); watched (I saw it at least once)Hint: Use one of the allowed values listed in the message, written exactly; describe_table shows what each one means. To add a value, alter_column the enum. Then call insert_row again (dry_run: true checks without writing).Docs: read_docs("guides/database-objects/columns#describe-what-each-enum-value-means")The agent can correct the value and try again, or ask you. Every other error follows the same pattern: a misspelled table or column gets the closest real name, a wrong filter operator gets the one meant, and a tool argument the tool doesn’t take gets the tool’s arguments. Read the engine’s errors shows them all. Before it writes a link, the agent can call list_link_targets, which returns the rows the link column accepts, such as the genres a film may link to. Over HTTP, a refused write comes back with status 422 and the reasons in the body.
The agent reads the docs of its version
Section titled “The agent reads the docs of its version”datanotes mcp also serves this documentation, read from the installed datanotes package, with three tools: list_docs, read_docs and search_docs. The agent looks up an operation or an example without internet access, and the pages always match the version it talks to. Read the docs offline shows the tools.
Every write names the agent that made it
Section titled “Every write names the agent that made it”The engine records an actor with every change. Through datanotes mcp, the actor comes from the MCP connection:
harnessandharness_version: the client’s name and version from the MCP handshake, such asclaude-codesession: the MCP session, shortened to 16 characterstool: the tool the agent calledauth:owner, or the token’s name for a named tokenmodel: the model, when an MCP server reports it withPOST /datanotes/sessions/identify/
describe_history shows the actor of each commit:
{ "table": "main.public.film", "version": 10, "operation": "UPDATE", "migration_id": "20261009T215306-update_rows-update-film", "actor": { "session": "5d8c0456-3b15-40", "harness": "claude-code", "harness_version": "2.5.0", "tool": "batch", "auth": "owner" }, "batch": "20261009T215306-batch-log-the-screening-of-metropoli"}Three operations read this record:
list_migrations: the journal, newest first, each operation withby, the agent’s namedescribe_history: the commits of one table or of every table, with the full actor; it needs history onlist_changes: every row change with its actor, and withkinds: ["tool_call"]every tool call the MCP server reported, reads included
A script records its own actor with the X-Datanotes-Actor header or the client’s actor option, as the HTTP API guide shows.
Undo what an agent did
Section titled “Undo what an agent did”Because the journal records every write, you can take back one operation or a whole run:
- One operation:
undo_migrationwith itsmigration_id; for abatch, it reverts every write in it - A whole run:
restore_databasewithbefore_migrationset to the agent’s first operation or batch; every table with history goes back to its state before that operation (run it withdry_runfirst) - One table:
restore_tableto a version or a time
The journal records an undo too, so you can undo the undo. The engine refuses an undo that would overwrite a note you edited since, and lists the conflicts. Restore a table or the database shows each operation with the engine’s answers, and the prompts show how to ask for them.
Give each agent only what it needs
Section titled “Give each agent only what it needs”The owner token can do everything. A named token limits an agent to the tables in write, to schema changes only with ddl, and to configuration, undo and restore only with maintain. The journal then names the token with each of its writes. Connect agents shows how to create one and pass it to an MCP client.