Errors and limits
Errors reach your agent at three levels: HTTP errors on the request itself, JSON-RPC protocol errors, and tool errors inside a normal response. Tool errors are the ones an agent handles most often, and each carries a stable code and a hint about whether retrying can help.
Tool errors
Section titled “Tool errors”When a tool fails, the response is a normal MCP tool result with isError: true. The error is in structuredContent.error, and the same JSON is in the text content:
{ "code": "LOCKED", "message": "The prop is checked out by someone else; coordinate with the holder or wait for the lock to lapse.", "details": { "lock": { "holder": { "name": "sam" } } }, "http_status": 409, "retryable": false}| Field | Meaning |
|---|---|
code |
Stable error code. The same codes the REST API uses. |
message |
Human-readable explanation, usually with the fix. |
details |
Extra structured data, when there is any (for example the lock holder, or retry_after_seconds). |
http_status |
The underlying HTTP status, when the error came from the library. |
retryable |
true when backing off and calling the same tool again can succeed (timeouts, rate limits, the agent-call allowance once the month rolls over, and 5xx errors). |
Common codes
Section titled “Common codes”| Code | Meaning | What to do |
|---|---|---|
FORBIDDEN |
The key is read-only and the tool needs write, or the member’s role does not allow the action. |
Use a write-scoped key, or ask an admin. |
VALIDATION |
An argument is invalid (bad hash, wrong combination of arguments, bad cursor). | Read the message and fix the arguments. |
ENTRY_NOT_FOUND, NOT_FOUND |
The prop or object does not exist. | Check the id. |
LOCKED |
Someone else has the prop checked out. details names them. |
Do not force it. Tell the user or wait. |
STALE_FACE |
The prop has a newer version than the one your check-in was based on. | Re-sync: download the current version, reapply, check in again. |
CONFLICT |
The action clashes with existing state, for example a review round already open on that version. | Use the existing item named in the error. |
HASH_MISMATCH |
Inline file bytes do not match content_hash. |
Recompute the SHA-256. |
DENYLISTED |
This content is blocked in your library. | Do not retry. |
USER_NOT_FOUND |
A member name did not match anyone. | Use the exact login name or a numeric id. |
JOB_STATE |
The job is in a state that does not allow this, such as cancelling an import that already started. | Check the job with get_unreal_import. |
RATE_LIMITED |
The API key went over its rate limit. retryable is true and details.retry_after_seconds is the wait. |
Wait that many seconds, then retry. Slow down the loop making calls. |
AGENT_CALL_LIMIT |
The workspace used its monthly agent-call allowance. details has retry_after_seconds (seconds until the next UTC month), used and limit. |
Stop calling tools and tell the user. Retrying works only after the month rolls over, or once an admin raises the allowance. |
EMBEDDINGS_UNAVAILABLE |
Similarity or image search cannot run: 409 means the reference prop has no similarity data yet; 503 means the service is temporarily unavailable. |
For 409, pick another reference or try later. For 503, back off and retry later; text search still works. |
INTERNAL |
The tool failed unexpectedly. | Retry once after a pause. |
The Unreal import tools have their own codes, listed in Import props into Unreal.
Protocol errors
Section titled “Protocol errors”Some mistakes are rejected before a tool runs, as a JSON-RPC error instead of a tool result:
| JSON-RPC code | Cause |
|---|---|
-32602 |
Unknown tool name, unknown argument, missing required argument, or an argument of the wrong type. The message names the problem and, for unknown arguments, lists the allowed ones. |
-32601 |
A method the server does not support. |
-32600 |
The body is not a valid JSON-RPC 2.0 message. |
-32000 |
A request other than a tool call (for example initialize or tools/list) went over the rate limit. error.data carries the same payload as a RATE_LIMITED tool error, and the response has a Retry-After header. |
-32700 |
The body is not valid JSON. |
HTTP errors
Section titled “HTTP errors”These are returned on the request itself, before it reaches a tool. Most MCP clients show them as a connection or transport error. The body is {"error": "<message>"}.
| Status | Cause |
|---|---|
401 |
Missing, wrong or revoked API key. |
402 |
The workspace is suspended. Fix billing at app.prophouse.dev/billing. |
413 |
The request is larger than 1 MB, or has no Content-Length. |
429 |
The rate limit or the monthly agent-call allowance was reached on a batch request, or on a message without an id. Single requests get an MCP error instead; see Rate limit. |
503 |
The library is being prepared. Retry after the number of seconds in Retry-After. |
Rate limit
Section titled “Rate limit”Each API key can make 240 requests per minute. Every request with that key counts toward it, including connection requests and tool calls. Over the limit, a tool call gets a normal tool error (isError: true) instead of a transport failure:
{ "code": "RATE_LIMITED", "message": "Rate limit exceeded for this key. Back off and retry.", "details": { "retry_after_seconds": 30 }, "http_status": 429, "retryable": true}The HTTP response also carries a Retry-After header with the same number of seconds. Other single requests, such as initialize or tools/list, get a JSON-RPC error with code -32000 and this payload in error.data. A JSON-RPC batch, or a message without an id, gets plain HTTP 429 with Retry-After: 30.
When your agent sees RATE_LIMITED, it should wait for details.retry_after_seconds before trying again, not retry in a tight loop. Other tool errors with retryable: true deserve the same treatment; details.retry_after_seconds gives the wait when the server provided one.
Agent-call allowance
Section titled “Agent-call allowance”On hosted plans, every MCP tool call counts toward the workspace’s agent-call allowance for the calendar month.
| Plan | Agent calls per month |
|---|---|
| Free | 500 |
| Indie | 5,000 per seat |
| Studio | 25,000 per seat |
| Enterprise | Custom |
What counts:
- Each request that calls a tool counts once, whether the tool succeeds or fails.
- Connecting (
initialize), listing tools and pings do not count. - Searching, browsing and downloading from the desktop app, the web library, the Unreal plugin and the REST API never count. Search is unlimited there.
The allowance is a ceiling, not a bill: you are never charged for agent calls. When the workspace reaches it, tool calls return a tool error with code AGENT_CALL_LIMIT until the next month starts (UTC). details.retry_after_seconds is the number of seconds until then, and details.used and details.limit show the count; the response also has a Retry-After header. A batch request that includes a tool call gets plain HTTP 429 instead. Searching from the apps keeps working. To raise the ceiling, add seats or move to a larger plan at app.prophouse.dev/billing. The Usage page at app.prophouse.dev/usage shows this month’s agent calls against the allowance, broken down by API key.
Request size
Section titled “Request size”Every MCP request must be 1 MB or smaller. That bounds the two tools that carry file data inline:
search_props_by_image: keep the reference image under about 500 KB before base64 encoding.checkin_propwithfile_base64: keep the file under about 500 KB. The server refuses inline files over 700,000 bytes. Larger files use the upload flow (upload_required), which has no such limit.
Download and upload URLs
Section titled “Download and upload URLs”Tools that hand out file URLs (get_download_url, checkout_prop, and checkin_prop when it returns upload_required) give one of two kinds:
| Kind | How to recognize it | How to use it |
|---|---|---|
| Presigned | A full URL. Download URLs come with an expires_at time; uploads have mode: "direct". |
Send no Authorization header. For uploads, send exactly the headers provided. Use a download URL before expires_at, or ask for a new one. |
| Server path | A path such as /api/v1/blobs/<content_hash>. Downloads have expires_at: null and requires_auth: true; uploads have mode: "server". |
Resolve it against https://api.prophouse.dev and send your usual Authorization: Bearer header. |
Sending your API key to a presigned URL makes the request fail, because the URL already carries its own authorization.
