Skip to main content
Glama
gabriansa

finviz-screener-mcp

by gabriansa
README.md
# finviz-screener-mcp

FastMCP server that exposes **only** Finviz’s stock screener via [`finvizfinance`](https://pypi.org/project/finvizfinance/).

**US-only:** every `screen()` call hardcodes `Country=USA`. No news, maps, insider trading, or Elite API — just screener filters → structured JSON.

## Tools

| Tool | Purpose |
|------|---------|
| `list_filters` | Progressive discovery (index / one category / one section / search) — avoids dumping ~1694 values |
| `screen` | Compact screener results (default limit 25, max 100; offset capped at 400; null fields and the always-USA `Country` column stripped) |

Both tools are annotated read-only (`readOnlyHint`) so clients can skip confirmation prompts. Invalid input and upstream failures are raised as MCP tool errors (`isError: true`) with messages like `[invalid_filter] ...` or `[upstream] ...`, so agents can self-correct.

**Keep agent context small**

```text
list_filters()                         → category names + counts only
list_filters(category="Sector")        → values for Sector only
list_filters(section="signals")        → signals only
list_filters(query="rsi")              → matching names
screen(..., limit=15, table="custom", columns=[...])  → only needed fields
```

Example `screen` call:

```json
{
  "filters": {
    "Sector": "Technology",
    "Market Cap.": "Large ($10bln to $200bln)"
  },
  "table": "custom",
  "order": "Market Cap.",
  "ascend": false,
  "columns": ["Ticker", "Market Cap.", "P/E", "RSI", "Price"],
  "limit": 15
}
```

## Important caveats

- **Unofficial scrape.** `finvizfinance` parses the free Finviz HTML screener. There is no free official API; Elite’s CSV/API export is a separate paid surface and is **not** used here.
- Quotes on the free site are **delayed**.
- Scraping can break if Finviz changes its DOM, and may be against Finviz Terms of Service. Use responsibly.

## Proxy fallback (for hosted/cloud IPs)

Datacenter hosts (e.g. Horizon) are often IP-blocked by Finviz (`screen` → HTTP 403), even though the same code works fine from a home connection. `screen()` handles this automatically:

1. Reuse the proxy that served the previous request, if there is one — no probing needed.
2. Try the direct connection (the only path taken locally). Once it fails, it's skipped for `FINVIZ_DIRECT_COOLDOWN` seconds (default 600) so a blocked host stops paying the timeout on every call, then retried in case the block was temporary.
3. Rotate through free proxies from a handful of actively-maintained GitHub lists (`proxy_pool.py`), pulling fresh lists as older ones are used up, until a request lands or `FINVIZ_FETCH_BUDGET` seconds (default 45) elapse.

Most free proxies are dead, so trying them one at a time would spend a full connect timeout per corpse. Instead the pool probes candidates **concurrently against Finviz itself** (40 at a time) and only hands back ones that answered HTTP 200 — which also screens out proxies Finviz has already blocked. In practice a cold rotation finds a working route in ~15s and subsequent calls reuse it in ~5s.

No configuration needed. This is still best-effort — free proxies are inherently unreliable. For a guaranteed hosted fix, point `finvizfinance.util.set_proxy` at a paid residential proxy instead (datacenter proxies tend to get 403'd same as the host itself).

## Local setup

Requires Python 3.11+ (3.12 recommended; see `.python-version`).

```bash
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -r requirements.txt
```

Run the server (stdio):

```bash
fastmcp run main.py:mcp
# or
python main.py
```

Inspect tools:

```bash
fastmcp inspect main.py:mcp
```

### Cursor / Claude Desktop (stdio)

```json
{
  "mcpServers": {
    "finviz-screener": {
      "command": "uv",
      "args": ["run", "--with-requirements", "requirements.txt", "fastmcp", "run", "main.py:mcp"],
      "cwd": "/absolute/path/to/finviz-screener-mcp"
    }
  }
}
```

Or with an activated venv:

```json
{
  "mcpServers": {
    "finviz-screener": {
      "command": "/absolute/path/to/finviz-screener-mcp/.venv/bin/fastmcp",
      "args": ["run", "main.py:mcp"],
      "cwd": "/absolute/path/to/finviz-screener-mcp"
    }
  }
}
```

No API key or `.env` is required.

## Deploy on Prefect Horizon (FastMCP Cloud)

Horizon is the managed host from the FastMCP team ([docs](https://gofastmcp.com/deployment/fastmcp-cloud); console: [horizon.prefect.io](https://horizon.prefect.io)). URLs look like `https://{name}.fastmcp.app/mcp`.

1. Push this repo to GitHub.
2. Sign in at [horizon.prefect.io](https://horizon.prefect.io) with GitHub and select the repo.
3. Configure:
   - **Entrypoint:** `main.py:mcp`
   - **Dependencies:** `requirements.txt` (auto-detected)
4. Deploy. Horizon ignores `if __name__ == "__main__"` and installs deps from `requirements.txt`.
5. Connect clients to `https://your-server-name.fastmcp.app/mcp`.

No environment variables or secrets are required for this server. If you add any later: **Settings → Environment Variables**, mark sensitive, then **rebuild and redeploy** (env changes do not hot-reload).

## Smoke check

```bash
python -c "
from main import list_filters, screen
print([c['name'] for c in list_filters()['categories'][:5]])
print(screen(filters={'Sector': 'Technology'}, limit=3)['count'])
"
```