Errors, limits, and correlation
The four layers a failure can come back in, every tool error code, the admission limits, and how to make a call traceable.
A call to the Operator MCP server can fail in four places. Check them in this order.
1. HTTP and OAuth
The request never reached a tool.
| Response | Meaning | Handle by |
|---|---|---|
401 + WWW-Authenticate: Bearer … | Missing, expired, or refused token. The body is an OAuth error, not JSON-RPC. | Re-run OAuth discovery from the challenge. Never retry with improvised credentials. |
403 + insufficient_scope | The token carries neither operator scope. | Authorize again requesting mcp:operator.read. |
400 with JSON-RPC -32022 | The request is not MCP 2026-07-28 (for example, it has no _meta envelope). data.supported lists the served revisions. | Use a client that speaks 2026-07-28. |
400 with JSON-RPC -32020 | The Mcp-Method or Mcp-Name header is missing or does not match the body. | Fix the headers. |
405 | An authenticated GET or DELETE. The endpoint is stateless and only takes POST. | Send POST. |
404 | Not an MCP endpoint. | Check the URL. |
500 | The server could not check the token. | Retry after a short wait. |
2. JSON-RPC errors
The request was well formed, but the call was refused before the tool ran. These come back with HTTP 200 and a JSON-RPC error:
| Code | Meaning | Handle by |
|---|---|---|
-32602 (InvalidParams) | Unknown tool name, or arguments that fail the tool's schema: a missing required argument, an extra one, or a value out of range. | Fix the call. Retrying unchanged cannot succeed. |
3. Tool errors
The tool ran and failed. The result has isError: true, and its text content is:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests. Retry after the rate-limit window resets.",
"retryable": true
}
}Show the message, and retry only when retryable is true.
code | Retryable | Meaning |
|---|---|---|
rate_limit_exceeded | yes | Over 120 calls a minute for this tool. See admission limits. |
concurrency_limit_exceeded | yes | Over 12 calls in flight for this tool. |
rate_limit_unavailable, concurrency_limit_unavailable | yes | The admission store is unreachable. The server refuses rather than admit an unchecked call. |
forbidden | no | The person is no longer an admin or member of the organization. |
organization_scope_mismatch | no | The call named an organization the token is not bound to. Authorize again for that organization. |
invalid_organization_selector | no | organizationId and organizationSlug name different organizations. |
organization_not_found | no | No organization matches the selector. |
organization_required | no | No organization could be resolved for the call. |
not_found | no | The requested module is not in this organization. |
operation_not_found | no | get_operation was given an operationId that is not the module's current outline run. |
dependency_unavailable | when the backend answered 5xx | Vidext's read service could not answer. |
invalid_dependency_response | yes | Vidext's read service answered with something the server could not read. |
internal_error | yes | Anything else. The message is generic on purpose. |
4. In-band failures from the knowledge tools
search_knowledge and read_knowledge_source report most failures inside a normal result, without isError. Always check success:
{ "success": false, "status": "unavailable", "reason": "timeout", "error": "Organization knowledge is unavailable right now." }status | Tool | Meaning | Retry? |
|---|---|---|---|
invalid | both | Empty or too vague query, or no handle. | No, fix the input. |
unavailable | both | Knowledge could not be read. reason: "timeout" when the 15-second budget ran out. | Yes. |
rejected | read_knowledge_source | The handle did not come from search_knowledge for this organization, or no longer matches its source. | No, search again. |
missing, empty | read_knowledge_source | The source, or its processed text, is gone. | No. |
Admission limits
Every tool call is counted per person, organization, and tool:
| Limit | Value |
|---|---|
| Rate | 120 calls per minute (sliding window) |
| Concurrency | 12 calls in flight |
A refused call comes back as a tool error (rate_limit_exceeded or concurrency_limit_exceeded), not as HTTP 429. The MCP endpoint sends no 429, no Retry-After, and no RateLimit headers. Back off for a few seconds and retry. Calls refused with InvalidParams are not counted.
If the admission store is unavailable, the server fails closed with a retryable *_unavailable error instead of running the call unchecked.
The Docs MCP server has no admission limits of its own.
Limits on the OAuth and registration routes
These HTTP routes answer 429 when you go over, and advertise their quota in headers:
| Route | Limit | Headers |
|---|---|---|
/api/auth/* | 50 requests per 10 seconds by default; sign-in, sign-up, and email-code routes 5 per 5 minutes | RateLimit-Policy (for example "auth";q=50;w=10) and RateLimit-Limit: the quota, not what is left. Retry-After on a refusal. |
/api/agent/auth and /api/agent/auth/claim/complete | 60 registrations and 30 completions per hour, per IP | The same, plus the live window: RateLimit (limit=…, remaining=…, reset=…), RateLimit-Remaining, RateLimit-Reset. Retry-After on every 429. |
The authorize, token, and consent endpoints (/api/auth/oauth2/authorize, /token, /consent) do not carry these headers.
Correlation
Make each call traceable, so a support request can be matched to Vidext's logs:
- Send an
x-request-idheader with a unique value on eachPOST, and keep it. Without one, the server generates its own, which you never see. - Send a W3C
traceparentheader, orx-trace-id, to tie the call to your own trace. - Keep the
moduleIdyou passed.
When a tool fails, Vidext records the request ID, the trace ID, the tool, and the moduleId. Responses do not echo these headers back. To report a problem, send the request ID, the time, the tool, and the error code.
Last updated on