Skip to main content
A failed tool call comes back with isError set and the error envelope in the result’s TEXT block, as JSON. It is deliberately not in structuredContent: a client validates any structuredContent it sees against the tool’s output schema with no exemption for errors, so an envelope there would hard fail every standard client. Branch on isError, then parse the text block. Two things arrive with isError set, and only one of them is this envelope. A call that never reached the tool, because its arguments did not match the published inputSchema, is refused by the protocol layer, and its text block is a SENTENCE rather than JSON:
So do not parse the text block unconditionally. Try JSON first and treat a parse failure as a malformed request on your side: there is no code to branch on, because nothing was attempted and no error in the table below occurred. This is the same refusal that catches a misspelled parameter name, and it is the most common one a new integration meets. On the REST surface the equivalent refusal DOES carry a code (invalid_argument, HTTP 400), so a client written against both surfaces cannot share one branch here.

The envelope

The envelope also carries whatever fields the specific error adds, such as deviceId, reason or capability. Read the ones you branch on and ignore the rest. retryable means re-issuing the SAME call unchanged is meaningful, not that you should loop. It is a property of the occurrence, not of the code: the same code can be retryable once and not the next time, so read the field rather than assuming from the code.

Codes

The code field is a closed set of 44 values across MCP tools and REST. Adding one is additive; changing or removing one is a breaking change. A tool call can only answer the 18 marked “MCP tools and REST”; the rest arrive only in the REST envelope.

The other two surfaces

The closed set above covers MCP tool calls and the REST envelope. This service answers in three envelopes, not one, and a client that branches on the table above everywhere will break on the other two. Each surface always answers in its own envelope. The REST envelope carries retryable when the refusal knows the answer: it is a property of the occurrence rather than of the code, so read the field instead of inferring it from the code. The HTTP status still carries the outcome, and a 401 additionally carries a reason naming which credential problem it was.

Device surface codes

The device surface speaks the visualremote/1 dialect, so it has its OWN closed set of 10 codes. The two sets are deliberately separate: the dialect stops at the device channel rather than spreading into the rest of this API. Branch on these when you are writing a worker. One of them is worth calling out. DEVICE_NOT_FOUND covers an unknown, expired, terminal or wrong-device run token, all answering identically so that a token holder cannot probe which it was. It is also what you get if you point an ORG API KEY at this surface, and in that case nothing is missing: this is the worker channel, its credential is the run token in the url, and no Authorization header is read at all. Drive devices yourself over /mcp instead.