Skip to content

7 · If something goes wrong

What you will have done: read every refusal this endpoint gives, and learned which of them are faults and which are the product working.

Most of what follows is not a fault. A gate refusing a call is the gate doing its job, and the sentence it gives you names what was missing. Read the sentence before changing anything.

401 — the token

{"jsonrpc":"2.0","id":null,"error":{"code":-32001,
 "message":"unauthorized: mint a token in Settings → API"}}

The header is missing, misspelled, or carries a token that has expired or been deleted. Four things to check, in this order.

  1. Is the header there at all? In Claude Code, claude mcp list shows ! Needs authentication rather than ✔ Connected. In OpenCode, check that oauth is false: it probes for OAuth on a remote server and can take that path instead of your header.
  2. Is it the whole value? The token is prefixed nck_, and a value truncated by a copy is indistinguishable from a wrong one.
  3. Has it expired? Settings › Access & Security › API lists every token with its expiry. A token past it stops working with this same sentence.
  4. Is it the right instance? A token is minted on one instance and means nothing on another.

The raw value is shown once at mint and only its hash is stored, so a token you cannot find is a token you re-mint rather than recover.

403 — the scope

A token reaching one of the sixteen call sites that put something beyond recall into the world is refused, and the refusal names the scope and where it is granted.

this token cannot put letters on the wire: mint one with the wire scope in Settings → Access & Security → API

This is not a bug to route around, and there is nothing to route around it with. The refusal holds whichever way the call arrived: a named tool, api_request, admin_request, the REST API directly, or a procedure running inside the pod. The connecting page lists all sixteen.

If the work genuinely needs it, an administrator ticks Can put letters on the wire (wire) when minting. Ask yourself first whether it does. A token carrying wire is a token that can send mail to strangers unattended.

The run tool refuses the same way, for its own scope:

this token cannot run scripts: mint one with the scripts scope in Settings → Access & Security → API

Two refusals in this family are not about scopes at all and cannot be granted away.

a token cannot mint tokens comes from the mint route, and every token gets it. Minting is how a scope is granted, so a token that could mint could grant itself the wire. A person mints tokens.

A hook refused it before it left your machine. If the wire guard is installed, it refuses the same acts locally with a sentence naming the specific harm rather than the scope. That is the guard working, and the answer is the same: a person does this one.

405 on GET /mcp

method not allowed

Expected. The endpoint is stateless and nothing streams, so there is no server-initiated channel for a GET to open. A client that insists on one is configured for SSE rather than streamable HTTP; in Claude Code that is -t http, not -t sse.

A move that parked

A work_move to done comes back as the item in its new column, with the id of the approval somebody now has to decide:

{"item": {"status": "in_review", "…": "…"}, "approval_id": "…"}

The status is the answer. A move that landed reads done; a move that parked reads in_review and carries an approval_id rather than null.

This is expected, and it is the single most common surprise on this endpoint. Every move to done an agent makes parks, whether or not the item was marked as needing review.

There is nothing to retry. The item is in In review, a person who is not its author will decide, and an agent is never that person. Comment on the item saying what you did and leave it.

An import that was refused

Three different refusals wear similar clothes.

"…preview this file before importing it." An import may only write the file whose preview was read. If the file changed between the two calls, even by a byte, the write is refused so that a mapping checked against one sheet is never applied to another. Run the dry run again on the file you actually have.

Nothing was written and you expected it to be. Dry run is the default. dry_run anything other than the literal false only reports. Read the plan it gave you, then call again with dry_run: false.

A hook refused it locally. If neutron-import-guard is installed, a dry_run: false call with no matching dry run earlier in the session is stopped on your own machine before it leaves. That is the guard working. Do the dry run.

An import that went in and should not have has one clean way back: the write returns a batch_id, and DELETE /api/admin/crm/people/import/:batch rolls it back while nothing has been sent.

A result that stopped mid-sentence

…truncated (over 48000 chars)

Results are capped at 48,000 characters. The cap is on the whole body, so a broad query is cut wherever the cap falls rather than at a record boundary, and parsing what came back will fail in confusing ways.

A request tool stops reading at the cap rather than reading everything and slicing, so it says over 48000 chars and cannot tell you how much more there was. A procedure, which has the whole answer in hand before it cuts, still ends …truncated (91234 chars total) and names the full size.

Narrow the question rather than retrying it. mail_search takes limit, campaign and identity. work_search takes a project, a status and an assignee. Where a procedure exists for what you are asking, use it: returning a digest instead of six bodies is most of what a procedure is for.

Other answers worth recognising

What you seeWhat it means
-32601 method not supportedThe method is not one of the seven this endpoint answers. On resources/list it means an instance older than resources, which is a version to check rather than a fault.
-32002 no resource named "…"The URI is not one this instance serves. resources/list and resources/templates/list have them, and the message says so.
-32002 … could not be read: the route answered HTTP 403The resource exists and your token may not read it. The refusal comes back as an error rather than as the resource’s contents, deliberately.
path not allowed — write it canonically, as the router holds it: no query string, no whitespace, no dot or empty segments, no encodingapi_request was given something outside the literal path alphabet: a dot or empty segment, an encoded character, whitespace, or a path carrying a ?. The gate admits a known shape rather than rejecting a known-bad one, because the URL parser deletes a tab, a newline or a trailing space before anything dispatches, so a denylist would read a different string from the one that gets called. Query parameters belong in the separate query argument.
path not allowed — every route this tool reaches starts /api/api_request was given a path outside /api. There is nothing else for it to reach.
path not allowed — canonical /api/admin/* …The same shapes through admin_request, which reaches only /api/admin/* plus GET on /api/agents, /api/models and /api/settings, and no /api/work/* route by design. Reach for api_request if the route you want is somewhere else.
/api/chat is not reachable here: it streams …A request tool was pointed at /api/chat. A turn started there would be filed in the ledger as a web turn. The chat tool runs one and returns its text.
/api/events is an endless event stream, so there is no answer to wait for …A request tool was pointed at the event stream. The sentence names what to read instead: GET /api/inbox, GET /api/threads, or the approvals_inbox tool.
that route attaches to a live thread and streams until its client leaves …/api/threads/{id}/attach. GET /api/threads/:id reads the thread as it stands.
that route streams a run's events until its client leaves …/api/workflows/runs/{runId}/events. GET /api/workflows/runs/:runId reads the run as it stands.
that route answers a stream rather than a body, so there is nothing here to wait for. Read the state it reports instead.Any other route answering text/event-stream. The gate cannot enumerate every streaming route, so this one is caught on the response and the body is dropped rather than read.
that route had not finished answering after 30 seconds …The read hit its time budget. What had arrived was discarded and nothing was retried, so this is not a partial answer to parse. Ask for less.
that body is N KiB and the limit is 1024 KiB. Nothing was sent.The JSON body exceeded 1 MiB. Nothing left the tool. Split the work across calls, or use the route’s own import path where it has one.
there is no /api/mcp — this endpoint is /mcp, and no tool calls itA request tool was pointed at the MCP endpoint itself.
query must be an object of plain values, one level deepThe query argument was a nested object, an array, or not an object at all. One level, plain values.
method must be one of GET/POST/PUT/PATCH/DELETEAny other method, on either request tool.
an agent does not decide approvals …approval_decide was called on an agent turn. It is refused before it reaches the route, whatever the card. A person settles it, on screen or with a token of their own.
item_id must be a work item uuidA work tool was given a project id or a key where it wanted an item’s uuid. Read the item with work_search first.
A refusal naming a revisionSomebody changed the item since you read it. Read it again and retry against the revision it gives you.
A claim refused, naming who holds itAnother worker has the card. It is an answer, not a failure.
A spine move that reports something other than what you asked forThe spine is forward-only. Somebody who has unsubscribed does not move to positive, and the answer says what actually happened.
An address that came back UNKNOWNRead the per-rung account beside it. A prober that could not reach the network is a different thing from an absent mailbox, and the answer distinguishes them.

When it is genuinely broken

A tool listed in tools/list that answers with a sentence about its own arguments whatever you pass it is a dispatch fault rather than a gate, and worth reporting with the exact call. So is a 500.

Everything else on this page is a boundary. The endpoint is built so that the failures you meet are refusals with sentences attached, and the sentence is the documentation.

Last updated on