MCP Server Errors
MCP Server Errors
How to handle errors returned by the foura-mcp server.
Every error response from any of the four tools (foura_auto, foura_single, foura_proxy, foura_browser) is structured. LLM agents can read the code field for retry logic without parsing prose.
Envelope shape
Every error (isError: true) carries a structuredContent block. Minimum fields on every error:
{
"service": "auto | single | proxy | browser",
"code": "rate_limited",
"error": "Rate limit exceeded"
}
On upstream errors with HTTP status, status is also present. When the platform's shared allowance refuses a call, the envelope adds retryAfter, current.{concurrency, rpm}, and limits.{maxConcurrency, maxRpm}, same shape as the underlying REST API errors.
When one of your plan's own limits refuses a call, the code IS that limit: plan_limit_ followed by credits, bandwidth, rate, concurrency, browser_daily, premium or feature. retryAfter carries the wait where a wait clears it, and is absent for a limit that a wait cannot clear, such as a feature the plan does not include. plan_limit_browser_daily carries no retryAfter either: it clears at midnight UTC.
On foura_auto, a plan limit met inside its ladder comes back as rate_limited or forbidden, with the plan's code in reason.
Stable code values
| Code | HTTP | Meaning | Retry safe? |
|---|---|---|---|
ssrf_blocked |
n/a | The target is a private or reserved address (RFC 5735, RFC 6598, IPv6 reserved), the URL isn't http(s), or its host name didn't resolve | No, check the URL. A lookup that failed briefly can be retried |
upstream_non_json |
varies | Upstream returned a body that wasn't valid JSON | Maybe, investigate |
output_validation_failed |
n/a | The MCP server's outputSchema rejected the upstream response, or the tool couldn't complete the call at all (no API key configured, API unreachable) |
Maybe: check the setup, then report |
bad_request |
400 | Input shape rejected by the FourA API, or the target URL points at FourA's own API instead of a page (only /inspect can be fetched there). Nothing is fetched or charged |
No, fix the arguments |
auth_failed |
401 | The FourA API key is missing, invalid, or deactivated; this is not about target-site credentials | No, fix the FourA key |
forbidden |
403 | Target rejected the request (a site check, a country restriction) | No, or switch to foura_proxy |
not_found |
404 | Target URL or endpoint doesn't exist | No |
rate_limited |
429 | The platform's shared per-minute allowance, or a 429 from the target that your validate rejected. On foura_auto it can also be your plan's credits, traffic or rate limit (see reason) |
Yes, wait retryAfter when it's there, otherwise back off |
at_capacity |
503 | Concurrency cap hit (current.concurrency > limits.maxConcurrency) |
Yes, wait retryAfter seconds |
service_disabled |
503 | The service is switched off for maintenance. A tool your plan doesn't include comes back as plan_limit_feature |
Contact support |
service_unavailable |
503 | Generic 503 from upstream | Yes, short backoff |
upstream_error |
500+ or 0 | The target answered with a server error, or on foura_proxy, foura_browser and foura_auto never answered |
Yes, exponential backoff |
upstream_client_error |
4xx | Other 4xx not covered above | Usually no |
upstream_unknown |
other | The request ran but produced no accepted answer: on foura_single the target never answered (timeout, refused connection), and on any tool your validate rejected a 2xx or 3xx response. Read status and error |
Investigate |
no_eligible_proxy |
n/a | No proxy matches the strict exitCountries allowlist; details.exitCountries contains the normalized scope |
Retry later; change scope only explicitly |
plan_limit_credits |
429 | The monthly credits on your plan are spent | Yes, after retryAfter, or change the plan |
plan_limit_bandwidth |
429 | The traffic allowance on your plan is spent for this billing period | Yes, after retryAfter, or change the plan |
plan_limit_rate |
429 | Your plan's requests per minute for that endpoint | Yes, after retryAfter |
plan_limit_concurrency |
429 | Your plan's simultaneous requests for that endpoint | Yes, after retryAfter |
plan_limit_browser_daily |
429 | Your plan's daily Browser allowance is spent | Yes, tomorrow, or use foura_single / foura_proxy |
plan_limit_premium |
403 | You sent exitClass: "premium" on a plan that does not include premium exits |
No, drop the parameter or change the plan |
plan_limit_feature |
403 | The plan does not carry that endpoint or that capability | No, change the plan |
HTTP-level errors from the MCP server
Some failures happen at the MCP transport layer, before any tool is called. These return raw JSON-RPC errors (no structuredContent):
| HTTP | When | What you see |
|---|---|---|
| 400 | Unsupported MCP-Protocol-Version header |
Unsupported MCP-Protocol-Version: <value>. Supported (from @modelcontextprotocol/sdk): 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07. Upgrade foura-mcp's SDK pin to extend. |
| 401 | A tool call or resource read with no API key. Listing tools and prompts works without one | JSON-RPC error + WWW-Authenticate: Bearer realm="foura-mcp" |
| 403 | Disallowed Origin or Host header (DNS-rebinding defense, CVE-2025-66414) |
Origin <value> is not in the allowlist or Host <value> is not in the allowlist |
| 405 | GET or DELETE on /mcp (stateless mode) |
Method not allowed in stateless mode. Use POST /mcp. |
| 413 | Request body > 256 KB | Express default 413 |
The allowlists for 403 are env-configurable for self-hosters via FOURA_MCP_ALLOWED_HOSTS and FOURA_MCP_ALLOWED_ORIGINS.
Refused browser profiles
A browser profile that the catalogue cannot present, or a profile sent with unblocker set to false, comes back as an upstream failure with the reason in error and the request never leaves FourA. The message names what is available, so retry with one of the listed combinations rather than the same one.
These are refusals, not outages: retrying the identical request cannot succeed, and no other browser was used in its place.
Retry strategy
Five buckets:
- Your own plan refused it, not the target: any
plan_limit_*code. The same work through another tool is refused as well, so switching endpoints spends nothing but time. Wait outretryAfterwhere there is one; otherwise the plan has to change.plan_limit_premiumis the one you can clear yourself, by droppingexitClass. - Wait and retry:
rate_limited,at_capacity,service_unavailable,upstream_error. HonourretryAfterwhen present. Use exponential backoff with jitter when it isn't there. Don't answer it by re-issuing every queued tool call at once: cut how many you run in parallel instead. - Preserve scope and retry later:
no_eligible_proxy. Do not removeexitCountriesor substitute another country silently. Change or widen the allowlist only when the user explicitly changes the requirement. - Do not retry until the input or credential is fixed:
bad_request,auth_failed,not_found,ssrf_blocked. Forauth_failed, verify the FourA API key, not target-site credentials. - Switch tool when the content requires it:
forbiddenonfoura_singlecan justify a boundedfoura_proxyattempt. Usefoura_browserwhen the desired content needs JavaScript. After a successful proxy selection, pass the returnedproxyID intofoura_browser.proxyinstead of starting a new selection.
Retry example (TypeScript, MCP-side)
async function callWithRetry(call: () => Promise<any>, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const r = await call();
if (!r.isError) return r;
const code = r.structuredContent?.code;
const wait = r.structuredContent?.retryAfter ?? Math.min(2 ** attempt, 30);
if (["rate_limited", "at_capacity", "service_unavailable", "upstream_error"].includes(code)) {
await new Promise((res) => setTimeout(res, wait * 1000));
continue;
}
// Non-retryable, surface to caller
throw new Error(`${code}: ${r.structuredContent?.error}`);
}
throw new Error("max retries exceeded");
}
Related
- MCP Server, the four tools and their schemas
- MCP Recipes, workflow prompts shipped with the server
- API Errors, same envelope at the underlying REST API layer
- Rate Limits, the account and platform limits behind
rate_limitedandat_capacity