# MCP Errors & Limits

> Reference for error conventions, rate limits, circuit-breaker behavior, request tracing, and known gaps in the Lev MCP server.

Source: https://www.lev.com/docs/build/mcp/errors-limits

Last updated: April 2026

---
## Error Convention

Every tool returns a JSON string. On failure, the string describes the error in plain language. The MCP transport never sees a raw HTTP status code.

**Examples**

```text
Validation failed: loan_amount must be greater than 0
Deal not found: 101
Unauthorized: your session has expired. Please sign in again.
Upstream service is temporarily unavailable. Please retry in a few seconds.
```

This shape is deliberate. Models can relay the message back to the user without parsing an envelope, and humans reviewing the transcript see exactly what went wrong.

  Internally the server maps backend responses to typed exceptions before serializing: 400/422 → validation, 401 → authentication, 403 → forbidden, 404 → not found, 429 → rate limited, 5xx → server error. Each maps to a distinct human-readable prefix.

## Common Errors

**What each error usually means**

- **Validation failed: ...:** Backend rejected the request body. Check enum values and required fields. Enums are lowercase on the wire.
- **... not found: :** Resource does not exist for this user, or has been soft-deleted. Confirm the id and the active account.
- **Unauthorized: ...:** JWT is missing, malformed, or expired. The client should refresh automatically; if not, re-run sign-in.
- **Forbidden: ...:** Your Lev role does not have access to the requested resource. The MCP server does not elevate privileges.
- **Too many requests: ...:** Rate limit hit. Back off and retry — the circuit breaker does not open on 429.
- **Upstream service is temporarily unavailable:** Backend returned a 5xx. Repeated failures will trip the circuit breaker — see below.

## Rate Limits

**Limits**

- **Overall:** 100 requests per minute per session.
- **Per tool:** 20 requests per tool per minute.
- **Concurrency:** 10 concurrent in-flight requests.

When you hit a rate limit, the error message includes guidance to retry. Back off with jitter; do not retry immediately in a tight loop.

## Circuit Breaker

The MCP server protects the Lev backend with a simple circuit breaker in front of every tool call.

**Circuit breaker**

- **Threshold:** 5 consecutive upstream 5xx responses opens the breaker.
- **Cooldown:** 30 seconds in the OPEN state, during which all tool calls return "Upstream service is temporarily unavailable".
- **Recovery:** After cooldown, the breaker moves to HALF_OPEN and lets a single probe through. A clean response closes the breaker; another failure re-opens it.
- **429s:** Rate limits are not treated as upstream failures. A 429 does not trip the breaker.

If every tool starts returning "Upstream service is temporarily unavailable" at once, the breaker is likely open. Check [MCP Health Checks](#health-checks) and retry after 30 seconds.

## Request Tracing

Every tool call forwards an `X-Request-Id` UUID to lev-backend. When something surprising happens:

Write down the approximate timestamp of the failing tool call.

Share the tool name and the timestamp with Lev support.

Lev correlates the request ID with backend logs and follows up.

Request IDs are not surfaced to the LLM output today. This is intentional — they add tokens without helping the model reason. If you need them exposed for a specific integration, reach out.

## Health Checks

The server exposes a health endpoint at `/health`. It is safe to poll.

```bash
curl <mcp-server-url>/health
```

```json
{
  "status": "ok",
  "version": "0.1.0",
  "lev_api": "reachable",
  "circuit_breaker": "closed"
}
```

`circuit_breaker` reports `closed`, `open`, or `half_open`. `lev_api` reports `reachable` or `degraded` based on the last probe. Use these to differentiate between a backend outage and a client-side issue.

## Known Gaps

AI lender matching is live on the MCP surface: `recommend_lenders_for_deal` runs a deal's match, `get_lender_recommendations` reads the ranked lenders, and `unlock_contact` reveals a matched contact. Lender discovery is deal-scoped — there is no directory-wide lender search tool, so use `get_lender` for a direct lookup when you already know a `lender_id`.

  If you hit a gap that blocks a real workflow, tell Lev — gaps get prioritized based on real use, not hypothetical needs.