# MCP

MCP uses the same operations as the [API](/docs/reference/api/). The hosted endpoint is `https://app.adoor.halmoslabs.com/mcp`. Send `Authorization: Bearer` with a connection credential. These pages are public: [the docs](https://adoor.halmoslabs.com/docs/) and [the agent index](https://adoor.halmoslabs.com/llms.txt). OAuth metadata `service_documentation` and the MCP server instructions point at those two URLs.

`query`, `execute`, `context`, `history`, `undo`, `export`, and `request_access` are always listed. Workspace and space tools, including `spaces_create`, `spaces_archive`, and `spaces_unarchive`, are always listed too. Sharing, organizing, billing, and account changes are separate tools, one per operation. `import` is listed for ChatGPT only. Claude, Grok, Cursor, and Muse do not see it. A tool that needs access you don't have returns an error that says what to request with `request_access`. Grants are checked when the tool is called. Each heading below is one tool. Listing sessions, revoking a session, signing out everywhere, changing email, and deleting the account stay on the website and the CLI. They are not MCP tools.

Call `context` to see the signed-in account email and every workspace this connection can open, with names and slugs. Call `whoami` for a stable account id, the account name, the email, and a nickname of the account and the workspace. The id does not change when the connection is refreshed, reconnected, or its access changes. Pass `workspace` as an id or slug on any tool to use one of those workspaces. Workspaces belong to the account chosen at sign-in. To use a different account, reconnect and sign in as it. A name that is not listed does not exist. The server says so, lists the current tools, and tells the client to call `context` and to refresh the connector's tool list. ChatGPT keeps the tool list for an existing conversation: refresh the connector in app settings, then start a new chat. Codex, Claude, and Cursor pick up `tools/list` on a new session.

Federations and consent stay on the website. `open_checkout` and `open_billing_portal` return a link the user opens. Checkout records a claim on the workspace billing account, and either call can create a Stripe customer and store its id. No card data is returned, this call makes no charge, and the plan does not change until a webhook. `export` returns a download link for the whole workspace.

Each tool accepts an optional `workspace` id or slug. The call uses that workspace only when this connection can open it. Any other value is the same error as a missing workspace. `workspaces_create` accepts that selector and an optional `account` id. The new workspace belongs to the signed-in account and is not placed inside the selected workspace. A different account id creates nothing. There is no organization. The text content and the structured result are the same JSON, with a one-line summary before the JSON. A link in a tool result is `url` plus `kind`. `approvalUrl` and `deliveryUrl` are the same link. A result with `status` `needs_human` is not finished until a person opens `url`. When a credential field was removed, `secretsOmitted` names it. User table columns are not removed.

## Organizing data

A workspace is one project, team, or app. Sharing and permissions belong to that workspace. Call `context` first. Reuse a workspace that already matches the project and pass `workspace` with its slug. Writing in the default space is fine when it is the only user space, or when the plan cannot add another space. The default space starts named General; a renamed default keeps its name. When another space is possible, make a dedicated space for the project and pass `spaceId`.

When context.fullAccess is true, create the workspace and the space yourself, unless the workspace is already at its space limit and the only user space is the default: then use that space and do not create another. Otherwise call the workspace or space tool; if it returns an error, call request_access with the capability it names. Only the signed-in user can choose Allow everything on the consent screen. Allow everything is the preselected choice and includes account, member, space, workspace, and billing changes. Regular data handling can read, write, import, undo, and history in one space. It cannot drop tables or wipe undo history. It cannot read another space, and a field its grant leaves out stays denied. It refuses account settings, members, invites, billing, deleting workspaces, and creating a space. Cautious can read that one space. This connection cannot upgrade itself. `request_access` does not create a workspace or a space. It asks for more authority in a workspace this connection can already open. `moreSpaces` asks for a space the connection cannot see. The approving person chooses the space.

A space groups related tables inside a workspace. `CREATE TABLE` with no `spaceId` lands in the default space, which starts named General. Pass `spaceId` (a space id or its short name) to put a table in an existing space, and keep a project's tables together. Writing in that default space is fine when it is the only user space, or when the plan cannot add another. When another space is possible, make a dedicated space for the project.

Name the workspace with a short project name. Table and column names use lowercase letters, numbers, and underscores, start with a letter, and are at most 63 characters. Table and column comments are not stored, so the name says what the data is.

When the only user space is the default space, the `context` message says writing there is fine. When the plan can add a space, it suggests a dedicated space. When full access is at the space limit, the message names that default space and says no new space was created. A `CREATE TABLE` that lands in the default space still commits, and the result includes `hint`.

| Tool | Class | Grant |
| --- | --- | --- |
| `query` | Read | `select` on the tables the SQL touches |
| `execute` | Write. `DELETE`, `TRUNCATE`, and `DROP` are sensitive writes | a write action; `create` for schema changes |
| `context` | Read | the connection’s own grant; no extra capability |
| `history` | Read. Row images are sensitive | You can read and inspect your own changes. Someone else's row images need `recover`. Lists omit read-only queries unless `reads` is true |
| `undo` | Sensitive write | Write on the columns the change touched. You can undo your own changes. Another connection's changes need `recover` |
| `import` | Write. The download is open-world. Rows are not inserted | a write action, and the client is ChatGPT. Other clients do not see it |
| `request_access` | Write of a pending request only. The grant does not change | none |
| `export` | Read. The snapshot is a download | always listed. `export` plus unrestricted workspace read |
| `invite_member` | Sensitive write. Open-world | always listed. `manage_members` |
| `resend_invitation` | Sensitive write. Open-world | always listed. `manage_members` |
| `revoke_invitation` | Sensitive write | always listed. `manage_members` |
| `update_member` | Sensitive write | always listed. `manage_members` |
| `remove_member` | Sensitive write | always listed. `manage_members` |
| `leave_workspace` | Sensitive write | always listed. No management capability. The last owner is refused |
| `create_connection` | Write | always listed. `manage_connections` |
| `revoke_connection` | Sensitive write | always listed. Same authority as revoking a connection on the website |
| `update_connection` | Sensitive write | always listed. `manage_connections` |
| `list_access_requests` | Read | always listed. `inspect_access` or `manage_connections` |
| `resolve_access_request` | Sensitive write | always listed. `manage_connections`. This connection cannot approve its own request |
| `rename_workspace` | Write. Not idempotent | always listed. `manage_workspace` |
| `rename_space` | Write. Not idempotent | always listed. `manage_spaces` |
| `move_table` | Sensitive write | always listed. `manage_spaces` |
| `add_reference` | Write | always listed. Create-table access in both spaces |
| `remove_reference` | Sensitive write | always listed. Create-table access in both spaces |
| `list_references` | Read | always listed |
| `get_billing` | Read | always listed. `manage_billing` |
| `get_usage` | Read | always listed. `inspect_access` |
| `open_checkout` | Write. Open-world. Returns a link the user opens | always listed. `manage_billing` |
| `open_billing_portal` | Write. Open-world. Returns a link the user opens | always listed. `manage_billing` |
| `trim_history` | Sensitive write. Can't be undone | Owner only. Nothing newer than 30 days is deleted. Regular access cannot wipe undo history |
| `get_account` | Read | always listed. `account_profile` |
| `rename_account` | Write. Not idempotent | always listed. `account_profile` |
| `list_invitations` | Read | always listed. `account_workspaces` |
| `accept_invitation` | Write | always listed. `account_workspaces` |
| `decline_invitation` | Sensitive write | always listed. `account_workspaces` |
| `spaces_create` | Write | always listed. `manage_spaces` |
| `spaces_archive` | Write. Idempotent. Rows stay | always listed. `manage_spaces` |
| `spaces_unarchive` | Write. Idempotent | always listed. `manage_spaces` |
| `whoami` | Read | always listed. No extra capability. `id` is stable for this account |
| `workspaces_create` | Write | always listed. `account_workspaces`. Optional `workspace` selects the acting connection. Optional `account` must be the signed-in account |

## Shared limits and errors

A verified credential is limited to 300 requests per minute. An address without that credential is limited to 6000 unauthenticated requests per minute. A refusal is `429 RATE_LIMITED` with `Retry-After`. A full waiter list is `503 QUEUE_FULL`.

SQL runs at most 4 queries per workspace and 2 per account, with 32 waiting per workspace for up to 10 seconds. Past that, the call is `429 QUERY_BUSY`. A statement stops at 30 seconds (`STATEMENT_TIMEOUT`). A free-plan write stops at 2 seconds and a free-plan read at 5 seconds. An `execute` script stops at 60 seconds (`SCRIPT_TIMEOUT`). Free-plan writes are 1 MB (1000000 bytes, not MiB) per minute per workspace, with a 250 KB burst. Paid workspaces are 64 MB per minute with an 8 MB burst. Crossing the write budget is `429 RATE_LIMITED`. A single statement larger than the per-minute allowance plus the burst is refused without `Retry-After`, because waiting cannot refill past a full bucket. A write that runs spends those bytes, including one that commits.

| Code | When |
| --- | --- |
| `UNAUTHORIZED` | The credential is missing, revoked, or expired. |
| `FORBIDDEN` | The grant cannot perform the call. |
| `INVALID_REQUEST` | The input does not match the tool schema. |
| `NOT_FOUND` | The workspace, row, transfer, or event is not visible. |
| `RATE_LIMITED` | A request, write, invite, or export-parse limit refused the call. |
| `QUERY_BUSY` | The workspace SQL queue is full. |
| `PAYLOAD_TOO_LARGE` | The body, file, or normalized export is over its ceiling. |
| `CONFLICT` | `expectRowCount` did not match, or a concurrent edit won. |
| `PLAN_LIMIT_EXCEEDED` | A free-plan row, space, or table ceiling refused the change. |
| `STORAGE_QUOTA_EXCEEDED` | The workspace storage quota refused the write. |

## query

Reads rows with PostgreSQL `SELECT`, including joins, expressions, CTEs, and aggregates, or exports with `COPY ... TO 'adoor://new'`. Nothing in the tables is written. `COPY TO` needs ordinary read permission on the tables in that query, including one table or a filtered subset. It does not need the `export` capability. The whole-workspace snapshot is the `export` tool. It returns a download link.

**Inputs.** `sql` (required), optional `params` (at most 65535 values and 8 MiB), `limit` (1 to 1000), `cursor`, and `workspace`. `atomic` is ignored. `dryRun` is rejected.

**Outputs.** `rows`, `rowCount`, `truncated`, `nextCursor`, and `command`. A `COPY TO` row includes `download_url`. The summary states how many rows were returned or exported.

**Example.**

```json
{ "sql": "SELECT name, company FROM contacts ORDER BY id", "limit": 100 }
```

```json
{ "rows": [{ "name": "Leah Chen", "company": "Elm Studio" }], "rowCount": 1, "truncated": false, "nextCursor": null }
```

**Side effects.** A `SELECT` has none. `COPY TO` stages a download that expires six hours after creation. The staged file is deleted when the download finishes.

**Errors.** `FORBIDDEN` when a named table or field is outside the grant. `STATEMENT_TIMEOUT` at 30 seconds (5 seconds on a free-plan read). `RESULT_LIMIT_EXCEEDED` above 16 MiB. The shared HTTP and SQL limits apply. Every page contains at most 1000 rows. MCP defaults to 100 when `limit` and a constant SQL `LIMIT` are both absent. A constant `LIMIT` of 1000 or less is kept. A page without `ORDER BY` a unique key can repeat or skip rows when other writes commit.

## execute

Runs PostgreSQL against this connection’s own ADoor database. This is not a shell and it does not run code outside that database. MCP rejects `SELECT` and `COPY TO`; `query` reads and exports. HTTP and the CLI accept `SELECT` in `execute`.

Accepted writes are `INSERT`, `UPDATE`, `DELETE`, managed `CREATE TABLE`, `TRUNCATE` or `DROP TABLE` of one user table, `DROP COLUMN`, `RENAME` of a table or field, `ALTER COLUMN TYPE`, `ADD UNIQUE` or `CHECK`, `DROP CONSTRAINT` of a field rule, `SET` or `DROP NOT NULL`, `SET` or `DROP` a literal `DEFAULT`, a plain `CREATE INDEX` or `DROP INDEX`, and `COPY FROM` an uploaded ADoor file. `DELETE`, `TRUNCATE`, and `DROP` overwrite or remove data, so this tool is a sensitive write. The tool stays in `tools/list` when the permission list has no write action. The call then returns an error that says to call `request_access` for insert or update, and create for new tables. Schema statements also need `create`.

**Inputs.** `sql` (required), optional `params`, `idempotencyKey`, `atomic`, `dryRun`, `expectRowCount`, `limit`, `cursor`, `spaceId`, and `workspace`. `spaceId` is a space id or short name this connection can already see. Omit it and `CREATE TABLE` lands in the default space, which starts named General.

**Outputs.** `command`, `rowCount`, and for a script `batch`, `failedAt`, `completed`, and `committedStatements`. `CREATE INDEX` returns the index id. A `CREATE TABLE` in the default space includes `hint`. The table is still created. A rolled-back statement has `status` `rolled_back`, `committed` false, and no event id.

**Example.**

```json
{ "sql": "INSERT INTO contacts (name, company) VALUES ('Leah Chen', 'Elm Studio')" }
```

```json
{ "command": "INSERT", "rowCount": 1 }
```

**Side effects.** The default is one transaction: a failure rolls the script back. `atomic: false` commits each statement on its own and names the statements that already committed. `dryRun: true` runs the transaction and rolls it back, so nothing is committed and no history event is kept. `dryRun` rejects schema changes, `COPY`, `TRUNCATE`, `DROP`, `atomic: false`, and an `idempotencyKey`. The same idempotency key is only for the exact same script. A completed `COPY FROM` deletes the staged bytes. A large write keeps one snapshot and can be undone. A history list of that change may say undo may conflict until undo checks every row.

**Errors.** `STATEMENT_TIMEOUT` (30 seconds; 2 seconds for a free-plan write). `SCRIPT_TIMEOUT` at 60 seconds. A deadlock sets `retryable`. `RULE_VIOLATED_BY_EXISTING_ROWS` names how many rows break a new rule and returns no values. `SCHEMA_IN_USE` when a multi-column rule blocks a drop. `FORBIDDEN` without the write or `create` grant. Write-byte `RATE_LIMITED` and the shared HTTP and SQL limits apply.

## context

Reads the signed-in account email, every workspace this connection can open, with names and slugs, this connection, the current workspace, the spaces and tables it can see, field rules, indexes, permissions, and the expiry clocks. The result says how to switch workspace: pass `workspace` with a slug on any tool. When this connection cannot write, it says to call `request_access` for insert or update. When this workspace is personal, or General is the only user space, the message says to ask the user for a project workspace before creating tables. It does not change data. No extra capability is required. `connections` and `people` are included only when this connection can see them. No secret is included. A rule or index that names a hidden field is omitted. The account is `name` and `email` only.

**Inputs.** Optional `workspace`.

**Outputs.** `account` (`name` and `email`, or null when this connection has no signed-in account), `connection`, `workspace`, `workspaces`, `spaces`, `tables`, `permissions`, `capabilities`, `expiresAt`, `effectiveExpiresAt`, and `expiry`. `workspaces` lists only workspaces this connection can open. `effectiveExpiresAt` is the earliest of this connection’s clock, ancestor connections, the access credential, and a browser sign-in when the call has one. A permission grant has no clock. An OAuth access token lasts 1 hour and is refreshed. A truncated list sets `tablesTruncated`, `spacesTruncated`, `connectionsTruncated`, or `peopleTruncated` and includes the first 1000 items. `omitted` names a field this connection cannot see.

**Example.**

```json
{}
```

```json
{ "account": { "name": "Ada", "email": "ada@example.com" }, "workspace": { "name": "Acme", "slug": "acme" }, "expiresAt": null, "effectiveExpiresAt": "2026-10-20T12:00:00.000Z", "tablesTruncated": false }
```

**Side effects.** None.

**Errors.** `FORBIDDEN` is recorded in `omitted` for a list this connection cannot see, rather than failing the whole call. The shared HTTP limits apply. This tool does not run SQL.

## history

Reads retained changes, newest first. Passing `id` inspects one change, including its row images. You can read and inspect your own changes. Listing does not need `recover`. Someone else's row images, and undo marks for someone else's changes, need `recover`. Row images are sensitive: they contain stored values. They still need unrestricted read of that table. Events this connection cannot see are skipped before the page is filled. A list omits read-only queries. Pass `reads` true to include them. Those query events stay in the usage ledger either way. Inspecting one event by `id` still returns a query event. Pass `table` for that table's current name. The match uses that live table's id, so a dropped table that used the same name stays out. Pass `tableId` to list one table, including one that was dropped. A missing table, a dropped table passed by its old name, and a table this connection cannot see are all `Table not found`. For an event of at most 64 retained rows, `canUndo` is false when a later edit of the same row would be overwritten. It does not read the live row. The comparison uses the columns that were in the event image, by attnum, so a column added later does not by itself reject the undo, including a default that is different on each existing row. A later change to that column still rejects undoing the insert, because that would delete the row. A DROP TABLE event stays in the workspace list, in that space, and under the dropped table's `tableId`, including when the same name exists in another space. When `canUndo` is false because the grant lacks delete, `undoReason` names that permission. A change above the undo limit has `canUndo` false, and `undoReason` repeats `note`. A stored number and the same value stored as text compare as one value, matching undo. A list reads the row count from the usage ledger and does not read every row image. An event with more than 64 retained rows reads one sample image for a schema block. When that sample does not block undo, `canUndo` is `unknown`, because a later edit of another row can still conflict. Undo checks every row. A large write keeps one snapshot and can be undone. A history list of that change may say undo may conflict until undo checks every row. Inspecting one event returns a page of row images.

**Inputs.** Optional `id`, `before`, `limit`, `cursor`, `reads`, `table`, `tableId`, and `workspace`. A list uses `before` and `limit` (1 to 100, default 100). With `id`, `limit` (1 to 100, default 50) and `cursor` page that change's rows. `cursor` is the previous `nextCursor`. `id` cannot be combined with `before`, `table`, or `tableId`. `reads` is ignored when `id` is set.

**Outputs.** A list has `activity`, `rowCount`, `limit`, `truncated`, and `nextCursor`. `truncated` is false only when no further visible event remains. Each event includes `connectionId` and `actorLabel`. `actorLabel` is the connection name, its access level, and the first 8 characters of the connection id. An inspection adds `inspected: true`, `changes` for this page, `changeCount` for every retained row, `limit`, `truncated`, and `nextCursor`.

**Example.**

```json
{ "limit": 20 }
```

```json
{ "activity": [{ "id": "1042" }], "rowCount": 1, "limit": 20, "truncated": false, "nextCursor": null }
```

**Side effects.** None.

**Errors.** `INVALID_REQUEST` when `id` is combined with `before`, `table`, or `tableId`, or when `cursor` is passed without `id`. `NOT_FOUND` with `Table not found` when `table` or `tableId` names a missing table or a table this connection cannot see. `FORBIDDEN` when the event is another connection's and the grant lacks `recover`, or when this connection cannot read the whole table. The shared HTTP limits apply.

## undo

Reverses one retained change, or several when every event was made by this connection. Ownership is the connection. A later grant change does not move earlier events to another connection. The current grant still has to allow the write. This overwrites current values, so it is a sensitive write. You can undo your own changes. That needs write access to the columns that change touched. Undoing an insert requires the delete permission. It does not need read access to the whole table or to every column, and it does not need the `recover` capability. Another connection's changes, a human's changes, and a batch that mixes both still need `recover`. For a caller without `recover`, an event that belongs to another connection looks the same as an event that does not exist. A mixed batch is refused before anything is written and does not name those events. A later edit that would be overwritten is left unchanged and nothing is committed. A column added later is left in place, including a default that is different on each existing row. Undoing an insert still stops when a later change wrote that column, because the row would be deleted. The tool stays in `tools/list` without `recover`. A caller that can only read is told to call `request_access` with that capability.

**Inputs.** `id` (one event id from `history`) or `ids` (more than one). Optional `workspace`. `ids` are reversed newest first.

**Outputs.** The restored change. A conflicting later edit returns without committing.

**Example.**

```json
{ "id": "1042" }
```

**Side effects.** The named change is reversed and a new history event records the undo. Other rows stay as they are.

**Errors.** `FORBIDDEN` without write access to the columns that change touched, or without the delete permission when the change is an insert. A caller that can only read gets that same refusal for another connection's event and for an event that does not exist. The message does not name event ids. `NOT_FOUND` when a caller with write access, or with `recover`, asks for an event that is not available: a missing event, or, without `recover`, another connection's event. History does not list that other connection's event, and undo uses the same message as a missing id. The message does not name event ids. If history did list the event, undo would return `requires recover` instead. `CONFLICT` when a later edit would be overwritten. `UNDO_TOO_LARGE` when a retained change has no snapshot and exceeds 10,000 rows or 80 MiB of images, or when several per-row changes together exceed 1,000 rows or 8 MiB. A single change within that limit is restored in chunks inside one transaction. Changes that are each undoable but too large together say to undo them separately, and each stays undoable. A write above the single-change limit still commits and includes a warning to export first. `UNDO_TIMEOUT` when the restoration runs out of time. Nothing is changed, and the error says it took too long. Write-byte `RATE_LIMITED` and the shared HTTP and SQL limits apply.

## import

Stages bytes from a client file object so `COPY FROM` in `execute` can load them. Rows are not inserted. The file object has `download_url` and `file_id`, and may include `mime_type` and `file_name`.

import accepts ChatGPT files from `files.oaiusercontent.com` only. A Claude, Grok, or Muse URL is refused with `FILE_URL_NOT_ALLOWED` and is not fetched. A Claude or Grok conversation export inside that ChatGPT file is normalized under the same ceilings: a download at or under 32 MiB, normalized JSON at or under 64 MiB, 100,000 conversations, and 1,000,000 messages. A ChatGPT export, CSV, and any other unrecognized file are staged unchanged. The tool is a write because it stores staged bytes, and it is open-world because it downloads that ChatGPT file. It is listed for ChatGPT whether or not the grant includes a write. A missing write returns an error that says to call `request_access`, and the file is not downloaded. Claude, Grok, Cursor, and Muse do not see it. Calling it anyway returns `FILE_URL_NOT_ALLOWED` and names ChatGPT as the supported client. It needs no named capability.

**Inputs.** `file.download_url`, `file.file_id`, optional `file.mime_type`, `file.file_name`, and optional `workspace`.

**Outputs.** `transfer_id`, an `adoor://` URI, `bytes`, and `sha256`. A recognized export also has `format` (`claude` or `grok`), `conversations`, `messages`, `normalized`, and `original_sha256`.

**Example.** A ChatGPT attachment:

```json
{ "file": { "download_url": "https://files.oaiusercontent.com/file", "file_id": "file_abc", "file_name": "contacts.csv" } }
```

```json
{ "transfer_id": "8c6…", "uri": "adoor://8c6…", "bytes": 42, "sha256": "…" }
```

A Claude or Grok file URL returns:

```json
{ "error": { "code": "FILE_URL_NOT_ALLOWED", "message": "import accepts ChatGPT files from files.oaiusercontent.com only. Claude, Grok, and Muse URLs are not accepted and are not downloaded." } }
```

**Side effects.** The bytes are staged for six hours, or until `COPY FROM` finishes or `SELECT adoor_file('close', '<transfer_id>')` deletes them. Table rows are unchanged. Redirects are not followed. The signed URL and the upstream file id are not stored.

**Errors.** `FILE_URL_NOT_ALLOWED` for every host other than `files.oaiusercontent.com`. `FILE_DOWNLOAD_FAILED` when the attachment does not return the file. `PAYLOAD_TOO_LARGE` when the file or the normalized export is over its ceiling. `TRANSFER_QUOTA` when in-flight bytes are at the ceiling. A named export allows one parse in flight and six parse starts per minute per workspace and per account; the next one is `RATE_LIMITED`. The shared HTTP limits apply.

## request_access

Creates a pending request for more authority and returns an approval link. The request grants nothing. Existing permissions and capabilities are kept. Approval can only widen access. No capability is required to call it, and a read-only grant still sees the tool.

**Inputs.** At least one of `permissions`, `capabilities`, or `moreSpaces`. Optional `reason` (at most 500 characters) and `workspace`. Name a space only when this connection can already see it. For a space it cannot see, `moreSpaces` carries the per-space grant and no space id. The approving person chooses the spaces and can narrow the added grant.

**Outputs.** `url` with `kind` `approval`. `approvalUrl` is the same link. `addedPermissions` and `addedCapabilities` name only what this call adds. `permissions` is the full grant approval would apply. A duplicate pending request keeps the previous link and reason.

**Example.**

```json
{ "capabilities": ["recover"], "reason": "Undo a change another connection made." }
```

```json
{ "url": "https://app.adoor.halmoslabs.com/#/w/acme/access/…", "kind": "approval", "status": "pending" }
```

**Side effects.** One pending request is stored. The connection’s grant does not change until a person approves it on the website. `query`, `execute`, `context`, `history`, `undo`, `export`, and `request_access` are always listed. Workspace and space tools, including `spaces_create`, `spaces_archive`, and `spaces_unarchive`, are always listed too. Sharing, organizing, billing, and account changes are separate tools, one per operation. `import` is listed for ChatGPT only. A call without the grant names `request_access`.

**Errors.** `INVALID_REQUEST` when permissions, capabilities, and `moreSpaces` are all absent. `RATE_LIMITED` when this connection already has 3 pending requests; repeating the same request returns its existing link. The shared HTTP limits apply. This tool does not run SQL.

## invite_member

Invites a person to this workspace by email. Needs `manage_members`. This is a sensitive write, and the mail leaves ADoor, so it is open-world. The result has `url` and no invitation token.

**Inputs.** `email`. Optional `role` (`owner`, `editor`, or `viewer`), `permissions`, `capabilities`, and `workspace`. Omitting `role` and `permissions` invites a viewer.

**Example.**

```json
{ "email": "teammate@example.com", "role": "viewer" }
```

```json
{ "url": "https://app.adoor.halmoslabs.com/#/w/acme/credentials/…", "kind": "delivery" }
```

**Side effects.** Creates a pending invitation and sends mail when the sender is allowed. This call does not grant the caller any new access.

**Errors.** `FORBIDDEN` names `request_access` when `manage_members` is missing. `INVITE_RATE_LIMITED` after a burst of 5 invitations, then 1 per minute, 50 per workspace per UTC day (10 for an inviting account younger than 7 days), or a second send to the same mailbox within an hour. A named table that does not exist is `NOT_FOUND`. The invitation link still works when mail is refused.

## resend_invitation

Emails a pending invitation again and replaces its link. The new link expires seven days after this resend. Needs `manage_members`. The previous link stops working. The result says whether the email went out. No invitation token is returned.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_members` is missing. `NOT_FOUND` when the invitation is not pending.

## revoke_invitation

Revokes a pending invitation so its link stops working. Needs `manage_members`. No token is returned.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_members` is missing.

## update_member

Changes a member’s role, permissions, or capabilities, or the grant on a pending invitation. Needs `manage_members`. Pass at least one of `role`, `permissions`, or `capabilities`. A pending invitation keeps its link, and the change sends no email.

**Inputs.** `id`. Optional `role`, `permissions`, `capabilities`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_members` is missing. `NOT_FOUND` for a missing table named by a permission.

## remove_member

Removes a person from this workspace and revokes that identity. Needs `manage_members`. This is a sensitive write.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_members` is missing.

## leave_workspace

Leaves this workspace. No management capability is required. The last owner is refused. This is a sensitive write.

**Inputs.** Optional `workspace`.

**Errors.** `LAST_OWNER` when the last owner tries to leave.

## create_connection

Creates a child connection in this workspace. Needs `manage_connections`. The new secret is not returned. The result has `url` for the signed-in account to open.

**Inputs.** Optional `name`, `role` (`app`, `agent`, `server`, or `sandbox`), `permissions`, `capabilities`, `ttlSeconds`, `expiresAt`, `profileId`, `profileVersion`, `scope` (`workspace` or `account`), `idempotencyKey`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_connections` is missing.

## revoke_connection

Revokes a connection so it stops working. The same authority as revoking a connection on the website applies. No token is returned. This is a sensitive write.

**Inputs.** `id`. Optional `workspace`.

## update_connection

Replaces a child connection’s permissions and optional capabilities. Needs `manage_connections`. `permissions` is required. The change stays within the caller’s authority. This is a sensitive write.

**Inputs.** `id`, `permissions`. Optional `capabilities` and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_connections` is missing.

## list_access_requests

Lists pending access requests in this workspace that this connection can inspect or act on. Needs `inspect_access` or `manage_connections`. Nothing is changed.

**Inputs.** Optional `limit`, `cursor`, and `workspace`.

## resolve_access_request

Approves or denies someone else’s pending access request. Needs `manage_connections`. This connection cannot approve its own request. That case returns the approval link and changes nothing. Optional `permissions` and `capabilities` can only narrow the grant. This is a sensitive write.

**Inputs.** `id` and `decision` (`approve` or `deny`). Optional `permissions`, `capabilities`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_connections` is missing.

## rename_workspace

Renames this workspace or changes its personal or team label. Needs `manage_workspace`. Repeating the same name still writes a workspace event, so this is not idempotent.

**Inputs.** `name`. Optional `kind` (`personal` or `team`) and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_workspace` is missing.

## rename_space

Renames a space this connection can see. Needs `manage_spaces`. The short name stays. Repeating the same name still bumps the revision and writes a space event, so this is not idempotent. A space this connection cannot see is not found, and its name is not returned.

**Inputs.** `id` (space id or short name) and `name`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_spaces` is missing. `NOT_FOUND` when the space is not visible.

## move_table

Moves a table into another space in this workspace. Needs `manage_spaces`. This is a sensitive write. A table or space this connection cannot see is not found, and its name is not returned.

**Inputs.** `table` and `spaceId`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_spaces` is missing. `NOT_FOUND` when the table or space is not visible.

## add_reference

Adds a foreign key between two tables in this workspace. Needs create-table access in both spaces. Pass `createColumn` true to add a nullable source field.

**Inputs.** `table`, `column`, and `targetTable`. Optional `targetColumn`, `createColumn`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when create-table access is missing. `NOT_FOUND` when a table is not visible.

## remove_reference

Removes a foreign-key reference and keeps the field and the rows. Needs create-table access in both spaces. This is a sensitive write.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `NOT_FOUND` when the reference is not visible.

## list_references

Lists foreign-key references this connection can see. Pass `spaceId` to limit the list to one space. Nothing is changed.

**Inputs.** Optional `spaceId` and `workspace`.

## get_billing

Reads the current plan, storage allowance, and whether upgrade checkout is open. Needs `manage_billing`. No card data is returned. Card entry and cancellation stay on the billing page.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_billing` is missing.

## get_usage

Reads usage against the plan caps. Needs `inspect_access`. `period` is `YYYY-MM`. Omit it for the current period. No card data is returned.

**Inputs.** Optional `period` and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `inspect_access` is missing.

## open_checkout

Returns a link the user opens for Stripe Checkout of the paid storage plan when this workspace is allowlisted. Needs `manage_billing`. This call records a checkout claim on the workspace billing account before the link is returned, and clears that claim if Stripe rejects the session. When the workspace has no Stripe customer for the current key mode, this call creates one and stores its id. No card data is returned and this call makes no charge. The ADoor plan does not change until Stripe sends a webhook. This is open-world.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_billing` is missing. `BILLING_NOT_CONFIGURED` when Stripe is not set up. No charge is made in that case. A workspace that is not allowlisted is rejected before Stripe is called.

## open_billing_portal

Returns a link the user opens for the Stripe Customer Portal, for invoices, payment methods, and cancellation. Needs `manage_billing`. When this workspace has no Stripe customer for the current key mode, this call creates one and stores its id before the link is returned. No card data is returned and this call makes no charge. The ADoor plan does not change until a webhook arrives. This is open-world.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `manage_billing` is missing. `BILLING_NOT_CONFIGURED` when Stripe is not set up. A workspace that is not allowlisted and has no Stripe customer or subscription is rejected before Stripe is called.

## tables_deleted

Lists tables the owner deleted in the last 30 days. Each entry has `id`, `name`, `spaceId`, `droppedAt`, `purgeAfter`, and `rowCount`. After 30 days the table is gone. Only the workspace owner can call this.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` when the caller is not the workspace owner.

## tables_purge

Permanently deletes one table from Recently deleted, or all of it. Pass `id` from `tables_deleted`, or `all` set to true, not both. Dropped tables and truncated copies stop counting toward the storage quota immediately. This can't be undone. Restore and undo will not bring the data back. Only the workspace owner can call this. This is a sensitive write.

**Inputs.** `id` or `all`. Optional `workspace`.

**Errors.** `FORBIDDEN` when the caller is not the workspace owner. `NOT_FOUND` when that table is not in Recently deleted, or when Recently deleted is already empty. `INVALID_REQUEST` when both `id` and `all` are sent, or neither is.

## tables_restore

Restores one table from Recently deleted. Pass `id` from `tables_deleted`. Pass `name` when the old name is taken. A name clash returns `TABLE_EXISTS` and changes nothing. Only the workspace owner can call this. This is a sensitive write.

**Inputs.** `id`. Optional `name` and `workspace`.

**Errors.** `NOT_FOUND` when that table is not in Recently deleted. `TABLE_EXISTS` when the name is already in use. `FORBIDDEN` when the caller is not the workspace owner.

## trim_history

Drops retained history images older than 30 days so the history budget can accept new rows. This can't be undone. Omit `table` to drop the oldest images older than 30 days until the workspace fits. Name a table to drop that table’s undo images older than 30 days. Nothing newer than 30 days is deleted. Live rows stay. The result is counts, never row values, plus `retentionDays` and `keptRecentRows`. Only the workspace owner can trim history. Regular access cannot wipe undo history. This is a sensitive write.

**Inputs.** Optional `table` and `workspace`.

**Errors.** `FORBIDDEN` when the caller is not the workspace owner. Regular access gets its own message.

## get_account

Reads this account’s id, display name, and email. Needs `account_profile`. The display name is the only profile field that changes immediately.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `account_profile` is missing.

## rename_account

Changes this account’s display name. Needs `account_profile`. Email is not accepted here. Repeating the same name still updates the account and writes an audit event, so this is not idempotent.

**Inputs.** `name`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `account_profile` is missing.

## list_invitations

Lists pending invitations addressed to this account’s verified email. Needs `account_workspaces`. Each row names that workspace. Invitations for any other email are omitted.

**Inputs.** Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `account_workspaces` is missing.

## accept_invitation

Accepts one invitation addressed to this account’s verified email and joins that workspace. Needs `account_workspaces`. The inviter’s live grant is the ceiling. No invitation token is returned. No email is sent. The membership change stays inside ADoor, so this is not open-world.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `account_workspaces` is missing. `NOT_FOUND` for an invitation that is missing or addressed to someone else, without another workspace’s name.

## decline_invitation

Declines one invitation addressed to this account’s verified email. Needs `account_workspaces`. This is a sensitive write. A missing invitation, or one addressed to someone else, is not found and does not name another workspace.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` when `account_workspaces` is missing. `NOT_FOUND` without another workspace’s name.

## export

Exports the whole workspace and retained history as a download. The tool is always listed. It needs the `export` capability and unrestricted workspace read. HTTP and the CLI return the snapshot inline. MCP returns a download link and does not inline the snapshot.

**Inputs.** Optional `workspace`.

**Outputs.** `url` with `kind` `download`, plus `download_url`, `byte_count`, `sha256`, and `expires_at`. The snapshot is the file behind that link.

**Side effects.** Stages one export file. It expires on the same clock as other downloads. One table or a filtered query still uses `COPY ... TO 'adoor://new'` in `query`, which needs ordinary read permission and does not need `export`.

**Errors.** `FORBIDDEN` without `export` or without unrestricted workspace read. The message names `request_access` when the capability is missing. `RESULT_LIMIT_EXCEEDED` above 16 MiB.

## spaces_create

Creates a space in a workspace this connection can open. Needs `manage_spaces`. The default space starts named General. Creating a space records read and write on that new space id only.

**Inputs.** Optional `name`, `slug`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` with `manage_spaces`.

## spaces_archive

Archives a space. The space, its tables, and its rows stay. An archived space does not count toward the space plan limit. The default space and the ADoor system space cannot be archived. Needs `manage_spaces`. Nothing is deleted. Repeating the call on an archived space has no further effect.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` with `manage_spaces`.

## spaces_unarchive

Restores an archived space. It counts toward the space plan limit again. Restoring a space past that limit is rejected and nothing changes. Needs `manage_spaces`. Repeating the call on a space that is already active has no further effect.

**Inputs.** `id`. Optional `workspace`.

**Errors.** `FORBIDDEN` names `request_access` with `manage_spaces`.

## whoami

Read-only profile for this connection. It takes no arguments. `id` is a stable account id. It does not change when the connection is refreshed, reconnected, or its access changes, and it is not an email address. `name`, `email`, and `nickname` are included when this account has them. `nickname` is the account name and the workspace name.

**Inputs.** None.

**Errors.** `UNAUTHORIZED` when this connection has no signed-in account.

## workspaces_create

Creates a personal or team workspace owned by the signed-in account. Needs `account_workspaces`. Optional `workspace` selects the connection acting in that workspace. The new workspace is not created inside it. Optional `account` must be that signed-in account. A different account creates nothing. There is no organization.

**Inputs.** `name`. Optional `kind`, `account`, and `workspace`.

**Errors.** `FORBIDDEN` names `request_access` with `account_workspaces`.

## Credentials

`create_connection` and `invite_member` return a website link. The tool result does not contain the new secret. Links include the workspace short name, for example `#/w/acme/access/<id>`. App consent stays on `/oauth/consent/<id>`. Trusting an issuer and approving an app stay on the website. Approving this connection’s own access request returns that same approval link. A holder of `manage_connections` can approve someone else’s request with `resolve_access_request`.

Ordinary MCP calls use the live grant and do not keep a session. Live change notifications use `https://app.adoor.halmoslabs.com/mcp/live`.
