Reference
MCP server
Every environment of a hosted dabadata project can be an MCP server. Developers' agents (Claude Code, Cursor) use it with the secret key to build and debug the app; people use it inside ChatGPT and Claude, signed in as themselves, with the app's tables and forms drawn as live widgets in the conversation. A self-hosted server also has a local stdio server, at the end of this page.
The hosted MCP server
The address is https://dabadata.com/mcp/<project id> for live and https://dabadata.com/mcp/<project id>_test for test (on a self-hosted server, /mcp/<project name> for its default project). It speaks MCP over Streamable HTTP: JSON-RPC in a POST, answered as JSON, no session needed. Every call goes through the same rules, trace and usage as the HTTP API, with source: "mcp" in the trace.
Turn it on
The server is off until the definition has an mcp block with enabled: true (until then the address answers 404):
"mcp": {
"enabled": true,
"name": "Team expenses",
"instructions": "Track shared expenses. Amounts are in euros.",
"allowAnonymous": false,
"tables": {
"expenses": { "tools": ["list", "get", "create", "update"], "widget": "table", "describe": "One row per expense" }
}
}| Key | Meaning |
|---|---|
enabled | true turns the server on. |
name | What the agent and the chat call the app (1 to 80 characters). Default: the project's title. |
instructions | Text for the model about how to use the app (up to 8,000 characters). Default: a generated text naming the tables and describe_app. |
allowAnonymous | true lets clients with no credential in, as anonymous under the rules. Default false. |
tables.<t>.tools | Which of list, get, create, update, delete the table offers. Default: every operation the rules could ever allow. |
tables.<t>.widget | table, form, record, none, or the name of a custom widget. |
tables.<t>.describe | Text added to the table's tool descriptions. |
Without tables every declared table gets its default tools and no widget. Apply it like any definition change: a dry run, then PUT /v1/schema, the console's Definition page, or the agent's apply_definition. The console's Apps page shows whether it is on and adds the block for you.
Who connects, and with what
Credential (Authorization: Bearer) | Gets |
|---|---|
| The environment's secret key | The developer toolset: the app's tools plus the tools that build and debug it. Rules are bypassed, as with the secret key on /v1. For your own coding agent only. |
| A user token of the environment, a token from the environment's OAuth sign-in, or a provider JWT it trusts | The user toolset: describe_app and the tables' tools, every call under the rules as that person. |
| Nothing | 401. When the project has hosted end-user accounts (auth.users.enabled), the 401 points the client at the project's OAuth server, so ChatGPT and Claude start a sign-in on the project's own page and receive a user token for that person. With allowAnonymous: true, an anonymous session instead. |
A key or token of another environment is refused (401); the address alone grants nothing. View tokens and refresh tokens are not accepted here.
Tools
| Tool | Toolset | What it does |
|---|---|---|
describe_app | both | Tables, columns, tools and widgets, and for the caller whether read, create, update and delete are allowed in each table - with the rule's explanation and whether it covers all rows, some (rows where ...) or none. Also the actions and views the caller may use. The first call. |
<t>_list | both | where, orderBy, desc, limit (1 to 200, default 50). The rows the caller may read. The input schema offers only columns the caller may read on every row. |
<t>_get, <t>_create, <t>_update, <t>_delete | both | One row by id; values to create or change. Input schemas come from the columns. |
transact | both | Several writes and checks, all or nothing, like POST /v1/transact. |
action_<name> | both | One per action: { params }, run like POST /v1/actions/<name>. An action that calls a model says so in its description and returns ai as the HTTP route does. |
view_<name> | both | One read-only tool per view: { params?, where?, orderBy?, desc?, limit? } (limit at most the smaller of the view's max and 200). The result is { view, rows, params, traceId }. Listed for users when the view's allow could let someone in. |
get_definition | developer | The definition in force. |
apply_definition | developer | { definition, dryRun }: exactly PUT /v1/schema - validation, plan, version, audit. |
migrate | developer | { steps, definition?, dryRun?, confirm? }: exactly POST /v1/schema/migrate - drop, rename, change a type. The agent is told to dry-run first and show the result. |
trace_recent | developer | The newest trace rows (limit, table, user). |
simulate | developer | Would this user be allowed? Changes nothing. |
sql_read | developer | Read-only SQL on the environment's tables (when the server's SQL console is on). |
widgets_list, widget_put | developer | List and store custom widgets. |
sites_list, site_create | developer | The environment's sites with their URLs, status and domains; create one ({ name, spa? }). |
site_deploy | developer | { site, files: [ { path, content, encoding? } ] }: uploads the files on the server and publishes them, so an agent can build a page and put it live in one call. encoding is "utf-8" (default) or "base64"; at most 10 MB per call. A deploy is the whole site. Waits up to 30 s for the readiness check and says whether the site's hostnames serve it. |
site_rollback | developer | Make an earlier deploy of a site live again. |
site_domain_add, site_domain_check | developer | Connect a custom domain (hostname, redirectFrom?) and get the DNS record to add; check a pending domain now. |
A result carries the data as structuredContent ({ rows }, { row }) and as text. A denial is a tool error whose text is the rule's explanation, so the model can tell the person why, or fix the rule. Rows are shaped as on /v1: a column the caller may not read is left out, and a write answers { id } for a row the caller may not read.
Strict tables in the user toolset. A strict table (access: "views") has no <t>_* tools for end users: describe_app and the server's default instructions leave it out, the transact tool's table list does not offer it (an op naming it is refused with the 403 explanation), and its built-in widget is not found. The views and actions over it are listed as usual - that is how a person's chat reads and writes it. The developer toolset is unchanged.
describe_app adds views.<name> - { tool, table, allow, params, columns, computed, filter, sort, limit, access }, access being the allow rule's decision for the caller - and one text line per view, view <name>(<params>) over <table>: <explanation>. Columns the caller can never see (a column read rule of nobody, or another role) are left out of tables.<t>.columns; the rules themselves are shown as they are.
Widgets
A table with a widget shows its results as an interactive view in the chat (the MCP Apps extension; ChatGPT reads the same metadata). The built-ins are table (a list with add, edit and delete), form and record. Each result also carries a 15-minute read-only live token for the same caller, so the widget subscribes to the table under that person's read rule and rows changed anywhere - another chat, your web app, the console - appear while they watch.
A custom widget is one HTML document of at most 256 KB, stored per environment with widget_put, the console, or PUT /v1/project/widgets/:name, and served as ui://app/<name>. Including <script src="https://dabadata.com/mcp-widget.js?v=5bce207b"></script> gives it the host bridge and the live helper:
<!doctype html>
<main id="total">...</main>
<script src="https://dabadata.com/mcp-widget.js?v=5bce207b"></script>
<script>
const w = window.DabadataWidget;
const show = (rows) => {
const sum = rows.reduce((s, r) => s + r.amount, 0);
document.getElementById("total").textContent = `Total: ${sum.toFixed(2)}`;
};
w.on("result", (result) => {
show(w.rowsOf(result));
w.live(w.liveMeta(result), { onRows: show }); // stays current while the chat is open
});
</script>Writes from a widget go through w.callTool("<t>_create", { values }), so they are the signed-in person's and the rules decide.
Connect a client
The one-line setup for Claude Code and Cursor, and how people add the app to ChatGPT and Claude as a connector, are on Build with an AI agent.
The local server (stdio)
For a self-hosted server: an MCP server that runs on your machine and talks to dabadata over HTTP with the project key. The hosted server above needs none of this.
How it acts
The agent is an ordinary end user. When the MCP server starts, it uses the project key to mint a user token for the identity you configure, and every row it reads or writes goes through that token. So the agent is held to the same rules as any person using your app, and its calls show up in the trace under its own id (with source http for now).
The tools that look at the project as a whole - get_schema, read_trace, simulate, reload_schema and mint_user_token - use the project key.
Tools
| Tool | What it does |
|---|---|
get_schema | The schema JSON plus a plain-language summary of each table's rules. |
list_rows { table, where?, limit?, orderBy?, desc? } | The rows the agent may read. |
get_row { table, id } | One row, or the explanation of why it is hidden. |
mutate { table, op, id?, values? } | Insert, update or delete. A denial is a normal result with explanation and traceId, so the agent can reason about it. |
read_trace { limit?, table?, user? } | One line per interaction: #id time source op table rowId user(role) decision durationMs - explanation. |
simulate { user, table, op, row?, values? } | Would this user be allowed? Changes nothing, not traced. |
reload_schema | Re-import projects/<name>.ts, create missing tables and columns, report drift. |
mint_user_token { id, name?, role } | A 30-day user token plus a ready to-do demo URL. |
Install
cd mcp && bun installThe dabadata server must be running; the MCP server talks to it over HTTP.
Add it to Claude Code
Use the absolute path of your checkout in place of /path/to/dabadata:
claude mcp add dabadata -- bun /path/to/dabadata/mcp/src/index.tsWith a different identity or server:
claude mcp add dabadata \
-e DABADATA_URL=http://localhost:4600 \
-e 'DABADATA_MCP_USER={"id":"bot","name":"Helper bot","role":"editor"}' \
-- bun /path/to/dabadata/mcp/src/index.tsAdd it to Claude Desktop
Edit ~/Library/Application Support/Claude/claude_desktop_config.json (on macOS):
{
"mcpServers": {
"dabadata": {
"command": "bun",
"args": ["/path/to/dabadata/mcp/src/index.ts"],
"env": {
"DABADATA_URL": "http://localhost:4600",
"DABADATA_MCP_USER": "{\"id\":\"mcp\",\"name\":\"MCP agent\",\"role\":\"admin\"}"
}
}
}
}Claude Desktop may not have Bun on its PATH. If the server does not start, use the full path that which bun prints as command.
Environment and identity
| Variable | Default | Meaning |
|---|---|---|
DABADATA_URL | http://127.0.0.1:4600 | The dabadata server. |
DABADATA_PROJECT_KEY | read from the repository's .env | The project key; mints the agent's token and reads the trace. |
DABADATA_MCP_USER | {"id":"mcp","name":"MCP agent","role":"admin"} | Who the agent is, as JSON. role must be a declared role. |
Pick the identity on purpose. An agent with the viewer role can look but not touch; an editor can create and change its own rows; admin can do what admins can. Whatever it does is in the trace under that id.
Diagnostics go to stderr; stdout is the MCP protocol.
An example conversation
You: What tables are there, and can a viewer delete tasks?
Claude (calls
get_schema): There aretasksandannouncements. Anyone signed in can read tasks; admins and editors can create them; an admin or the task's owner can update; only admins can delete. So no, a viewer cannot delete tasks.You: Carol says she couldn't delete something. What happened?
Claude (calls
read_trace { user: "carol" }):#39 14:02:11 http delete tasks 5f1c... carol(viewer) denied 3ms - delete tasks: denied, role "viewer" not in ["admin"]. Carol is a viewer and only admins may delete tasks. Give her the admin role, or change thedeleterule inprojects/default.tsand callreload_schema.