Errors
Failures arrive through two different channels, and the distinction matters: one is a broken request, the other is something the model should read and react to.
Tool errors
Everything the caller can act on — validation, not found, missing scope or permission, an endpoint error — comes back as a successful JSON-RPC response whose result is flagged isError, carrying the API's own error envelope as text:
{ "jsonrpc": "2.0", "id": 2, "result": {
"isError": true,
"content": [{ "type": "text",
"text": "{ \"error\": { \"code\": \"NOT_FOUND\", \"message\": \"Payable not found.\" } }" }]
} }
The code vocabulary is the API's, unchanged. The ones you will meet most:
code | When |
|---|---|
VALIDATION_ERROR | The body failed the endpoint's validation; details.issues[] names each field. Also when the tool's own arguments (id, filters) do not match its schema — including limit, cursor, sort_by or fields inside filters, which are named arguments. |
BAD_REQUEST | Unknown filter, operator or fields name. |
NOT_FOUND | No such record, or it belongs to another workspace. |
CONFLICT | A duplicate, or a record still in use. |
UNPROCESSABLE_ENTITY | A business rule — deleting a category that has children, settling a title with no account. |
SECURITY_BLOCKED | The Security tab blocked the action on this channel. |
RATE_LIMITED | The API budget of this person in the organization ran out — see Rate limit. |
And the four MCP adds itself, because they are about authorization, not about the resource:
code | Who fixes it |
|---|---|
insufficient_scope | The token's scope does not reach the tool: the resource is not in the token, write access to it is missing, or the tool moves money and the token does not carry the money resource (or all). Use a token that reaches the resource with the requested access — through the OAuth flow access is read-only today; a personal token can carry write. |
insufficient_permission | An organization admin, granting the permission the error names. |
workspace_access_denied | The workspace_id is not one the person reaches (or does not exist). An admin grants the person the workspace, or the agent passes another one, from finance_workspaces_list. |
membership_inactive | Add the token's person to the organization or reactivate them — no permission fixes a membership that does not exist or is inactive. |
They are separate on purpose: four different conversations, with different people. A single error would say "you can't" without saying whom to ask. See Authentication.
There is also workspace_required: the call carried no workspace_id and the connection has no default workspace — the person reaches several. The agent calls finance_workspaces_list and repeats the call with one of the ids. See Authentication → The workspace.
And the ones from confirming destructive tools — in all of them, nothing was changed:
code | When |
|---|---|
confirmation_required | A destructive tool called by a client that cannot confirm: 2025, or 2026-07-28 without form elicitation. |
USER_DECLINED | On 2026-07-28, the person declined or cancelled the confirmation. |
confirmation_replayed | The resent confirmation already ran the tool once. Call again without requestState to ask anew. |
confirmation_unavailable | The server could not guarantee the confirmation at that moment. Try again later. |
input_required is not an error
On 2026-07-28, the first tools/call of a destructive tool may come back with resultType: "input_required" and a confirmation request for the person. That is the normal flow, not a failure: the client shows the question and resends the same call with the answer. See Confirming destructive tools.
Protocol errors
A malformed message gets a JSON-RPC error object, not a result:
| Code | Meaning |
|---|---|
-32700 | Parse error — the body is not valid JSON |
-32600 | Invalid request — not a JSON-RPC object, or an array (batching is not supported) |
-32601 | Method not found |
-32602 | Invalid params — unknown tool, prompt or argument, missing name; on 2026-07-28, also resource not found |
-32002 | Resource not found — only on 2025 (2026-07-28 uses -32602) |
-32603 | Internal error — unexpected failure running the tool |
-32020 | Header mismatch — on 2026-07-28, an MCP-Protocol-Version, Mcp-Method or Mcp-Name header that does not match the body (HTTP 400) |
-32022 | Unsupported protocol version — the requested version is not supported; data.supported lists the supported ones (HTTP 400) |
Transport errors
Refusals that happen before the JSON-RPC message is read are plain HTTP responses: token missing, invalid or issued for another resource, a default workspace (X-Workspace-Id / ?workspace_id=) from another organization, Origin outside the allow-list, protocol rate limit, Kobana Account unavailable to validate the token (503), a body above 14 MB (413, with the JSON-RPC error -32600 Request body too large (limit 14680064 bytes) and id: null). They are listed in Authentication → Failure modes.
Why isError and not a JSON-RPC error
A 404 on a title is not a defect of the MCP client — it is information about the world, and the model on the other side needs it to correct itself. Burying it in a protocol error would make the client treat it as a transport failure, hide the text and, at best, retry it unchanged. Hence the rule: if the caller can act, it is a result; if the request is broken, it is an error.
Observability
Every tools/call writes a structured line with the tool, the workspace, the person, the token type (oauth or personal), the status and the duration. Writes are recorded with the mcp channel and the token's person, so what an agent did shows up next to what a person did, on the same screens.