Reference

Limits & errors.

Everything a client needs to behave well: what will throttle you, how much comes back at once, and what to branch on when a call fails.

Rate limits

Two fixed windows, both per minute. The pre-auth limit is deliberately the looser of the two: grants share egress IPs, so a per-IP cap tighter than the per-grant cap would throttle unrelated callers behind one NAT.

120 / minute — per grant
Applied once your key is resolved. Exceeding it returns -32000 with data.code: "rate_limited" and a resetAt timestamp.
300 / minute — per IP, before authentication
Applied to unauthenticated traffic so an unknown caller cannot force the key-hashing path repeatedly. Answers HTTP 429 with Retry-After.

Response budget

A single result is capped at 64,000 bytes serialized. List tools page within it rather than truncating: when more remains, the result carries nextCursor — in _meta and named in the result text, so it survives a host that drops _meta. Pass it back unchanged. An absent cursor means you have everything.

Error vocabulary

Branch on error.data.code, not the numeric JSON-RPC code. The strings are the stable contract; the numeric band can be renumbered by a protocol revision without breaking you.

unauthenticated — -32006
No credential, or one that failed. Not retryable without a different key.
unauthorized — -32003
Authenticated, but your identity may not see or modify this row. Row-level security decided it, not the server.
invalid_input — -32602
Arguments failed validation. The message names the field. Not retryable unchanged.
not_found — -32602
The id does not exist or is invisible to you — deliberately indistinguishable, so the surface cannot be used to enumerate.
conflict — -32005
Already exists, or a unique constraint refused it. Often means your write already succeeded.
rate_limited — -32000
Back off until resetAt. Retryable.
upstream_error — -32001
The database returned something unexpected. Retryable with backoff.
server_misconfigured — -32000
Our fault, not yours — a required binding is missing. Not retryable; tell us.

Note that -32000 carries both rate_limited and server_misconfigured — one is retryable and one is not. That is precisely why the string, not the number, is the contract.

Writes and retries

Tools whose table has no unique index require an idempotency_key: a retry with the same key returns the stored result instead of acting twice. This matters most where a write also notifies a real person — see the badges in the catalogue.