finviz-mcp
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
finviz-mcp
Read-only MCP server for finviz.com US market data, with a hard robots.txt compliance guard in front of every request.
14 tools, stdio transport, no API key required. Built for agents that need US fundamentals and market context next to an Indonesian-market data stack.
python finviz_cli.py list # every tool, one line each
python finviz_cli.py quote ticker=AAPL
python finviz_cli.py quote_batch tickers=AAPL,MSFT,JPM preset=valuation
python finviz_cli.py group_performance group=sectorWhy it exists
The obvious Finviz tool is the screener — and the docs used to say it was unusable. That was
wrong, and the correction is the most useful thing in this repo. Every /screener?... URL
does redirect to /screener/stocks?..., which finviz's own robots.txt Disallows for generic
clients — so custom filters still require an explicit opt-in. But the allow-listed signal
URLs (v=340, v=320, v=210) work, and the "JavaScript-rendered, zero rows" claim was this
server's own parsing gap: those pages ship their full result set inside a
<script type="application/json"> blob at data.rows[], not as an HTML <table>. Reading
that blob yields typed numbers and every row on the page (f=industry_shellcompanies returns
all 328 shell companies, 20 per page, with marketCap/price as floats, and v=121/v=161
give valuation and balance-sheet fields). Use screener_signal for the allow-listed signals
and custom_screener with allow_noncompliant=True for arbitrary filters. For fundamentals
on a list of tickers you already have, quote_batch is still cheaper and fully compliant.
Related MCP server: Finviz MCP Server
Tools (16)
Tool | Source | Robots | Notes |
|
| OK | reachability + robots state + tool count |
|
| OK | explains allow/deny for any path |
|
| OK | ~83 fundamentals/technicals for one ticker |
|
| OK | fundamentals comparison for a known ticker list, 1..40 tickers |
|
| OK | 13–14 homepage mover blocks (gainers, new high, overbought, …) |
|
| OK | breadth, futures, forex/rates/crypto, headlines, calendar |
|
| OK | headlines + source + url |
|
| OK | Form-4 rows; server-side filters (ticker, txn, min value, 10% owner), newest-first, explicit coverage block |
|
| OK | JSON, actual/forecast/previous, windowed |
|
| OK | EPS estimate vs actual, surprise, 1-day price reaction |
|
| OK | ex-date, ordinary/special, yield |
|
| OK | 11 sectors / 148 industries: stocks, cap, div, P/E, chg %, volume |
|
| OK | allow-listed signals only; rows via embedded JSON |
|
| opt-in | arbitrary filters, one page; needs |
|
| opt-in | catalogue of 23 groups / 828 valid |
|
| opt-in | paginated fetch, |
Calendars: dateTo is a lie, weekdays are scanned one by one
Verified 2026-10-31 against all three /api/calendar/* endpoints:
Request | Response |
| every row dated 2026-11-02 ( |
| 0 rows (4 Nov alone holds 388 names across 8 pages) |
| 6..13 Nov — a fixed 7-day window ending on that week's Friday |
So earnings_calendar / dividends_calendar ask one weekday at a time and
economic_calendar walks week-by-week via each week's Friday, then filters to the window
requested. Consequences worth knowing:
A ticker can sit on page 2+ of a busy day (NVO on 2026-11-04 is page 2 of 8). Default is 2 pages/day ≈ 100 rows; raise
max_pages_per_dayand readcounts.rows_truncated_by_page_cap—rows: []withtruncation_is_possible: truemeans not scanned, not not scheduled.counts.coverage_completesays whether every weekday in the range was scanned.Saturday/Sunday are never scanned; that is also why the old default window returned nothing every weekend.
economic_calendarreportsweeks_fetched(1..26) so the shape of the walk is visible.
Reading screener rows without a 1e6 error
Free-tier finviz reports marketCap in millions of USD — the raw float is
48499.35 for a $48.5B company. Both screener tools therefore also emit
marketCap_usd and a _units legend (marketCap, price, prevClose, change,
volume), while leaving the original keys untouched. Measured against quote():
screener 5149206.09 == quote 5149.21B.
screener_all stops on its own rather than trusting the page count: offsets past the
end clamp to the last page (r=341/401/981 all return first_row=320 with the
same 8 rows) and r>=1001 is HTTP 403, so a naive loop would spend its entire budget
re-reading one page. Verified full pull: f=industry_shellcompanies → 17 pages, 328
unique tickers (== totalCount), 0 duplicates, 28.7 s.
quote_batch — the screener replacement
_FUNDA_PRESETS: valuation (P/E, P/S, P/B, P/C, P/FCF, PEG, EV/, Price/),
profitability (EPS, ROE, ROA, ROI, Margin, Sales), growth (Growth, EPS this Y,
EPS next Y, EPS next 5Y, EPS Q/Q, Sales Q/Q), balance (Debt, Current Ratio, Quick Ratio,
Book/sh, Cash/sh), plus all. extra_metrics adds exact labels; include_header adds
sector/industry/country per ticker.
Preset matching is by label keyword, not exact string, so a small label drift on Finviz degrades to a missing column instead of a silently empty one. Measured: 3 tickers in 2.8 s at the default 1 s rate limit, 10 columns returned.
quote_batch does not discover tickers. If you do not already know the list, this server
cannot help you, and any tool here that pretends otherwise is lying.
Insider trades — filter server-side or lose rows
insider_trades used to filter client-side over whatever the default page held, and that
produced silently wrong answers: ticker="NVDA" returned 0 rows (NVDA simply was not in
the 200-row global page) while ticker="AAPL" returned 1 of a real 200. The fix pushes every
filter into finviz's own query params — t (ticker), tc (transaction type), tv (min value),
or (owner type), o (sort).
Query-param semantics, all verified on 2026-10-30:
Param | Values | Note |
| one ticker | comma list → 0 rows; dots must be dashes ( |
| 1=buy, 2=sale, 3=buy+sale, 4=option, 5=buy+option, 6=sale+option, 7=all |
|
| integer USD | combines with |
| 10 = 10% owners only, -10 = everyone except them | 10% owners are rare per-ticker, so |
|
| default finviz order is NOT newest-first; |
Coverage ceiling. The page is one table of at most 200 rows and ignores pagination —
p=1, p=2, p=3 and r=201 all return the identical 200 rows (byte-identical hash). So
only the newest ~200 matching rows exist. The tool reports this instead of hiding it:
coverage.page_rows_total, rows_matching_filters, truncated_by_limit, page_is_full, and
window.{newest,oldest_on_page}. Sorting by value reaches far older rows (t=NVDA&o=-transactionValue
spans 2017→2026) than sorting by date (2025→2026) — the 200-row cut is by sort order, so a
"latest transactions" query genuinely only sees recent history for busy tickers.
An empty result for a ticker now distinguishes the two causes: the tool probes /stock?t=
and reports coverage.ticker_exists — a typo returns ticker_exists: false, a real ticker with
no matching rows returns true.
Calendar pagination
The internal JSON APIs return 50 rows per page and ignore pageSize (verified).
_calendar_collect() merges up to 3 pages (150 rows) before applying filters, because
row-level filters over a single page silently miss matches on later pages. Verified effect:
min_yield=8 on dividends went from 4 rows to 22. Page counts are reported back as
counts.pages_fetched and counts.raw_items_considered.
Known-unavailable endpoints — do not re-probe
/api/quoteresponds but always returns400 {"error":"invalid quote query"}for 16 parameter shapes tried./map.ashx(heatmap) renders via a JS bundle; the data is not in the HTML./etf,/earnings,/insider(without.ashx) → 404.
Unexplained behaviour
A wider date range can return fewer rows (1–30 Sep → 17 items; 8–11 Sep → 18 items). The endpoint appears to window or offset results itself. Date ranges are not fully deterministic — do not build anything that depends on strict monotonicity.
Compliance model
robots.txt is fetched live (cached 24 h) and evaluated with a correct longest-match matcher
(* wildcard, $ anchor, Allow wins ties). Every request is checked before sending
and again against the final URL after redirects — that second check is what catches the
/screener?* → /screener/stocks?* trap.
Verified policy map:
Path | Verdict |
| allowed (no matching rule) |
| allowed — and usable (rows in embedded JSON) |
| Disallow: /screener?* and |
| Disallowed (and paid) |
screener_signal runs without any override for the allow-listed signals — an earlier
version hardcoded v=111 and so denied itself; if a signal URL is not Allow-listed it still
refuses. custom_screener needs allow_noncompliant=True because arbitrary f= filters
302-redirect into /screener/stocks?*; the response is then flagged robots_compliant: false.
FINVIZ_ALLOW_NONCOMPLIANT=1 flips the default globally; that is your call to make, not this
server's.
Three stdlib robots.txt traps this server works around
Do not "simplify" this code away:
RobotFileParserstoresUser-agent: *rules inrp.default_entry, not inrp.entries. Reading onlyentriessilently allows every request.RuleLine.pathis percent-quoted, so/screener?*is stored as/screener%3F%2A; it must beunquote()d before matching or the rule never fires.Python's built-in
can_fetchtreats*literally, so it also gets/screener?*wrong. This server does its own longest-match evaluation.
Install
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txtRegister with Hermes (or any MCP client that speaks stdio):
mcp_servers:
finviz:
command: /path/to/.venv/bin/python
args: ["/path/to/finviz_mcp_server.py"]
trust: full
timeout: 120
env:
FINVIZ_MIN_INTERVAL: "1.0"
FINVIZ_CACHE_TTL: "60"No env block is required — every variable has a default. The server never reads secrets, because it has none.
Usage
Via an MCP client: finviz_quote(ticker="AAPL"), finviz_economic_calendar(min_importance=3).
Without a gateway restart, use the CLI (it speaks JSON-RPC over stdio to the same server):
python finviz_cli.py list
python finviz_cli.py quote ticker=AAPL
python finviz_cli.py quote_batch tickers=AAPL,MSFT,JPM preset=all
python finviz_cli.py group_performance group=industry
python finviz_cli.py economic_calendar min_importance=3
python finviz_cli.py screener_signal signal=topgainers allow_noncompliant=trueArgument coercion follows the tool's declared schema, not punctuation: tickers=AAPL,MSFT
becomes an array because the parameter is list[str], while
filters=fa_div_o6,fa_pe_u20 stays one string because the parameter is str. Getting this
wrong is the most common way to break the CLI, so it is handled centrally in coerce_typed().
Runtime test scripts
Script | What it proves |
| real MCP SDK client: initialize, |
| tool-by-tool functional pass |
| the three JSON-API calendar tools + regression |
| calendar row shapes and pagination counters |
| all older tools after a change; asserts the two screener tools fail for the right reason |
They locate the server and interpreter relative to themselves (HERE), and accept overrides:
FINVIZ_PY, FINVIZ_SERVER, FINVIZ_ENV_FILE, FINVIZ_STDERR_LOG. FINVIZ_ENV_FILE is
optional — without it they inherit os.environ, so they work outside any agent framework.
Tuning
Env var | Default | Meaning |
|
| seconds between requests — deliberate, do not lower |
|
| response cache seconds |
|
| robots.txt cache seconds |
|
| per-request timeout |
| see source | override UA |
|
|
|
Limitations / risks
US markets only. No IDX coverage; use a separate source for Indonesian data.
Free tier is delayed, not real-time. Real-time + export is Finviz Elite.
Finviz is migrating to React-rendered pages (
v=340). Expect breakage on their next view change.Scraping at volume risks IP blocks. The 1 s limiter is deliberate.
Data is external and unverified: label outputs
partially verified, never authoritative.quote_batchcosts one request per ticker at 1 s each — 40 tickers is ~40 s. Batch size is capped for that reason, not by an API limit.Calendar date ranges are not fully deterministic (see above).
License
MIT — see LICENSE. Not affiliated with Finviz.
Educational/analytical use only. Not investment advice.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Resolves all 10,000+ US public stock tickers to SEC CIK numbers, recent 10-K/10-Q/8-K filings, an.
US company financial statements, insider trades, 13F funds and short interest, each with its filing
SEC filings and financials with accession links, insider and congress trades, macro. OAuth or key.
Stocks, ETFs, crypto, commodities, forex: quotes, fundamentals, SEC filings, point-in-time history.
Related MCP Servers
- AlicenseAqualityDmaintenanceProvides access to historical stock market data from US, UK, Russian, Turkish, and Hong Kong exchanges, including company profiles, market metrics, sector performance, rankings, and visualization data for financial analysis.99 npmLGPL 3.0
- AlicenseNot gradedqualityDmaintenanceProvides comprehensive stock screening and fundamental analysis capabilities using Finviz data, including earnings tracking, volume surge detection, technical indicators, dividend growth screening, and SEC filing retrieval.62MIT

akyla-mcpofficial
AlicenseAqualityDmaintenanceProvides as-reported US equity fundamentals, live quotes, financial statements, valuation comps, and a screener from SEC filings, with per-cell filing provenance for citations.726 PyPIMIT- FlicenseNot gradedqualityDmaintenanceEnables users to fetch real-time stock fundamentals, news, price history, and perform screener searches using Finviz and Yahoo Finance data without API keys. Note: screener functionality works only locally due to IP restrictions.-