Skip to main content
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.

Server

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

Pages and Canvas blocks

Canvas blocks for agents covers these tools in detail.

Scenarios and history

Connected data

Workspace, people, and automation

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.
  • 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.

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 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.
  • 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. 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:
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:
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.
  • 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. 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.
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: 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.