For agents
Persistent analytical data for agents.
An agent that analyses data needs somewhere to keep it once the session ends. Tablemere gives your agents a shared home for the data they create and use: Apache Iceberg tables in a catalog that outlives the conversation, queried with DuckDB wherever the agent runs. Your agent brings the compute. Tablemere stores and maintains the data.
The session ends. The data remains.
A table an agent writes today is an Iceberg table in a managed catalog, not a file in a sandbox that is gone at the next start. Compaction, snapshot expiry, orphan cleanup and daily off-host backups run on it whether or not anyone is connected.
Another agent opens the same table.
A later session, a second agent, a colleague with PyIceberg or Spark: everyone points an engine at the same catalog and reads the same rows. Access is a grant per catalog, read or write, per person and per agent key.
The engine is the agent's own DuckDB.
The agent asks Tablemere for a connection recipe, then talks to the catalog and the object store directly. No row travels through Tablemere's compute; there is no query endpoint to be slow, metered or in the way.
01Who does what
The agent brings the compute. Tablemere stores and maintains the data.
Tablemere is an Iceberg REST catalog and S3-compatible object storage run as one system in Nuremberg, Germany. It hands out credentials, keeps the tables healthy and records who did what. Everything that touches rows happens in the engine your agent runs, on the machine your agent runs on.
DuckDB 1.5.5 or newer reads and writes the tables; so do PyIceberg 0.12 and Spark 3.5 with Iceberg 1.11. The recipes for all three come from one command, tablemere connect, and the same table is readable from each. Snowflake connects to the catalog today; its data path is not available yet.
| Your agent | Tablemere |
|---|---|
| Runs DuckDB | Runs the Iceberg catalog and the object store |
| Reads and writes rows | Vends per-table storage credentials that expire |
| Decides what to keep | Compacts to 128 MB files, expires snapshots, removes orphans, hourly |
| Holds one agent key | Confines the key to the catalogs you chose; revokes it on its own |
| Reads its limits first | Answers GET /v1/usage with every limit and its headroom |
| Asks the human when it must | Keeps signup, Terms and key recovery as a person's actions |
02The lifecycle
Create, ingest, discover, query, write back, share, recover.
Nine steps, each a real command or a real API call. The person's part is approving once in the browser, creating the account there if needed; from the third step on, the agent works alone with its own connection. Placeholders in angle brackets stand for values the previous step returned; no credential on this page is real.
The agent installs the CLI and runs tablemere login. The CLI prints a link with a code, opens the browser when there is one, and waits up to fifteen minutes; the agent shows the person the link. --level read asks for read only, --name names the connection, --catalog picks one; the default is the organisation's default catalog with read and write.
$ curl -fsSL https://tablemere.com/install.sh | sh
$ tablemere login
# Open https://tablemere.com/app/device?user_code=XXXX-XXXX
# and approve the connection in your browser (code XXXX-XXXX).
# … waiting (up to 15 minutes)
The person opens the link. With no account yet, the same page offers to create one: name, email, organisation, the link in the mail, a password, and the Terms; the code survives. Then a consent screen names the machine, the catalog and the access asked for, which they may lower to read. On approval the CLI receives the key of a new agent named after the machine, saves it to ~/.config/tablemere/credentials.json and never shows it. Revoke it any time under Connections & tokens. A key can also be minted by hand for a machine without a browser.
# the CLI, once approved: Connected to My warehouse (read and write) as laptop. Saved to ~/.config/tablemere/credentials.json. # by hand, from a person's terminal, key shown once: $ tablemere agent create --name reporter \ --grant <warehouse_id>:write
Before touching anything, the agent reads its identity, its limits and the tables that exist. Output is JSON whenever stdout is not a terminal, so a script parses the same thing a person reads. GET /v1/usage lists every limit, the current value and the headroom: a limit is never met by failing.
$ tablemere whoami # who am I, my limits, what I hold on each catalog $ tablemere usage # storage 5 GB, catalogs 3/project, tables 50, namespaces 10, # objects 50,000, commits 20,000/month, each with its headroom $ tablemere table list --catalog lake $ tablemere table get --catalog lake --namespace demo --name cities # schema, format version, snapshots, rows, data_files
One command returns the paste-ready SQL for the agent's DuckDB (1.5.5 or newer), with the catalog's credential inside. The engine exchanges it for catalog tokens (900 s) and per-table storage sessions itself. ATTACH the bare bucket name: 'w-<uuid>' is read-write, the s3:// form attaches read-only. The recipe's fourth statement, an S3 secret for inspecting the bucket, is not needed for tables.
$ tablemere connect --catalog lake --engine duckdb INSTALL iceberg; LOAD iceberg; INSTALL httpfs; LOAD httpfs; CREATE SECRET tablemere (TYPE ICEBERG, CLIENT_ID '<client_id>', CLIENT_SECRET '<client_secret>', OAUTH2_SERVER_URI 'https://catalog.tablemere.eu/v1/oauth/tokens'); ATTACH 'w-<warehouse_uuid>' AS lake (TYPE ICEBERG, ENDPOINT 'https://catalog.tablemere.eu', SECRET tablemere);
Importing Parquet is one statement, from a local file or a URL; namespaces are schemas. Measured against the service: a million rows, 18 MB of Parquet, in about 2.3 s from a laptop, three data files, one commit. The result is an Iceberg table, format version 2.
-- a new table, from a file or a URL CREATE TABLE lake.demo.events AS SELECT * FROM read_parquet('events.parquet'); -- into a table that exists INSERT INTO lake.demo.events SELECT * FROM read_parquet('more-events.parquet'); COPY lake.demo.events FROM 'more-events.parquet' (FORMAT PARQUET); -- namespaces are schemas CREATE SCHEMA lake.staging;
From here on it is DuckDB. The engine lists the table through the catalog, receives storage credentials scoped to that table, and reads the Parquet files from s3.tablemere.eu itself. Tablemere sees the catalog call and the object reads; it never sees a result set.
SELECT date_trunc('day', event_time) AS day, count(*) AS events, avg(value) AS mean_value FROM lake.demo.events GROUP BY 1 ORDER BY 1;
A saved result is a table another session can open. One CTAS and the summary lives next to the source, versioned and maintained like it. CTAS makes every column optional; when identifier fields, required columns or partitioning matter, create the table first with tablemere table create --column name:type[:required][:identifier], then INSERT.
CREATE TABLE lake.demo.daily_summary AS SELECT date_trunc('day', event_time) AS day, count(*) AS events, avg(value) AS mean_value FROM lake.demo.events GROUP BY 1; -- a later session, another agent, a colleague: SELECT * FROM lake.demo.daily_summary ORDER BY day DESC LIMIT 7;
Access is a grant per catalog: read or write, to a member or to an agent key. A read holder's connect carries the read-only identity; the catalog refuses its commits and the store its writes. Colleagues join by invitation, or automatically once your organisation has claimed its email domain.
$ tablemere org invite --email colleague@company.com --role member # the invitation token is returned once and mailed $ tablemere catalog grant --catalog lake \ --principal <principal_id> --level read $ tablemere agent create --name analyst --grant <warehouse_id>:read $ tablemere catalog grants --catalog lake # who holds read or write, and how: admin, membership, creator, grant
A person's lost key is replaced by a code to their email, never recovered; an agent's lost key is revoked and a new agent created. The audit log answers who did what, for 400 days. Snapshot history is bounded: maintenance keeps 20 snapshots and 7 days per table, so time travel reaches back that far and no further.
$ tablemere recover --email you@company.com $ tablemere recover --email you@company.com --code 123456 --save # a NEW key; earlier keys keep working until revoked $ tablemere agent revoke --agent-id <agent_id> # every key of that agent stops at once $ tablemere audit --since 7d --action credentials.vend $ tablemere catalog rotate --catalog lake # new catalog credentials; old tokens refused now, vended # storage sessions run out within 900 s; fetch a fresh recipe
03Guard rails
What makes it safe to hand to an agent.
Designed for a caller that cannot read a dashboard
- Every error is typed. The body is
{"error": {"code", "message", …}}with, where it helps,remedy,limit,current,retry_after_seconds, and always arequest_id. The code says whether to change the request or the plan; the remedy says how. - The exit code carries the class. The CLI maps every failure to one of six codes (table on the right) and prints the server's typed body to stderr verbatim.
- Limits are readable before you hit them.
GET /v1/usage(tablemere usage) returns every limit, the current value and the headroom. Read it before provisioning. - Mutations are idempotent.
POST /v1/projects,/v1/warehousesand/v1/tablestake anIdempotency-Keyheader (--idempotency-keyin the CLI). A replay returns the original response; the same key on a different endpoint is409 idempotency_key_reused. - Storage credentials expire. Engines receive per-table credentials that live about an hour; catalog tokens live 900 s. A leaked credential is a bounded problem.
- Agent keys are confined and revocable. A key holds exactly the grants it was given, on the catalogs you chose, and
tablemere agent revokestops every key of that agent at once. Ten wrong secrets for one key id within ten minutes answer429withRetry-After. - Over quota, reads keep working. Past 5 GB, creating catalogs or tables and vending write credentials answer
409 quota_exceededuntil space is freed; reading through the catalog credential continues. - The catalog accepts only its own tokens. A Tablemere API key is 401 at
catalog.tablemere.euby design; engines mint catalog tokens from the catalog's own credential and refresh them themselves.
Exit codes, one meaning each
| Code | Meaning | What the agent does |
|---|---|---|
| 0 | ok | continue |
| 1 | invalid input | fix the values; never retry unchanged |
| 2 | limit or conflict | a real quota or a real 409; change the plan |
| 3 | auth | key missing, wrong or revoked; ask the human |
| 4 | not found | no such project, catalog, namespace or table; list them |
| 5 | server or unreachable | safe to retry with backoff |
| 6 | usage error | the command line is malformed; fix it, never retry |
The HTTP API and the CLI are the same calls; the exit code is the CLI's rendering of the response class. Full list of routes, errors and shapes: API reference, CLI reference.
04MCP
The same account as tools, for hosts that speak the protocol.
The Tablemere MCP server gives Claude Code, Claude Desktop, Cursor or any MCP host the account as tools: catalogs, connection recipes, short-lived table credentials, grants, tokens, members, the organisation, usage, the audit log and the Terms status. It is the third face of the same REST API as the CLI and the account page; it adds no permission of its own, caches nothing and sends no telemetry.
There is deliberately no query tool. Rows in a tool response are tokens in a context window and compute on our side; both are the wrong place for them. The agent calls connection and runs the recipe in its own DuckDB. Signup, login and key recovery are not tools either: those remain a person's actions.
The server is installable from a checkout today. The package name tablemere-mcp is reserved and not yet published on PyPI. Everything else, including the schema of every tool: /docs/mcp/.
$ tablemere mcp install # Claude Code: `claude mcp add-json`, user scope; the saved key # travels into the host's configuration and is never printed $ tablemere mcp install --client claude-desktop $ tablemere mcp install --print # the mcpServers snippet for any other host, key masked $ tablemere mcp run -- --list-tools # every tool with its JSON schema
| Tools | Names |
|---|---|
| Identity | whoami, usage, terms_status, terms_accept, audit |
| Catalogs | list_warehouses, create_warehouse, delete_warehouse, connection, table_credentials |
| Access | grants_list, grants_set, grants_revoke, tokens_list, tokens_create, tokens_update, tokens_revoke |
| Organisation | members_list, members_invite, members_set_role, members_remove, org_get, org_rename |
Every read carries readOnlyHint; delete_warehouse, grants_revoke, tokens_revoke and members_remove carry destructiveHint, so hosts ask before running them. Today the account-management tools want a human member's key, which is what tablemere mcp install uses by default; a confined agent key serves the data-side tools (connection, table_credentials, usage, its own audit).
05Start
Paste this to your agent.
A task in plain words, with the human step kept
What the agent will find there
- /docs/agent-setup/ is the page written for the agent: install, connect, the recipe, the first task. /llms.txt is the index; /llms-full.txt is the whole documentation as one plain-text file.
- The CLI is one Python file, standard library only, verified against a published SHA-256: /docs/cli/.
- The API is JSON in, JSON out, and every reply names the next call: /docs/api/.
- The engines, with every measured gotcha: /docs/engines/.
- The human's part is small: approve the connection once in the browser, creating the account and accepting the Terms there if needed, revoke it from the account page when done, and recover their own key if it is lost.
Tablemere SL is a company in formation in Spain; the service runs in Nuremberg, Germany. No SLA yet; we publish what we measure. Paid plans are to be announced; the free plan, 5 GB stored and no card, is the offer today.
Free to start: 5 GB, no card. Data in Nuremberg, Germany.