> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cfo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP tools and call shapes

> Reference for agents and developers: the cfo.ai MCP server's address, sign-in, and limits; every published tool and its actions; the one-action-block call shape and its refusals; workspace selection; results; pagination and truncation; and what is not published.

The cfo.ai MCP server publishes 36 tools: Ari's public product tools plus three MCP-only tools, `list_orgs`, `inspect_chats`, and `edit_chats`. This page describes the server, each tool, and the conventions every call follows. To connect a client, follow the [Agent instructions](/agent-instructions.md).

## Server

| Fact | Value |
| - | - |
| Address | `https://api.cfo.ai/mcp`. `https://api.cfo.ai/mcp/v2` serves the same tools; use `/mcp` for new connections. |
| Transport | Streamable HTTP, stateless. Each request authenticates and selects its workspace on its own, and the server neither issues nor requires an `Mcp-Session-Id`. |
| Sign-in | OAuth. A person approves the client in a browser, and the client sends the token it receives in the `Authorization` header. The protected resource metadata is at `https://api.cfo.ai/.well-known/oauth-protected-resource/mcp`, and clients register themselves through dynamic client registration or a client ID metadata document. |
| API keys | None. cfo.ai issues no API keys or access tokens to copy; every client signs in as a person and acts as that person. |
| Instructions | The server sends cfo.ai's modeling instructions when a client connects. |
| Tool list | Fixed for each server process. The tool list carries a five-minute private cache hint, and the server advertises `listChanged: false`. When a client's definitions are stale, refresh its tool list or start a new conversation. |
| Request size | A request body holds at most 1 MiB. A larger one is refused with **The MCP request is too large.** |
| Batches | JSON-RPC batch requests are refused with **JSON-RPC batches are not supported.** |
| Rate limit | 60 calls a minute for each tool, per user and workspace; `list_orgs` has no limit. A call past the limit fails with **rate limit exceeded for** followed by the tool name. |

## Published tools

A tool marked read-only never changes saved state. The **Actions or fields** column lists a multi-action tool's blocks; for a single-purpose tool it lists the fields it takes directly.

### Tables, formulas, and mappings

| Tool | What it does | Actions or fields |
| - | - | - |
| `inspect_table_blocks` | Read-only. Lists saved tables, reads cells with their formulas and sources, compares a table with an earlier saved state, and reads Variables, Dimensions, and their items. A `list` reads one Scenario; without `target` or `page` it also carries `other_scenarios`, each other Scenario the user can read that holds saved tables the listed Scenario does not, with `tables_not_in_this_scenario` counting them, or `[]` when none does. | `list`, `read`, `compare`, `variable`, `dimension`, `items`, `bounds` |
| `edit_table_blocks` | Creates, updates, reshapes, copies, or deletes saved tables in one atomic change each. Writes every model formula, and sets time rollups and the Scenario's calendar. | `create`, `update`, `reshape`, `copy`, `delete`, `set_time_rollup`, `update_calendar` |
| `inspect_dimension_mappings` | Read-only. Lists the mappings a reporting Dimension owns, or reads one lookup chosen by its exact source keys. | `target`, `keys`, `limit`, `after`, `scenario`, `as_of_point` |
| `edit_dimension_mappings` | Maps items of source Dimensions to a reporting Dimension's items, sets or clears the catch-all, removes rules or lookups, and combines or splits compatible source Dimensions. | `map`, `unmap`, `catch_all`, `clear_catch_all`, `drop_mapping`, `combine_dimension`, `split_dimension` |

### Pages and Canvas blocks

| Tool | What it does | Actions or fields |
| - | - | - |
| `inspect_pages` | Read-only. Reads the page tree, each Page's blocks in order, saved table settings, and reusable references. Each Page in a `list` or `read` carries `created_by`, from its first saved change: the workspace member's `display_name` and `kind`, `is_current_actor`, and, when Ari made the Page, `ari_chat` as `this_chat` (this conversation, its task steps and background imports included) or `another_chat`. `kind` is `unknown` when the record names no member, and a Page made before change logs recorded the chat has no `ari_chat`. | `list`, `read` |
| `edit_pages` | Creates, renames, moves, shares, and deletes Pages; sets page icons and date ranges; adds and edits Text blocks; reorders and removes blocks; creates share links. | `create`, `update`, `move`, `share`, `create_share_link`, `delete`, `add_text`, `update_text`, `delete_block`, `reorder_blocks` |
| `inspect_canvas_blocks` | Read-only. Reads selected Canvas blocks: saved source, dataset layouts and Scenario bindings, display state, position, revision, and references. | `target`, `page`, `scenario`, `as_of_point`, `source_offset` |
| `edit_canvas_blocks` | Creates a Canvas block on a Page, or changes one. | `create`, `update` |
| `inspect_canvas_library` | Read-only. Finds Canvas components and templates, and reads their instructions and props. | `list`, `read` |
| `validate_code_blocks` | Read-only. Renders up to 8 saved Canvas blocks in a browser and reports errors, warnings, what each chart plotted, guide anchors, and, on request, a screenshot. | `target`, `scenario`, `visual_review` |

[Canvas blocks for agents](/agents/canvas-blocks.md) covers these tools in detail.

### Scenarios and history

| Tool | What it does | Actions or fields |
| - | - | - |
| `inspect_scenarios` | Read-only. Reads Scenarios, their owners, Snapshots, and descendant trees. | `list`, `read`, `subtree` |
| `edit_scenarios` | Creates, renames, snapshots, merges, or deletes Scenarios. | `create`, `rename`, `snapshot`, `merge`, `delete` |
| `inspect_history` | Read-only. Reads the changelog, the net difference between two points in time, and what a Scenario would merge into its parent. | `changelog`, `changes_between_points`, `scenario_summary`, `read` |
| `edit_history` | Undoes or redoes changes, or reverts a Scenario to a past changelog entry. Each operation returns its new changelog entry and the action that reverses it. | `undo`, `redo`, `revert` |

### Connected data

| Tool | What it does | Actions or fields |
| - | - | - |
| `inspect_integrations` | Read-only. Reads saved connections with their readiness and safe next action, the integration catalog, permitted API endpoints, connection-link progress, ingestion status, sync setups, saved API and MCP settings (admins only), saved queries, and ingestion jobs. `query_results` runs one saved query and returns its rows without adding data to the Model; it runs in Snowflake for every connection, a file upload included, which is where the query's loads run. While a sync is under way, a read of a whole connection counts the rows its saved queries have already published in the model under `materialized` and `materialized_rows`; a read of one run reports only that run. `outcomes` also lists the user's own recent sign-ins to this MCP server. `sync_setups` for an MCP server carries `sync_page`, which states how one sync page calls the server and its 45-second allowance. | `catalog`, `connections`, `outcomes`, `endpoints`, `connect_progress`, `ingestion`, `sync_setups`, `configurations`, `queries`, `query_results`, `jobs`, `query_deletion_preview`, `read` |
| `edit_integrations` | Opens connection setup for the user, connects, re-links, and deletes connections and saved queries, including saved queries whose connection was deleted, edits saved API and MCP settings, saves queries and sets their column roles, applies query templates, starts and cancels query and raw-sync jobs, starts ingestion, ingests a CSV, saves API and MCP sync setups, chooses synced tables, and sets daily refresh times. | `request`, `connect`, `relink`, `update`, `delete`, `delete_query`, `apply_query_templates`, `save_query`, `set_query_column_role`, `start_query`, `start_raw_sync`, `cancel_job`, `start_ingestion`, `ingest_csv`, `save_setup`, `delete_setup`, `select_source_tables`, `set_refresh_time`, `clear_refresh_time` |
| `call_connected_api` | Calls one endpoint of a connected third-party API. The server supplies the credentials and enforces permissions. A JSON response larger than one result comes back with its largest list cut to the first items that fit and every other field whole; `message` states the list's path, how many of its items were kept, that a next-page cursor in the body follows every item including those left out, the value of the last kept item's `id` key when that key holds a string or a number, and the endpoint's integer query parameters with the values the request set. The request ran once, so a read is safe to repeat with the endpoint's own pagination or filter parameters. | `connection`, `endpoint`, `grantId`, `pathParams`, `query`, `body` |

### Workspace, people, and automation

| Tool | What it does | Actions or fields |
| - | - | - |
| `list_orgs` | Read-only, MCP only. Lists the user's workspaces with each one's ID, name, slug, and role. | none |
| `inspect_skills` | Read-only. Lists the built-in Skills and the signed-in user's own Skills that apply in the selected workspace, and reads their instructions and supporting files. Each of the user's Skills says where it applies, `all` or `this`. Reading a skill does not run it. | `list`, `instructions`, `file`, `read` |
| `edit_skills` | Creates, updates, or deletes the signed-in user's own Skills. `workspaces` sets where a Skill applies: `all`, the default, for every workspace where the user is a member, or `this` for the selected workspace only. Built-in skills cannot be edited. | `create`, `update`, `delete`, `workspaces` |
| `inspect_triggers` | Read-only. Reads the user's own scheduled Triggers, their schedules, and their last runs. | `query` |
| `edit_triggers` | Creates, updates, pauses, resumes, runs, or deletes the user's own Triggers. | `create`, `update`, `run`, `delete` |
| `inspect_guides` | Read-only. Reads the workspace's saved Guides. | `query`, `include_content` |
| `edit_guides` | Creates, replaces, or deletes saved Guides. | `create`, `update`, `delete` |
| `inspect_memory` | Read-only. Searches saved memories: the user's private facts and the workspace's shared facts. Each memory carries the `context` saved with it and `saved_with`, the `edit_memory` action that saved it: `retain` for a fact, `retain_document` for a statement extracted from a document, or `retain and retain_document` for a memory consolidated from both; a memory saved before the action was recorded has no `saved_with`. When the memory service cannot answer, a search with no results returns a refusal with code `memory_service_unavailable`, one plain sentence naming the fault, and `transient`, which says whether waiting helps; a billing limit on cfo.ai's provider account is `transient: false`. | `query`, `scope`, `tags`, `batch`, `include_superseded`, `limit` |
| `edit_memory` | Saves 1 to 12 facts, saves a document, replaces facts, or forgets them. `retain` and `retain_document` take `context`: why the memory was saved, and whose words or request it came from. The memory service reads it while extracting the memory, and recall shows it beside each one. A document's statements are recalled one at a time, apart from the rest of the document, with the document's context. A fact's `scope` is `user` (private, the default) or `organization` (shared with the workspace); a fact that `supersedes_memory_id` replaces keeps the older fact's scope, and `forget` names the scope of the memory it removes. | `retain`, `retain_document`, `forget` |
| `inspect_team` | Read-only. Reads the people and pending invitations in the workspace. | `query`, `target` |
| `edit_team` | Invites admins, promotes members, removes members, or cancels invitations. Every change needs the user's explicit confirmation. | `invite`, `update_member`, `remove_member`, `cancel_invite` |
| `edit_org` | Renames the workspace, or sets, clears, or copies its brand: the name, colors, heading typeface or font, corner shape, sidebar colors, logo, and whether link previews show it. Colors are saved as given; a brand with hard-to-read color pairs is refused, naming each pair. | `rename`, `brand` |
| `inspect_billing` | Read-only. Reads the plan, AI usage, spending settings, integration allowances, and available plans. It cannot change billing. A workspace an operator exempted from billing answers with `ok: false` and `error_kind: not_found`: **this workspace is exempt from billing: there is no plan to choose or pay for, AI usage is not metered, and connections have no plan limits**. | `read` |
| `inspect_profile` | Read-only. Reads the user's profile, personal theme, and the workspace brand, with `heading_font_url` beside the logo addresses when the brand has its own font. | none |
| `edit_profile` | Updates the user's display name, personal theme, or both. Theme colors are saved as given, as hex or `oklch(L C H)`; a theme with hard-to-read color pairs is refused, naming each pair. Sidebar colors come as a pair, `navSurface` and `navInk`. | `name`, `personal_theme` |
| `notify_user` | Emails the user at their verified address or sends them a Slack DM. Email to anyone else waits as a preview until the user presses Send. | `email`, `slack` |
| `inspect_documentation` | Read-only. Searches the cfo.ai documentation and reads whole pages. | `search`, `read` |

### Ari chats

Two MCP-only tools let a client hand a question to Ari instead of calling tools itself. A chat a client starts is an ordinary Ari chat, with the same agent, billing, memory, and Skills, and the person can open it in the cfo.ai web app, where its entry in the chat list carries an **MCP chat** mark.

| Tool | What it does | Actions or fields |
| - | - | - |
| `inspect_chats` | Read-only, MCP only. `list` returns the user's chats in this workspace, most recently updated first, each with its `ref`, `state` (`running`, `idle`, `needs_response`, or `error`), a busy flag, and its `resource_uri`. `read` returns one bounded page of a chat: the user's messages, Ari's last message in each turn, the questions and plans Ari waits on, cards that only the web app can answer as `waiting_in_app` items, and delivered files. | `list` (`status`, `title_query`, `limit`, `cursor`), `read` (`target`, `after`, `before`, `wait_seconds`, `steps`) |
| `edit_chats` | MCP only. `create` starts a chat with a message in the Scenario `_context.layerId` names. `send` adds a message to a chat, and also answers Ari's questions and approves a plan. `stop` ends the running turn; `drop_waiting` also deletes messages not yet sent. `update` renames, archives, or restores a chat. Turns bill the workspace like chats in the app. | `create` (`message`), `send` (`target`, `message`, `delivery`, `request_id`), `stop` (`target`, `drop_waiting`), `update` (`target`, `title`, `archived`) |

* **Reads are bounded.** `steps` chooses how Ari's tool work shows. `none`, the default, folds each turn's tool steps and the notes between them into one `steps` item with their count. `summary` shows one item per step. `full` adds each step's input and result, cut at 4,000 characters. A page holds at most 40 items and about 48 KB. A read without a cursor keeps the newest items and returns `older` to page back; a read with `after` keeps the oldest and returns `next`.
* **Waiting for Ari.** `create` and `send` return at once with `next`: the exact `inspect_chats` `read` call that waits, up to 50 seconds through `wait_seconds`, while the chat is busy. A read of a chat that is still busy returns another `next`. A client that sends a progress token gets a progress notification each second it waits, naming what Ari is doing. To hear when a turn starts or ends instead of waiting, subscribe to the chat's `resource_uri`, `cfoai://orgs/{org_id}/chats/{chat_id}`, on the MCP 2026-07-28 `subscriptions/listen` stream, and read the chat's newest page with `resources/read`. The older `resources/subscribe` is refused.
* **While Ari works.** `delivery` on `send` is `steer` by default, which ends the running turn after its current step and answers the message next; `queue` waits for the turn to end. `request_id`, a UUID the client makes, makes a repeated `send` a `no_op` that returns the same `message_id`.
* **Limits.** A `create`, or a `send` to a chat that is not busy, is refused when 5 of the user's chats in the workspace are already busy. Only the chat's owner can read or write it. Approval buttons, email **Send** buttons, confirmations, and sign-ins work only in the web app: give the person the `chat_url` the result carries. An MCP read does not mark the chat read in the web app.

## Fields every tool accepts

Every tool except `list_orgs` also accepts these two fields at the root. The server removes them before the tool runs.

| Field | Meaning |
| - | - |
| `org_id` | The workspace, by ID or slug. Optional when the user belongs to one workspace. See [Choose the workspace](#choose-the-workspace). |
| `_context` | The in-app editing context, all optional: `layerId` (the active Scenario's ID), `pageId` (the active Page's ID), `timeZone` (such as `America/New_York`), and `utcOffset` (such as `-04:00`). Send `timeZone` or `utcOffset` when relative dates matter. Any other key is refused with **\_context must contain only layerId, pageId, timeZone, and utcOffset**. |

### The Scenario a call uses

Most model tools take `scenario`, by name or returned reference. `"Main"` names the base Scenario whatever name the workspace stores for it, and the id that Page and block links print after `?layer=` is accepted too; a stored name wins, so a Scenario literally named Main keeps its own meaning. When a call omits `scenario`, the tool uses the Scenario in view: the one `_context.layerId` names, or Main when `_context` names none. Pass `scenario` on every write so the target does not depend on context. [Create and update formulas](/agents/write-formulas.md#write-in-a-scenario) quotes the sentence a table write adds when `scenario` was omitted.

## Shape each call as one action block

A tool with several actions takes exactly one block named for the action, with that action's fields inside it. There is no flat `action` field.

```json theme={null}
{ "read": { "target": "Operating Plan", "rows": ["Revenue"] }, "scenario": "Main" }
```

* Each tool's input schema says, in its description, which blocks exist and which fields sit beside them: "Fill exactly one action block: list, read. Fields beside the block apply to it: as\_of\_point, scenario."
* An empty block selects an action that needs no fields, such as `{"list": {}}`. A block set to `null` counts as not filled.
* Fields beside the block apply to it: `scenario` on most model tools, `as_of_point` on `inspect_pages` and `inspect_scenarios`, `validate` on `edit_canvas_blocks`, `target` on `edit_dimension_mappings`, and `idempotency_key` on `edit_memory`. A block that takes such a field also accepts it inside; when both places carry it, the two values must match.
* Single-purpose tools take their fields directly at the root, as the table above shows. `notify_user` takes an `email` block, a `slack` block, or both.
* The schema root is a plain object, so every client can see the blocks. The rule that exactly one block is filled is enforced when the call runs.
* Unknown fields and retired request shapes are refused before anything runs. Nothing is translated from an older shape.

### Refusals for a wrong shape

A refused call changes nothing. Its message names the problem without repeating any value you sent.

| Mistake | Message |
| - | - |
| No block filled | `no action block is filled; the call takes exactly one of [...]` |
| Two blocks filled | `the call takes exactly one action block; [...] are filled` |
| A flat `action` field | `action "read" is the read block; the fields of that action belong inside it: {"read": {"target": ...}}` |
| `action` inside a block | `the read block takes no action field; the block name selects the action` |
| A block that is not an object | `read must be an object holding that action's fields` |
| A root field that is not a block | `<name> is not an action block; the blocks are [...]` |
| A field the block does not take | `<field> is not a field of the <block> block` |
| A beside-field inside a block that does not take it | `<field> belongs beside the <block> block, not inside it` |
| The same field beside and inside, with different values | `<field> is <value> beside the <block> block and <value> inside it; a call carries one <field>, in either place` |
| A value of the wrong type or shape | `<path> received <what>; it accepts <what>`, such as `/sections/0 received a string of 9 characters; it accepts an object whose section is one of …`. At an action's root, the path reads `the input`. |

A shape is described by type: a string by its length, an array by its item count, an object by up to 8 of its key names, and a number, boolean, or null by its type.

The refusal arrives with `isError: true` and this structure:

```json theme={null}
{
  "error": {
    "code": "invalid_input",
    "message": "the call takes exactly one action block; [\"list\" \"read\"] are filled",
    "transient": false,
    "validation": {
      "issues": [{ "path": "", "code": "invalid_value", "message": "..." }],
      "accepted_fields": ["..."],
      "required_fields": ["..."],
      "truncated": false
    }
  },
  "state_changed": "no"
}
```

`validation` lists at most 20 issues; `truncated` is true when there were more. `accepted_fields` lists the chosen block's own fields plus the fields beside it.

## Choose the workspace

`list_orgs` takes no input and returns the workspaces the user can use through MCP:

```json theme={null}
{ "orgs": [{ "org_id": "4127", "name": "Example Co", "slug": "example-co", "role": "admin" }] }
```

Pass a returned ID or slug as `org_id` on later calls. A number, or a string of digits, always names a workspace ID; anything else is read as a slug.

| Situation | Answer |
| - | - |
| `org_id` omitted, one workspace | That workspace is used. |
| `org_id` omitted, several workspaces | `org_id is required: you belong to <n> cfo.ai workspaces — pass one of [<ID> (<name>), …] (or call list_orgs)` |
| `org_id` omitted, no workspace | `no org_id supplied and you are not a member of any cfo.ai workspace` |
| A workspace the user is not a member of, or a slug that matches none | `you do not have access to this workspace`. Unknown and inaccessible workspaces get the same answer. |
| A numeric `org_id` that is not a positive whole ID | `a numeric org_id must be a positive whole cfo.ai workspace ID; pass slugs as strings`, or for zero `org_id must be a positive numeric cfo.ai workspace ID` |
| `org_id` neither a string nor a number | `org_id must be a string or number` |
| A workspace that has not started its free trial or a plan | `Start the free trial in cfo.ai before using its MCP tools`. `list_orgs` leaves such workspaces out, and answers with this message when none remain. |
| A deactivated workspace | `This workspace has been deactivated. Contact help@cfo.ai for assistance.` |
| A workspace paused while cfo.ai investigates an issue | `This workspace is temporarily unavailable while we investigate an issue. We'll report back soon.` |

## References and links

* Selectors accept readable names, returned references, or IDs, unless a field says it takes less. Copy references from inspection results instead of building IDs.
* A returned reference has the form `Name#hex`, such as `Revenue#a1b`. The suffix is a prefix of the object's identity, at least three hex digits, and unique among objects of its kind when the result was written. Later creates can lengthen it; an older reference keeps resolving while it stays unique, and a fresh inspection carries the current form. A reference also keeps resolving after its object is renamed, when exactly one object of its kind carries the suffix and the written name is one that object carried: `Gusto#940` names the connection now called `Acme Payroll#940`, and `Completion and final validation#6ca` names a text block after an `update_text` changed its first line, with or without a page named. A suffix with a name the object never carried is refused as not a reference, with the object's current reference. Scenario and Page references follow the same rule: `Plan#725` names the Scenario since renamed `Plan - No Series A#725`, and `SA26-18a#50c` names a Page after `edit_pages` `update` renamed it, in `edit_pages`, `inspect_pages`, and the `page` a table `create` names. `Main` with the base Scenario's suffix names the base Scenario.
* A name that is ambiguous or matches nothing is refused with `error.candidates` when alternatives are known: each with `display_name`, `ref` to pass back, `kind`, and, when ranked, `score` and `match`.
* A result names each object with `ref`, the selector to pass back, and `url`, the address that opens it in cfo.ai.
* A write also returns a resource link for each thing it created or changed, described "Open in cfo.ai" and meant for the user. End your reply with the link a tool returned, exactly as printed; a link you assemble yourself will not open.

## Results

Every result is JSON text. A result that is a JSON object also arrives as `structuredContent`. A `validate_code_blocks` call with `visual_review` also returns the render as image content.

### Write receipts

A write receipt opens with a shared head. Tool-specific sections sit beside it.

| Field | Values |
| - | - |
| `status` | `applied`: everything asked for is saved. `partially_applied`: some items are saved; `items` says which. `rejected`: nothing is saved because the request or every item was refused. `no_op`: nothing is saved because saved state already matched. `blocked`: nothing is saved because a dependency or a rule refused the write. |
| `state_changed` | `yes`, `no`, or `unknown` |
| `counts` | `requested`, and when nonzero `created`, `reused`, `applied`, `skipped`, `rejected`, `unchanged`, and `unknown` |
| `items[]` | One entry per requested item, in request order: `ref`, `display_name`, `status` (`created`, `reused`, `applied`, `skipped`, `rejected`, `unchanged`, or `unknown`), and `reason` when it did not land |
| `readback` | `status`: `complete`, `partial`, `unavailable`, or `not_needed`, with `reason`. A write whose readback is unavailable still saved. |
| `warnings[]` | `code`, `message`, and `ref`: things the saved write will not do as asked |
| `error` | Present exactly when `status` is `rejected` or `blocked` |

An item whose status is `unknown` may or may not have landed. Read current state before you repeat it.

### Refusals and failures

* A refusal carries `error` with `code`, `message`, `transient`, and, when known, `candidates` and `hint`, beside `state_changed`. Codes include `ambiguous`, `not_found`, `invalid`, `blocked`, `conflict`, `stale_target`, `selection_too_wide`, `file_too_large`, `result_unavailable`, and `browser_validation_unavailable`.
* `result_unavailable` reads **The call's outcome is unavailable. Read current state before repeating a write; repeating it could apply it again.**
* A `not_found` for a name or `Name#hex` reference that only a removed connection or a deleted table carried says so, with the day in UTC: `"Stripe#aa9" named a connection that was removed from this workspace on 2026-10-05.` or `"Vendor breakdown" named a table that was deleted on 2026-08-12.` The table fact appears only when you could read that table just before it was deleted. A Scenario reference whose suffix belongs to a removed Scenario says `"Disposable#a17" named a scenario that was deleted on 2026-10-19.` or `merged into its parent scenario`, only for a user who could see that Scenario just before it was removed. Any `candidates` are objects that exist now, and the `hint` says each is a different object.
* A result with `ok: false` and an `error_kind` is an answer, not a crash. Use its message and `hint` to choose the next call.
* Refusals and fully rejected writes arrive with `isError: true`. A write that saved some items stays a success, with each item's status.
* `transient: true` means the same call can succeed later without changes. A failure that knows whether it is retryable adds `Transient: true.` or `Transient: false.` to its text and `{"error": {"message", "transient"}}` to its structured content. The server never retries for you.

## Pagination and truncation

A list cut short carries `truncation` and, when it can continue, `next`:

* `truncation.by` says what cut it: `response_budget` (the next item did not fit one result), `count_limit` (the page reached the `limit` you set), `row_limit`, or `depth`.
* `truncation.shown` counts what came back, and `truncation.total`, when counted, what the selection matched. No `truncation` means the result is complete.
* `next` holds the values that continue the read. Pass a cursor back unchanged, with the same tool, action, target, filters, and snapshot.

| Tool and action | Page size | Continue with |
| - | - | - |
| `inspect_pages` `list` | `limit`. Without it, every page summary that fits one result; `truncation.total` counts all pages. | `next.after` as `after`, with the same `scenario` and `target` |
| `inspect_pages` `read` | A page whose block list was cut carries its own `truncation`. | — |
| `inspect_scenarios` `list` | `limit`. Without it, every visible Scenario that fits one result. | `next.after` as `after` |
| `inspect_history` `changelog` | At most 100 entries per read; `limit` takes up to 100. Without it, as many of those as fit one result. | `next.before` as `before` |
| `inspect_history` `changelog` with `search` | Each read searches up to 1,000 entries, newest first, and reports how many it read in `entries_searched`. | The `before` value the result carries |
| `inspect_table_blocks` `list` | `limit` up to 500. Without it, every table that fits one result. | `next.after` as `after` |
| `inspect_table_blocks` `read` and `compare` | `limit` up to 500 grid rows; default 500. Parents and descendants count separately. | `next.after` as `after`; it binds the table, selection, Scenario, and snapshot |
| `inspect_table_blocks` `items` | `limit` up to 500 items; default 500. | The last item's text as `after`, including an empty string |
| `inspect_table_blocks` `variable` and `dimension` | `limit` up to 100 saved formula definitions per page; default 100. | `next.after` as `after`, with the same `target` |
| `inspect_dimension_mappings` | `limit` rows per lookup. Without it, every row of each lookup, or as many per lookup as fit one result. Each lookup's `row_count` is its true total. | That lookup's `next` as `after`, with the same `keys` |
| `inspect_canvas_blocks` | A selection larger than one result is cut short. See [Canvas blocks for agents](/agents/canvas-blocks.md#read-saved-canvas-blocks). | `next` holds the arguments that read the rest |
| `inspect_integrations` `query_results` | `limit` up to 100 rows per read; without it, 100. The result holds as many of them as fit one result, with `offset` naming the first row shown. `total_rows` counts every row on the first read only. | `next.offset` as `offset`, with the same `query`. Rows keep one order across offsets only when the saved statement's outermost `SELECT` ends with `ORDER BY`; otherwise `order` says that pages can repeat or skip rows. |

A table `read` that selects rows, columns, or periods, limits `depth`, continues from `after`, or stops early returns `partial: true` at its root. `coverage.delivery` says whether the response reached the end of that selection, and `coverage.calculation` whether its values finished calculating. A `period_range` that reaches past the table's window returns only the periods inside the window and adds one `read_note` issue that counts and lists the requested periods left out and names the window: `13 of the 20 requested periods are not in this read (2025-02 through 2026-01, 2026-09): they are outside the table's window, 2026-02 through 2026-08.` A table that saves no valid dates names the default window instead. The note appears on a table with one Date axis at month, quarter, or year grain.

A changelog entry larger than one page keeps its place in the list. Without `include_details`, it comes back with an empty `changes` list and no `blocks` list, and `changes_truncation` and `blocks_truncation` beside them count what each list held. With `include_details`, it keeps every change, and a result larger than the allowance is replaced whole by the allowance marker.

## Read the Model at an earlier point

`inspect_pages`, `inspect_table_blocks` (every action except `bounds`), `inspect_canvas_blocks`, `inspect_dimension_mappings`, and `inspect_scenarios` take `as_of_point`. Without it they read the current state. It holds exactly one point type:

| Point type | Meaning |
| - | - |
| `change_log_id` | One of the workspace's changes, as a write receipt or `inspect_history` reports it. The snapshot is the Model right after that change. Change ids are numbered across all workspaces, so an id between two of this workspace's changes can belong to another workspace. It accepts the ID as an integer or as a string of digits, as do `before_change_log_id` on `inspect_table_blocks` `compare`, `at_change_log_id` on `edit_scenarios` `snapshot`, and `change_log_ids` and `to_change_log_id` on `edit_history`. Results print the ID in either form. |
| `timestamp` | An exact instant in RFC 3339 with a time zone, supplied by the user, the context, or a tool result. |
| `calendar_boundary` | `unit` (`day`, `week`, `month`, `quarter`, or `year`), `offset` (0 this period, -1 the previous, 1 the next), and `boundary` (`start` or `end`), in the user's time zone from `_context`, or UTC without one. Weeks start on Monday. Use it for "yesterday" or "last month". |
| `relative_duration` | `amount`, `unit` (`minute`, `hour`, `day`, or `week`), and `direction` (`ago` or `from_now`), from server time. A day is 24 hours and a week seven days. |

A `change_log_id` the workspace does not have is refused with `not_found`: `as_of_point.change_log_id: no change with id <n> exists in this workspace. Change ids are numbered across all workspaces, so an id between two of this workspace's changes can belong to another workspace.`, with the hint `This workspace's changes are the ids its write receipts and inspect_history report.` and, when one exists, `Its latest change before <n> is <m>.`

`inspect_history` `changes_between_points` takes a `start` point and an optional `end` point, which may also be `latest: true`. A `change_log_id` there resolves to the workspace's latest change at or before it.

## Safety annotations

Each tool carries standard MCP annotations, which clients use to decide which calls need the user's approval:

* `readOnlyHint: true` on every `inspect_*` tool, `inspect_chats` included, `validate_code_blocks`, and `list_orgs`.
* `openWorldHint: true` on tools that can reach outside cfo.ai, notify a person, send an invitation, or change an outside integration: `edit_pages`, `edit_triggers`, `edit_profile`, `edit_integrations`, `edit_team`, `edit_org`, `call_connected_api`, `notify_user`, and `validate_code_blocks`.
* `destructiveHint: false` only on `notify_user`. Every other write keeps the MCP default, which treats it as possibly destructive.

## What the server does not publish

Ari has tools that an outside agent does not get, because they need the cfo.ai app, belong to cfo.ai's own agents, or are switched on per workspace by a release setting that the server's single tool list cannot follow.

| Kind | Not published | Instead |
| - | - | - |
| Cards and views shown in the cfo.ai chat | Asking the user questions in a card, playing a Guide, showing a Scenario or table rows on screen, scenario confirmation cards, and sign-up steps | Ask the user in your own conversation, and describe what to open. |
| cfo.ai's own agents | Delegating to background agents, task tools, the ingestion worker's reads of raw source data, and reports to cfo.ai engineering | Read a connection's saved queries, their rows, and their jobs with `inspect_integrations` `queries`, `query_results`, and `jobs`; save and run a query with `edit_integrations` `save_query` and `start_query`; start a sync with `start_ingestion` and read its progress with `inspect_integrations` `ingestion`. cfo.ai runs the workers and jobs. |
| Ari's own runtime tools | Its code sandbox and files, web search, chat attachments and spreadsheets, calling other MCP servers, and reading past chats | Use your own client's tools. |
| Tools switched on per workspace | Downloading a Page as a spreadsheet, image generation, and the publication library | None through MCP. The user can download a Page as a spreadsheet in cfo.ai, or ask Ari for one. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.