The Tablemere MCP server

The MCP server is published from the repository note docs/mcp.md, as written on 2026-09-20. It is installable from a checkout (uvx --from <checkout>/platform/mcp tablemere-mcp); the package name tablemere-mcp is reserved and not yet on PyPI. tablemere mcp install (CLI reference) writes the host configuration below for you, with the saved key and without showing it.

tablemere-mcp gives an MCP host (Claude Desktop, Claude Code, Cursor, anything that speaks the protocol) the Tablemere account as tools: catalogs (a catalog is a warehouse in the API and in the tool names), connection recipes and short-lived table credentials, grants, tokens, members, the organisation, usage, the audit log, Terms. It is the third face of the same REST API as the CLI and the account page (docs/permissions-and-access.md), and it deliberately does not query data: the agent gets a connection recipe and runs DuckDB itself.

Source: platform/mcp/tablemere_mcp.py (one file). Transport: stdio. SDK: the official mcp Python package, pinned.

Install

The shortest path is the CLI, which already holds your key (tablemere signup --save or tablemere login --save wrote it to ~/.config/tablemere/credentials.json):

tablemere mcp install                          # Claude Code: `claude mcp add-json`, user scope
tablemere mcp install --client claude-desktop  # Claude Desktop: merges into claude_desktop_config.json (backup first)
tablemere mcp install --print                  # the snippet for any other host, key masked
tablemere mcp run -- --list-tools              # start the server yourself, key in its environment

The key travels from the credentials file into the host's configuration and is never printed; the output shows it masked (al_live_<key id>_********). --api-key installs a different key, for instance a token's; --from says what uvx installs (a checkout's platform/mcp, the default when the CLI runs from one, or the package name once it is published); --url picks the API and the saved key for it. tablemere mcp install --help has the rest.

The server itself is a Python package in the repo; there is no PyPI release yet. By hand, any of:

# run without installing (uv), from a checkout
uvx --from /path/to/tablemere/platform/mcp tablemere-mcp --list-tools

# install as a command (pipx)
pipx install /path/to/tablemere/platform/mcp
tablemere-mcp --version

# or build a wheel once and install that anywhere
cd platform/mcp && uv build && pipx install dist/tablemere_mcp-0.1.0-py3-none-any.whl

Python 3.10 or newer. --list-tools prints every tool with its JSON schema and exits; without it the server speaks MCP on stdin/stdout and is meant to be started by the host, not by hand.

Configure the host

Two environment variables, nothing else:

variable meaning default
TABLEMERE_API_KEY a token's API key (al_live_…). Create one in the account page (Tokens) or with tablemere agent create --name claude --grant <warehouse_id>:write; the key is shown once none; every tool then answers missing_credential
TABLEMERE_URL the API https://api.tablemere.eu

Claude Desktop (claude_desktop_config.json) and Claude Code (.mcp.json in a project, or claude mcp add-json tablemere '<the object>') take the same mcpServers shape:

{
  "mcpServers": {
    "tablemere": {
      "command": "uvx",
      "args": ["--from", "/path/to/tablemere/platform/mcp", "tablemere-mcp"],
      "env": {
        "TABLEMERE_API_KEY": "al_live_...",
        "TABLEMERE_URL": "https://api.tablemere.eu"
      }
    }
  }
}

With a pipx install, "command": "tablemere-mcp" and no args. tablemere mcp install writes exactly this object (with uvx by absolute path, because hosts start servers with a short PATH). Give the server a token created for it, not a human's key, once tokens can hold organisation roles (below): a token can be confined to the catalogs it needs and revoked on its own, and its actions appear under its own name in the audit log. Today the account-management tools need a human member's key, which is what tablemere mcp install uses by default.

Tools

Every tool's arguments are typed and described in its schema (tablemere-mcp --list-tools). Where the API wants an id, the tools also accept the name (catalog, project, organisation) and resolve it; an ambiguous name is an error listing the candidates, never a guess. When the token belongs to one organisation or sees one project, org / project can be omitted.

tool what it does API
whoami principal id, organisations and role in each, projects, Terms status GET /v1/usage, /v1/orgs, /v1/terms
usage every limit, current value and headroom; storage bytes per organisation and catalog GET /v1/usage
list_warehouses the catalogs the token can see, grouped by project, with locations and catalog URI GET /v1/projects + GET /v1/warehouses
create_warehouse a new catalog, in the organisation's default project unless one is named; takes an idempotency key (one is generated if omitted) POST /v1/warehouses
delete_warehouse irreversible: drops every table, both buckets, both identities, grants and usage; needs the catalog's exact name as confirm, checked before anything is sent; admins only DELETE /v1/warehouses/{id}
connection the paste-ready DuckDB / PyIceberg / Spark recipe for a catalog GET /v1/connection
table_credentials short-lived S3 credentials scoped to one table's prefix (about an hour) POST /v1/credentials
grants_list who holds read or write on a catalog and via what (membership, creator, admin, grant) GET /v1/warehouses/{id}/grants
grants_set give a member or token read or write on a catalog PUT /v1/warehouses/{id}/grants/{principal}
grants_revoke remove a grant DELETE /v1/warehouses/{id}/grants/{principal}
tokens_list the organisation's tokens (agents): id, name, grants, creator, last used GET /v1/orgs/{org}/agents
tokens_create a new token with grants [{warehouse, level}]; its key is returned once; at least one grant POST /v1/orgs/{org}/agents
tokens_update rename a token and/or replace its grants PATCH /v1/orgs/{org}/agents/{id}
tokens_revoke revoke a token: every key stops at once DELETE /v1/orgs/{org}/agents/{id}
members_list members with role and join date GET /v1/orgs/{org}/members
members_invite invite an address as admin or member; the invitation token is returned once POST /v1/orgs/{org}/invitations
members_set_role admin or member; the last admin cannot be demoted PATCH /v1/orgs/{org}/members/{id}
members_remove remove a member DELETE /v1/orgs/{org}/members/{id}
org_get the organisation(s): name, your role, domains, member count GET /v1/orgs
org_rename rename the organisation (admins) PATCH /v1/orgs/{org}
audit who did what: own events, or the organisation's for admins; since, action, limit GET /v1/audit
terms_status the Terms version in force and whether it is accepted GET /v1/terms
terms_accept record acceptance, after the human has read them POST /v1/terms/accept

Tools carry the MCP annotations hosts use to decide when to ask the user: readOnlyHint on every read, destructiveHint on delete_warehouse, grants_revoke, tokens_revoke, members_remove.

Errors

A failed call is an isError result whose content is the API's error envelope, unchanged, plus the HTTP status:

{"error": {"code": "quota_exceeded", "message": "3 of 3 warehouses.", "limit": 3, "current": 3,
           "remedy": "Delete one, or ask for a higher limit.", "request_id": "…"},
 "http_status": 409}

The code says whether to change the request or the plan; the remedy says how. Three envelopes are the server's own: missing_credential (no key configured), unreachable (no answer from TABLEMERE_URL; safe to retry), and route_missing — the deployed control plane predates a route of the 2026-09-20 contract (the grants routes, tokens_update, delete_warehouse). Nothing was changed in that case; the envelope names the method and path.

Today's control plane (measured 2026-09-20 against the local stack)

The server is written against the 2026-09-20 contract; the control plane is catching up. Driven with a human member's key (the signup principal), every tool answered, and grants_list, tokens_update and delete_warehouse came back route_missing (their routes do not exist yet; nothing was changed). Driven with an agent token's key created by that human:

tool as an agent token, today
whoami, usage, list_warehouses, connection, table_credentials, audit (own), terms_status work
org_get answers, with an empty list: the token is not a member of the organisation that created it
tokens_*, members_*, org_rename, audit with org no_organization (this server's envelope), for the same reason
create_warehouse 403 forbidden from the API: only an owner or admin may create a catalog
list_warehouses lists every catalog of the project, including those the token is confined out of; the API answers 404 on use, as it does for a stranger

The measured limitation, in one sentence: on today's control plane a token (principal kind agent) cannot call the organisation routes (/v1/orgs/…: tokens, members, the organisation itself, the organisation's audit log), because a token is not a member of the organisation that created it and the routes check membership. Nothing in this server can change that; it is a contract follow-up: docs/permissions-and-access.md already writes the MCP server as "authenticated with a token's API key", so either tokens gain organisation roles or the organisation routes accept a token as acting for its organisation within its grants, and the control plane has to implement it before that sentence is true for the account tools.

So until that lands, the account-management tools want a human member's API key: tablemere login --save (or tablemere signup --save) writes one to the credentials file, and tablemere mcp install uses it by default; tablemere api-key create --label mcp mints a separate key for the server, shown once, revocable on its own (tablemere api-key revoke). A confined agent token serves the data-side tools (connection, table_credentials, usage, own audit). The no_organization remedy says this to the agent.

Security model

  • The token's grants bound every tool. The server adds no permission of its own and holds no second credential; a read-only token gets read-only recipes and credentials, and a 403 from the API is returned as such. What an admin can do through the tools is exactly what that admin's token can do with curl.
  • The key is configuration, not conversation. It reaches the process through the environment and leaves it only as the Authorization header to TABLEMERE_URL. It is never logged, never part of a result, never in a traceback (Config.__repr__ prints set/unset). Keys the API creates on request (tokens_create, members_invite's invitation token) are returned once because that is what was asked for; store them where they belong, not in the chat.
  • Nothing is cached. Every tool call is one or a few HTTP requests made now; there is no local state, no credentials file, no memory between calls. Revoking the token ends the server's access at the next call.
  • No telemetry. The server talks to TABLEMERE_URL and to nothing else; the only thing it sends beyond the request is a User-Agent: tablemere-mcp/<version>.
  • Destructive actions are refused client-side when the confirmation is wrong (delete_warehouse compares confirm with the resolved name before sending) and the API refuses them again server-side.
  • Use HTTPS. TABLEMERE_URL defaults to https://api.tablemere.eu; a plain http:// URL is for a local stack only.

What is not there, on purpose

  • No query tool, no peek, no natural-language-to-SQL. Rows in a tool response are tokens in a context window and compute on our side; both are the wrong place for them (research/13 §1). The agent calls connection and runs the recipe in its own DuckDB:

    INSTALL iceberg; LOAD iceberg; INSTALL httpfs; LOAD httpfs;
    -- then the CREATE SECRET / ATTACH statements exactly as `connection` returned them
    SELECT count(*) FROM lake.<namespace>.<table>;
  • No table creation or schema tools. DuckDB does CREATE TABLE … AS SELECT * FROM read_parquet() against the attached catalog (platform/docs/import-parquet.md); a tool would only add a hop.

  • No signup, login, key recovery or account deletion. Those are a human's actions in the account page or the CLI (tablemere signup|login|recover|account), not something an agent should do with someone else's key.

  • No metadata plane yet (row counts, partitions, freshness, plan_query from research/13 §2): it needs API routes first; the MCP server only ever projects what the API offers.

Tests

/tmp/mcp-venv/bin/python -m pytest platform/mcp/tests -q          # 34 tests, no network
TABLEMERE_URL=http://localhost:8080 TABLEMERE_API_KEY=al_live_... \
  /tmp/mcp-venv/bin/python platform/mcp/tests/live_smoke.py        # skipped when the key is unset
python3 -m pytest platform/cli/tests -q                            # tablemere mcp install|run: 17 tests, temp HOME

The unit tests drive every tool through the MCP layer against an httpx.MockTransport that plays the API, and assert the request shape (method, path, query, JSON body, Authorization, Idempotency-Key) and the error paths. The live smoke creates one catalog, one token and one invitation under the configured principal, then revokes and deletes what the deployed API lets it; run it under a throwaway principal.