Skip to main content

MCP reference

Databases

The `db` action surface — the ten verbs, the tagged-value grammar for writing columns, structural edits, and the permission tiers that gate them.

A Nirvai table is a spreadsheet with rules. It looks like rows and columns, but each column knows what it is willing to hold: a status cell accepts four words and no others, an owner cell accepts people who actually work with you, a link cell accepts one specific row of one specific other table. Your assistant does not get to type whatever it likes into a cell — it asks the column first. Everything below runs through nirvai_execute under the db area; see the MCP reference for the alias grammar.

Always by id, never by name

You almost certainly have two tables called Leads — one from last year, one you actually use. They are different tables with the same label. Your assistant resolves a table to its stable id first, and every verb below is addressed by that id, never by the name it saw in the conversation.


The verbs​

VerbScopeWhat it does
db.selectreadRead rows, with a raw SELECT-only tail — see below.
db.get_richreadRead a record's page body — the document that lives inside the row.
db.insertwriteAdd rows. Per-row partial success: good rows land, bad rows come back with reasons.
db.updatewriteChange rows matching a condition. All-or-nothing — one invalid value and nothing is written.
db.update_richwriteReplace a record's page body. Guarded so it can't silently drop attached media.
db.editwriteChange the table's structure — add, drop, retype or rename columns.
db.create_databasecreateMake a new space to hold tables.
db.create_tablecreateMake a table inside a space, with its columns and views.
db.proposecreateAuthor a reviewable multi-table setup — a space, its tables, columns and views. You click Create.
db.deletedestructiveWithheld. Refused for every token — deletion is not possible over the connection.

Discovery comes from the master tools: nirvai_list_resources for "which tables do I have", and nirvai_describe for "what exactly is this one" — its columns, their allowed values, who can be assigned, which tables it links to, and your permission tier. That is read live, every time.

Where you finish this​

db.propose builds nothing. It hands you a page at /external/databases/…, and one proposal can carry a whole multi-table setup — a space plus several tables, their columns and their views. The page renders one card per package, each with its own Create button.

  • It's a form, not a receipt. Change a column's type or its name, drop a table you don't want, then create — package by package, in whatever order you like.
  • You have to be signed in, and only you can open it. A proposal link is not shareable.
  • db.insert and db.update have no review page. Rows land directly; the review step guards creating structure.
  • Once created, re-opening shows Created ✓ and will not make a second copy — see the review page.

Reading: db.select​

condition is a raw SELECT-only tail — a WHERE, an ORDER BY, a LIMIT. Anything that would change data is rejected outright.

nirvai_execute(
alias = "db.select",
args = { table_session: "…",
condition: "WHERE status = 'Open' ORDER BY id DESC LIMIT 50",
columns: ["id", "name", "status", "owner"] },
description = "List the open deals, newest first"
)
→ { rows, row_count, truncated }

A row ceiling always applies. truncated: true means there was more — your assistant pages through it with its own LIMIT/OFFSET rather than assuming it saw everything.


Writing values: the tagged-value grammar​

Simple columns take a plain value. Structured columns take a tag — a small object whose single key starts with $, naming what kind of thing you mean.

Column typeHow your assistant writes it
text, number, date, checkbox, email, url, phone, rating, currency, progress, colourThe plain value — "Acme", 42, "2025-03-01", true
single choice / status{"$select": "Open"} (or the bare option string)
many choices{"$multi_select": ["Urgent", "Renewal"]}
person{"$person": "jane@company.com"}
link to another record{"$relation": {"table_session": "…", "record_id": 42}}
file{"$file": "quote.pdf"}
agent{"$agent": "…"}

Two rules make this safe. Options are closed — the words a choice column accepts come from nirvai_describe, not from your assistant's imagination, and an unlisted value is refused rather than invented; the same goes for an assignee the table can't actually assign or a link to a row that doesn't exist. And errors are structured, never raw database errors: you get a named reason — INVALID_OPTION, PERSON_NOT_FOUND, UNKNOWN_COLUMN, PROTECTED_COLUMN, OUT_OF_RANGE, RELATION_RECORD_NOT_FOUND — with the column and, where it applies, the allowed set. A failed write tells your assistant how to fix itself.

Protected columns can never be written this way

A record's id, its created and updated timestamps, its page body and icon, and anything managed by the agent-task system are never writable through the record path and never appear as writable fields. The page body is read and written with db.get_rich / db.update_rich, which replaces the whole body — so your assistant reads it first and refuses to drop media unless told to.


Worked example: find, learn, read, write​

nirvai_list_resources(kind = "databases")
→ the tables you have, each with its stable id

nirvai_describe(kind = "databases", ref = "<table id>")
→ columns, allowed options, assignees, relations, views, your permission tier

nirvai_execute(
alias = "db.select",
args = { table_session: "…", condition: "WHERE stage = 'Qualified' LIMIT 20" },
description = "See which leads are already qualified"
)

nirvai_execute(
alias = "db.insert",
args = { table_session: "…",
records: [{ "Company Name": "Acme",
"Stage": {"$select": "Qualified"},
"Owner": {"$person": "jane@company.com"},
"Value": 48000 }] },
description = "Add Acme to the pipeline as a qualified lead"
)
→ { inserted_ids, inserted_count, row_errors }

The describe step is not optional politeness — it is where "Qualified" is confirmed as a real option and jane@company.com as a real assignee, before anything is written.


Changing structure: db.edit​

Adding a column is safe. Dropping, retyping or renaming one is destructive — the data in it goes. So db.edit runs in two beats: a dry run first, which returns the change plan (what would be added, dropped, retyped, renamed, and which saved views would be affected) and touches nothing; then the real edit, which for a destructive change requires an explicit acknowledgement in the call and is refused without it. Your assistant is expected to show you the plan before it acknowledges anything.


Limits & guarantees​

  • Your permission tier comes from ownership, not from the token. You own the table → full access; you are an org admin → write; you are an org member → read. A read-tier table refuses a write even when the token carries the write scope. The tier is shown in nirvai_describe.
  • insert is partial, update is total. Inserting ten rows may land eight and report two; updating either applies completely or not at all.
  • Nothing is created behind your back. db.propose writes a shape — spaces, tables, columns, views — never rows, never data. It becomes real when you click Create, once; re-opening the review page afterwards shows it as already created and will not create it twice.
  • Deletion is impossible. db.delete refuses for every token, always.
  • Writes show up immediately in your open table view and in the Activity Feed.

What's next​