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.
stepschooses how Ari’s tool work shows.none, the default, folds each turn’s tool steps and the notes between them into onestepsitem with their count.summaryshows one item per step.fulladds 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 returnsolderto page back; a read withafterkeeps the oldest and returnsnext. - Waiting for Ari.
createandsendreturn at once withnext: the exactinspect_chatsreadcall that waits, up to 50 seconds throughwait_seconds, while the chat is busy. A read of a chat that is still busy returns anothernext. 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’sresource_uri,cfoai://orgs/{org_id}/chats/{chat_id}, on the MCP 2026-07-28subscriptions/listenstream, and read the chat’s newest page withresources/read. The olderresources/subscribeis refused. - While Ari works.
deliveryonsendissteerby default, which ends the running turn after its current step and answers the message next;queuewaits for the turn to end.request_id, a UUID the client makes, makes a repeatedsendano_opthat returns the samemessage_id. - Limits. A
create, or asendto 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 thechat_urlthe result carries. An MCP read does not mark the chat read in the web app.
Fields every tool accepts
Every tool exceptlist_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 takescenario, 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 flataction 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 tonullcounts as not filled. - Fields beside the block apply to it:
scenarioon most model tools,as_of_pointoninspect_pagesandinspect_scenarios,validateonedit_canvas_blocks,targetonedit_dimension_mappings, andidempotency_keyonedit_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_usertakes anemailblock, aslackblock, 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:
org_id on later calls. A number, or a string of digits, always names a workspace ID; anything else is read as a slug.
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 asRevenue#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#940names the connection now calledAcme Payroll#940, andCompletion and final validation#6canames a text block after anupdate_textchanged 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#725names the Scenario since renamedPlan - No Series A#725, andSA26-18a#50cnames a Page afteredit_pagesupdaterenamed it, inedit_pages,inspect_pages, and thepagea tablecreatenames.Mainwith the base Scenario’s suffix names the base Scenario. - A name that is ambiguous or matches nothing is refused with
error.candidateswhen alternatives are known: each withdisplay_name,refto pass back,kind, and, when ranked,scoreandmatch. - A result names each object with
ref, the selector to pass back, andurl, 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 asstructuredContent. 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
errorwithcode,message,transient, and, when known,candidatesandhint, besidestate_changed. Codes includeambiguous,not_found,invalid,blocked,conflict,stale_target,selection_too_wide,file_too_large,result_unavailable, andbrowser_validation_unavailable. result_unavailablereads The call’s outcome is unavailable. Read current state before repeating a write; repeating it could apply it again.- A
not_foundfor a name orName#hexreference 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.ormerged into its parent scenario, only for a user who could see that Scenario just before it was removed. Anycandidatesare objects that exist now, and thehintsays each is a different object. - A result with
ok: falseand anerror_kindis an answer, not a crash. Use its message andhintto 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: truemeans the same call can succeed later without changes. A failure that knows whether it is retryable addsTransient: true.orTransient: 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 carriestruncation and, when it can continue, next:
truncation.bysays what cut it:response_budget(the next item did not fit one result),count_limit(the page reached thelimityou set),row_limit, ordepth.truncation.showncounts what came back, andtruncation.total, when counted, what the selection matched. Notruncationmeans the result is complete.nextholds 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: trueon everyinspect_*tool,inspect_chatsincluded,validate_code_blocks, andlist_orgs.openWorldHint: trueon 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, andvalidate_code_blocks.destructiveHint: falseonly onnotify_user. Every other write keeps the MCP default, which treats it as possibly destructive.