Skip to main content
Glama
Finviz MCP

Finviz MCP

README.md
<!-- mcp-name: io.github.giraygokirmak/finviz-mcp -->
# Finviz MCP — Market Data, Screeners & Fundamentals for AI Assistants

<p align="center">
  <sub>Quotes, the 84-field fundamentals snapshot, screeners, headlines with price reactions, SEC filings, insider and institutional ownership, short interest, earnings, dilution risk, options chains, why-is-it-moving explainers, market indices and rendered chart PNGs — <b>17 tools</b>, one server.</sub>
</p>

**Finviz MCP server** gives Claude, ChatGPT, Cursor, Copilot and any MCP client a single, structured way to read everything finviz.com publishes, without a browser, an API key, or a Finviz account.

Every tool is read-only, every failure is a typed error envelope, and every payload carries its own `source` URL so an answer can be traced back to the page it came from.

> [!NOTE]
> Independent open-source project — **not affiliated with, endorsed by, or associated with Finviz**. "Finviz" is a trademark of its respective owner. This server reads publicly reachable, server-rendered pages and the site's own unauthenticated JSON routes; it does not log in, does not use a Finviz account, and does not bypass the Elite paywall (Elite-gated responses are reported as `ELITE_ONLY`, never faked).

> [!IMPORTANT]
> **Not financial advice.** Nothing produced by this software is investment, financial, legal, tax, or accounting advice. Market data may be delayed, inaccurate, or incomplete and is provided without warranty. Do your own research.

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python 3.10-3.13](https://img.shields.io/badge/python-3.10--3.13-blue.svg)](https://www.python.org/downloads/)
[![MCP Ready](https://img.shields.io/badge/MCP-Ready-brightgreen)](https://modelcontextprotocol.com/)
[![Version](https://img.shields.io/badge/version-v0.1.0-blue)](https://github.com/giraygokirmak/finviz-mcp/releases)

---

## Why this server

Most "Finviz scrapers" stop at the quote header. This one covers the whole page — and, crucially, documents **which upstream route each field comes from**, because finviz has no official API:

| Question | Tool | Upstream source |
|---|---|---|
| What is VCIG trading at, and what do its 84 fundamentals say? | `finviz_quote` | `GET /stock?t=VCIG` (HTML) |
| Just the fundamentals, small payload | `finviz_snapshot` | same page, `table.js-snapshot-table` |
| Who are its peers? | `finviz_peers` | same page, Peers row |
| Screen the market | `finviz_screener` | `GET /screener/stocks` (SSR island) |
| Which filters exist? | `finviz_screener_filters` | `GET /api/screener-filters?type=stocks` |
| What moved it today? | `finviz_news`, `finviz_why_moving` | `/api/news/quote/{t}`, `/api/stocks-why-moving/{t}` |
| What did it file? | `finviz_filings` | `GET /api/quote/filings` |
| Who bought or sold? | `finviz_insider_trading`, `finviz_institutional_ownership` | `/stock?t={t}` HTML + JSON islands |
| How crowded is the short? | `finviz_short_interest` | `GET /stock?t={t}&ty=si` |
| Earnings history and revisions | `finviz_earnings` | `GET /stock?t={t}&ty=ea` |
| Can it fund itself? | `finviz_dilution` | `GET /stock?t={t}&ty=di` |
| Options chain with greeks | `finviz_options` | `GET /stock?t={t}&ty=oc` |
| A chart image | `finviz_chart` | `charts2-node.finviz.com/chart` (PNG) |
| What is the market doing? | `finviz_market_indices` | `GET /api/indices_data` |

---

## Architecture

```
src/finviz_mcp/
├── server.py                  # MCP routing layer — validation, delegation, error envelope
└── core/
    ├── errors.py              # ErrorCode enum, make_error(), FinvizServiceError
    ├── utils/validators.py    # ticker / filter / order / view / period / scale allow-lists
    └── services/
        ├── finviz_client.py   # httpx transport, TTL cache + stale fallback, retry, CF/Elite detection
        ├── proxy_manager.py   # optional rotating residential proxy (env only)
        ├── html_utils.py      # every HTML → dict parser (pure, synchronous)
        ├── quote_service.py   # quote / snapshot / peers / short interest / earnings / dilution
        ├── screener_service.py# SSR island parsing + filter catalogue
        ├── news_service.py    # /api/news/quote rows + reactions
        ├── filings_service.py # /api/quote/filings paging + form filter
        ├── insider_service.py # insider table + institutional ownership islands
        ├── market_service.py  # indices, why-moving, options chain
        └── chart_service.py   # charts2-node URL builder + PNG download
```

No business logic lives in `server.py`. Everything is testable without an MCP client, and the parsers are tested against **real captured markup**, not hand-written HTML.

---

## Quick start

### Run from source with uv (recommended)

```bash
git clone https://github.com/giraygokirmak/finviz-mcp.git
cd finviz-mcp
uv sync --dev
uv run finviz-mcp            # stdio (what MCP clients launch)
```

### Install as a package

```bash
pip install finviz-mcp-server
finviz-mcp                   # stdio
```

### Claude Desktop

`claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "finviz": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/to/finviz-mcp", "run", "finviz-mcp"]
    }
  }
}
```

Or, if you installed the package, just:

```json
{
  "mcpServers": {
    "finviz": { "command": "finviz-mcp" }
  }
}
```

### Docker / Docker Compose

```bash
docker compose up -d --build
```

The container listens on **port 8000 internally, published as `6007`** (change the left-hand side of the port mapping if that collides on your host):

```yaml
ports:
  - "6007:8000"
```

Point any streamable-http MCP client at `http://localhost:6007/mcp`. A liveness probe is served at `http://localhost:6007/health` (used by the image's `HEALTHCHECK`).

Chart PNGs are written to `FINVIZ_CHART_DIR` (`/home/mcpuser/.finviz-mcp/charts` in the image) and persisted by the `finviz-charts` named volume, so `finviz_chart` keeps returning real file paths across restarts.

Run it directly without Compose:

```bash
docker build -t finviz-mcp:latest .
docker run --rm -p 6007:8000 -v finviz-charts:/home/mcpuser/.finviz-mcp/charts finviz-mcp:latest
```

### Streamable HTTP from source

```bash
uv run finviz-mcp streamable-http --host 0.0.0.0 --port 6007
# MCP:    http://localhost:6007/mcp
# health: http://localhost:6007/health
```

---

## The 17 tools

### Quote & fundamentals

| Tool | What it returns |
|---|---|
| `finviz_quote(ticker)` | Last close and after-hours price with change, sector/industry/country/cap class/exchange, the full 84-metric snapshot (`snapshot`, `snapshot_numeric`, `snapshot_pairs`, `descriptions`), company profile, peers |
| `finviz_snapshot(ticker)` | The 84 metrics only — raw strings + coerced numbers + finviz's own metric descriptions |
| `finviz_peers(ticker, limit=12)` | The page's real peer row: ticker, company, industry, country, market cap |

### Screening

| Tool | What it returns |
|---|---|
| `finviz_screener(tickers, filters, order, view, page, columns)` | One page of results (20 rows upstream), `total_count`, `total_pages`, `selected_columns`, plus the `applied` filter echo |
| `finviz_screener_filters()` | Every filter key with its tab, UI type, title and option values (cached 1h) |

### News & events

| Tool | What it returns |
|---|---|
| `finviz_news(ticker, limit=30, include_reactions)` | Up to 100 newest-first headlines with source, URL, day-of-headline move and next-session move |
| `finviz_filings(ticker, page, page_size, forms, sort)` | SEC filings with accession numbers, report/filing dates and direct SEC links |
| `finviz_earnings(ticker)` | Next earnings date, last 12 quarters (actual vs estimate), annual history, estimate revisions, price reactions, fundamental series |
| `finviz_why_moving(ticker)` | Finviz's own explainer: headline, markdown bullet summary, sentiment, catalyst flag |

### Ownership & risk

| Tool | What it returns |
|---|---|
| `finviz_insider_trading(ticker, limit=40)` | Form-4 rows (insider, relationship, date, buy/sale, cost, shares, value, resulting holdings, SEC link) + 12-month aggregated buy/sell series |
| `finviz_institutional_ownership(ticker, limit=20)` | Managers and funds with `percOwnership` as both fraction and percent |
| `finviz_short_interest(ticker, limit=60)` | Bi-weekly FINRA settlement series + latest reading + computed short-float % |
| `finviz_dilution(ticker)` | Dilution model (share count, float, 60-day high, baby-shelf threshold, cash runway, months of cash) — with an income-statement fallback for names finviz does not flag |
| `finviz_options(ticker, expiry)` | Chain for one expiry: strike, bid/ask, IV, greeks, open interest, volume |

### Charts, market & ops

| Tool | What it returns |
|---|---|
| `finviz_chart(ticker, period, chart_type, scale, width, height, technicals)` | Downloads a PNG to `FINVIZ_CHART_DIR` and returns its absolute path, size and provenance |
| `finviz_market_indices()` | DJI / COMP / SPX / IWM: last, change, OHLC and the intraday series |
| `finviz_health()` | Version, cache occupancy, upstream reachability |

### Try these first

```text
Give me the full Finviz picture on VCIG
Screen technology stocks above $2B, cheapest P/E first
What are the last 10 SEC filings for AAPL, 8-Ks only?
Which insiders bought SNDL this year?
Chart NVDA weekly, 1200x600, with SMAs
```

---

## Upstream data sources (what the server actually calls)

Finviz publishes **no documented API**. Everything below was reverse-engineered from the rendered pages and the site's own React islands, and is what this server consumes:

**JSON routes** (undocumented, unversioned, unauthenticated)

```
GET /api/quote/filings?t={t}&page={n}&pageSize={n}&o={sort}&f={forms}
GET /api/news/quote/{t}                    → {"html": "<news rows>", "timestamp": "..."}
GET /api/stocks-why-moving/{t}             → headline, summary, sentiment, catalyst
GET /api/indices_data                      → DJI, COMP, SPX, IWM intraday OHLCV
GET /api/screener-filters?type=stocks      → 83 filters with options
```

**Document routes** (server-rendered HTML; the interesting data lives in `<script id="…-init-data">` islands)

```
GET /stock?t={t}&ta=1&p=d                  → header, 84-field snapshot, profile, peers,
                                             insider table, insider-init-data-0,
                                             institutional-ownership-init-data-0
GET /stock?t={t}&ty=si&ta=1&p=d            → route-init-data: short-interest series
GET /stock?t={t}&ty=ea&ta=1&p=d            → route-init-data: earnings + revisions
                                             fa-init-data-0: EPS/sales series
GET /stock?t={t}&ty=oc&ta=1&p=d[&e=YYYY-MM-DD] → route-init-data: options chain
GET /stock?t={t}&ty=lf&ta=1&p=d            → route-init-data: same shape as /api/quote/filings
GET /stock?t={t}&ty=di&ta=1&p=d            → route-init-data: dilution model (schema_version 1)
GET /screener/stocks?v={view}&f={filters}&o={order}&r={offset}&c={columns}
                                           → <script data-ssr-props="ssr-island-1"> JSON
```

**Binary**

```
GET https://charts2-node.finviz.com/chart?w=&h=&bw=2&bm=1&bb=1&t={t}&tf=d|w|m
    &s=linear|logarithmic&pm=0&am=0&ct=candle_stick|line_chart|heikin_ashi
    &o[0][ot]=sma&o[0][op]=20&o[0][oc]=DC32B363 …
```

Notes that shaped the implementation:

- **`/api/quote` does not work.** Every parameter spelling returns HTTP 400 `{"error":"invalid quote query"}`, so the quote is necessarily HTML-scraped.
- **`/api/related-tickers` is a trap.** It ignores its ticker argument and always returns the same mega-cap list, so peers come from the page's Peers row instead.
- **`/api/suggestions` ignores its query** and returns a fixed list — not used.
- The screener island is the whole result page: `data.rows[].text|numbers`, `totalCount`, `firstRow`, `pageSize`. Pagination is the 0-based `r=` offset; upstream caps at 20 rows per page regardless of `pageSize`.
- `c=` is a *count-like* column selector: `c=0,1,…,N` progressively adds numeric fields up to 67 in finviz's fixed order.
- Chart params: `s=log` and unknown `ct=` values return HTTP 400 `Invalid request` — validated client-side instead of forwarded blindly.
- A real Chrome User-Agent is required; a bare `python-httpx/x` UA gets a Cloudflare interstitial. No browser is needed (verified with plain `urllib`).

The full field-level reference — page anatomy, the 84 snapshot labels in page order, every
island's shape, the screener row schema, decoded chart parameters and the dead routes — is
in **[docs/FINVIZ_API_MAP.md](docs/FINVIZ_API_MAP.md)**.

**Elite-gated (reported as `ELITE_ONLY`, never faked):** `/api/compare-perf/*`, `/api/ticker-flags*`, `/api/watchlists*`, `/export/quote`, and full 13F ownership history.

---

## Error envelope

Every tool returns its normal payload on success and this shape on failure — never a raw exception string:

```json
{
  "error": {
    "code": "NO_DATA",
    "message": "finviz.com lists no option contracts for VCIG",
    "ticker": "VCIG",
    "retryable": false
  }
}
```

Codes: `SYMBOL_NOT_FOUND`, `INVALID_TICKER`, `INVALID_PARAMETER`, `UPSTREAM_RATE_LIMIT`, `UPSTREAM_TIMEOUT`, `UPSTREAM_BLOCKED`, `UPSTREAM_ERROR`, `ELITE_ONLY`, `NO_DATA`, `PARTIAL_DATA`, `PARSE_ERROR`, `DEPENDENCY_MISSING`, `INTERNAL_ERROR`.

`retryable` tells the client whether repeating the identical call could plausibly work (rate limit, timeout, Cloudflare challenge) or not (bad ticker, unsupported parameter, no such tab). Branch on `code`; show `message` to the user.

---

## Configuration

All settings are optional environment variables (see `.env.example`):

| Variable | Default | Purpose |
|---|---|---|
| `PORT` / `HOST` | `8000` / `127.0.0.1` | Streamable-http bind (CLI flags win) |
| `FINVIZ_TIMEOUT` | `25` | Upstream request timeout, seconds |
| `FINVIZ_CACHE_TTL` | `30` | Page/JSON cache lifetime, seconds |
| `FINVIZ_NEWS_TTL` | `60` | Headline cache lifetime, seconds |
| `FINVIZ_MAX_RETRIES` | `2` | Retries for 429 / 5xx / timeouts (4xx never retried) |
| `FINVIZ_CHART_DIR` | `~/.finviz-mcp/charts` | Where `finviz_chart` writes PNGs |
| `FINVIZ_USER_AGENT` | bundled Chrome UA | Override only if finviz starts challenging it |
| `PROXY_ENABLED` + `PROXY_*` | off | Optional rotating residential proxy for rate-limited egress |

The 30-second cache is a correctness feature, not just an optimisation: one `finviz_quote` fans out over a ~240 KB page, and quote + snapshot + peers would otherwise re-download it three times. Stale entries are served when upstream fails, so a rate-limited client still gets numbers.

---

## Development

```bash
uv sync --dev
uv run pytest tests/unit -q      # 87 tests, hermetic (no network)
```

CI never depends on finviz.com being reachable. To exercise the real site end to end
(every service, both a small cap and a large cap):

```bash
uv run python tools/smoke_live.py VCIG
uv run python tools/smoke_live.py AAPL
```

Fixtures under `tests/fixtures/` are **excerpts of real finviz markup** captured during development, regenerated with `tools/make_fixtures.py`; the parsers are therefore exercised against actual tag soup rather than an idealised version of it.

CI: `test.yml` runs the hermetic suite on Python 3.10–3.13; `publish-image.yml` re-runs it and then publishes a multi-arch (`amd64` + `arm64`) image to GHCR on `main` and on `v*` tags.

---

## Limitations

- **Scraping is inherently fragile.** A finviz redesign breaks the parsers in `html_utils.py` — one file, by design. `PARSE_ERROR` means "markup changed", not "retry".
- **Rate limiting is real.** The cache and the retry/backoff layer are polite by default; do not point a loop at thousands of tickers without pacing.
- **Elite features are not available.** Ownership history beyond the two islands, exports and watchlists need a Finviz subscription; this server says so instead of guessing.
- **Foreign private issuers** (6-K filers) often have no quarterly earnings rows, no insider table and no short-interest series. That returns `NO_DATA` with an explanation rather than an empty success.
- **No candle-level JSON.** Finviz renders charts as PNGs only, so `finviz_chart` returns an image path, not a series.

---

## License

MIT — see [LICENSE](LICENSE).