# MCP Server Rate Limiting + Caching Pattern

When an MCP server wraps multiple external APIs (public records, web search, etc.), many free-tier APIs enforce rate limits. The Super Search MCP server at `/root/docker/super-search/server.py` uses this pattern.

## Architecture

```
Query → TTL Cache (in-memory, per-provider TTL)
          ↓ miss
       Token Bucket Rate Limiter (per-provider)
          ↓ allow
       Provider API → success? → return + cache
          ↓ 429 / rate limited
       Fallback to next provider in chain
          ↓ all depleted
       Return cached or error
```

## Token Bucket Implementation

```python
from collections import defaultdict
import time

RATE_LIMITS = {
    "searxng":       {"tokens": 30, "interval": 1.0},   # local, high
    "exa":           {"tokens": 10, "interval": 1.0},   # paid tier
    "firecrawl":     {"tokens": 5,  "interval": 60.0},  # 1k/mo
    "opencorporates": {"tokens": 1, "interval": 1.0},   # 1/sec free
    "courtlistener": {"tokens": 10, "interval": 6.0},   # ~100/min free
}

CACHE_TTL = {
    "searxng": 120,          # 2 min
    "exa": 300,              # 5 min
    "opencorporates": 3600,  # 1 hour — company records
    "courtlistener": 3600,   # 1 hour — court cases
}

_buckets = {}
_cache = {}

async def _rate_limited_call(provider, fn, cache_key, *args):
    """Rate-limited, cached provider call with auto-fallback."""
    # Check TTL cache
    if cache_key in _cache:
        age = time.time() - _cache[cache_key]["ts"]
        if age < CACHE_TTL.get(provider, 300):
            return _cache[cache_key]["data"]

    # Token bucket
    now = time.time()
    rl = RATE_LIMITS.get(provider, {"tokens": 5, "interval": 1.0})
    if provider not in _buckets:
        _buckets[provider] = {"tokens": rl["tokens"], "last": now}
    
    b = _buckets[provider]
    elapsed = now - b["last"]
    b["tokens"] = min(rl["tokens"], b["tokens"] + int(elapsed / rl["interval"]) * rl["tokens"])
    b["last"] = now
    
    if b["tokens"] <= 0:
        raise RateLimitedError(f"{provider} rate limited")
    b["tokens"] -= 1

    # Call provider
    result = await fn(*args)
    _cache[cache_key] = {"data": result, "ts": time.time()}
    return result
```

## Fallback Chain Pattern

In each tool function, try providers in priority order, catching rate-limit errors:

```python
async def web_search(query, limit=5):
    for provider, fn in [
        ("searxng", _searxng_search),
        ("exa", _exa_search),
        ("opencorporates", _opencorporates_search),
        ("courtlistener", _courtlistener_search),
        ("firecrawl", _firecrawl_search),
    ]:
        try:
            results = await _rate_limited_call(provider, fn, f"{provider}:{query}", query, limit)
            if results:
                return {"provider": provider, "results": results}
        except RateLimitedError:
            continue  # try next provider
        except Exception:
            continue  # try next provider
    return {"error": "All search providers exhausted"}
```

## Free Public Records APIs for Skip Tracing

| API | Endpoint | Free Tier | Rate Limit | Best For |
|---|---|---|---|---|
| **OpenCorporates** | `api.opencorporates.com/v0.4/companies/search` | Yes | ~1 req/sec | Company registration lookup |
| **CourtListener** | `www.courtlistener.com/api/rest/v3/opinions/` | Yes | ~100 req/min | Federal/state court opinions |
| **USPTO** | `data.uspto.gov/` | Yes | Generous | Trademark/patent search |
| **PACER** (paid) | `pacer.uscourts.gov/` | $0.10/page | Billing-based | Federal court cases, bankruptcies |
| **SEC EDGAR** | `sec.gov/cgi-bin/cik_lookup` | Yes | Generous | Corporate filings |

## Key Design Decisions

- **TTL cache prevents duplicate API calls** for identical queries within the TTL window
- **Token bucket per provider** prevents any single provider from being overwhelmed
- **Rate-limited providers fall through silently** to the next provider in the chain
- **The chain always tries free options before paid ones**
- **No provider failure blocks the user** — they just get results from a different source
