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 agentTablemere
Runs DuckDBRuns the Iceberg catalog and the object store
Reads and writes rowsVends per-table storage credentials that expire
Decides what to keepCompacts to 128 MB files, expires snapshots, removes orphans, hourly
Holds one agent keyConfines the key to the catalogs you chose; revokes it on its own
Reads its limits firstAnswers GET /v1/usage with every limit and its headroom
Asks the human when it mustKeeps 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.

1 · THE AGENT ASKS TO CONNECT

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)
2 · THE PERSON APPROVES IN THE BROWSER

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
3 · THE AGENT DISCOVERS WHAT IT HOLDS

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
4 · CONNECT

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);
5 · INGEST

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;
6 · QUERY, IN THE AGENT'S ENVIRONMENT

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;
7 · WRITE BACK

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;
8 · SHARE

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
9 · RECOVER

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 a request_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/warehouses and /v1/tables take an Idempotency-Key header (--idempotency-key in the CLI). A replay returns the original response; the same key on a different endpoint is 409 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 revoke stops every key of that agent at once. Ten wrong secrets for one key id within ten minutes answer 429 with Retry-After.
  • Over quota, reads keep working. Past 5 GB, creating catalogs or tables and vending write credentials answer 409 quota_exceeded until 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.eu by design; engines mint catalog tokens from the catalog's own credential and refresh them themselves.

Exit codes, one meaning each

CodeMeaningWhat the agent does
0okcontinue
1invalid inputfix the values; never retry unchanged
2limit or conflicta real quota or a real 409; change the plan
3authkey missing, wrong or revoked; ask the human
4not foundno such project, catalog, namespace or table; list them
5server or unreachablesafe to retry with backoff
6usage errorthe 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
ToolsNames
Identitywhoami, usage, terms_status, terms_accept, audit
Catalogslist_warehouses, create_warehouse, delete_warehouse, connection, table_credentials
Accessgrants_list, grants_set, grants_revoke, tokens_list, tokens_create, tokens_update, tokens_revoke
Organisationmembers_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

"I want persistent analytical data for this project on Tablemere. Read https://tablemere.com/docs/agent-setup/ first and follow it. Install the CLI and run tablemere login; show me the link and the code, and I will approve the connection in my browser, creating my account and accepting the Terms there if I have none. Never ask for, print or look for a key: the CLI saves the connection itself. Then check what you are allowed to hold and how much headroom is left, get the DuckDB connection recipe, load my Parquet files into tables, run the analysis in your own DuckDB, and save any result worth keeping as a new table so a later session can open it. Stop and ask me before anything that deletes data."
Claude CodeCodexCursorany agent with HTTP

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.