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.
[](https://opensource.org/licenses/MIT)
[](https://www.python.org/downloads/)
[](https://modelcontextprotocol.com/)
[](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).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues