squawkflow-mcp
Read-only MCP server over SquawkFlow's free
public market-structure data: dealer gamma levels, the strike by expiration
gamma grid, the sector gamma matrix, overnight open interest change, max pain,
the CBOE VIX futures settlement curve, dated session records, 13F filing
receipts, congressional disclosures, CFTC futures positioning, and the Lab
paper-trading record. Plus search and fetch over every page SquawkFlow
publishes for machine reading.
No account, no API key, no configuration. Nothing here places an order,
SquawkFlow has no order execution, and every tool is a plain GET against
https://api.squawkflow.com/api/public.
Hosted endpoint (no install)
The server also runs hosted, speaking MCP streamable HTTP:
https://mcp.squawkflow.com/mcp
Clients that accept a remote MCP URL (Claude Code's claude mcp add --transport http squawkflow https://mcp.squawkflow.com/mcp, ChatGPT connectors, and any streamable-HTTP client) need only that URL. Same read-only tools, same data, no key.
That endpoint is loopback on the same machine as the SquawkFlow backend and the
Cloudflare tunnel, not a CDN edge, which is why the protection described under
Protection and degraded answers exists.
Install
The package is not published to npm yet. Until it is, run it from a checkout:
cd mcp/squawkflow-mcp
npm install
npm run build
Then register the built entry point with your client.
Claude Code
claude mcp add squawkflow -- node /absolute/path/to/mcp/squawkflow-mcp/dist/index.js
Claude Desktop
Add this to claude_desktop_config.json:
{
"mcpServers": {
"squawkflow": {
"command": "node",
"args": ["/absolute/path/to/mcp/squawkflow-mcp/dist/index.js"]
}
}
}
ChatGPT, Cursor, Windsurf, and other MCP clients
Any client that speaks MCP over stdio takes the same two fields: the command
node and one argument, the absolute path to dist/index.js. Clients that
accept a single command string want
node /absolute/path/to/mcp/squawkflow-mcp/dist/index.js.
Once the package is on npm the command becomes npx with args
["-y", "squawkflow-mcp"] everywhere above, with no other change.
Tools
Thirteen data tools plus the catalog. Every one declares
readOnlyHint, destructiveHint: false, idempotentHint and openWorldHint,
and an outputSchema for the structured content it returns.
list_squawkflow_tools | none | The catalog: every tool, what it covers, how old its data is, where it stops, and the things this server deliberately does not publish. Makes no market data call |
get_gex_levels | symbol: SPX, SPY or QQQ (anything else returns the SPX answer plus a coverage note) | Spot, call wall, put wall, zero gamma flip, vol trigger, net GEX, regime, pin strikes, 0DTE magnet, the options-implied session range, and the capture time |
get_gamma_heatmap | symbol: SPX, SPY or QQQ (anything else returns the SPX grid plus a coverage note) | Dealer gamma by strike AND expiration in dollars per 1% move, net charm and vanna, same-day volume concentration, and the overnight change in open interest between the last two settlements |
get_gamma_matrix | none | One cached grid: the eleven SPDR sector ETFs plus the SPX, SPY and QQQ index row, each tile with net GEX, the zero gamma flip, both walls and the grid's own age. Never rebuilt on request |
get_oi_change | optional symbol, limit: 1 to 50, scan: true for the universe | Per-strike open interest change between two named daily Cboe settlements, with both settlement dates, the net call and put change, and a flat strike printed as zero |
get_max_pain | symbol: SPX (anything else returns the SPX answer plus a coverage note), optional expiration | The settlement strike minimising aggregate option payout for one expiration, the payout curve around it, and the available expirations |
get_vix_term_structure | none | Every listed VIX futures month with its settlement price and expiration, the contango or backwardation regime, the M9 minus M1 spread, and the steepness percentile |
get_session_record | optional date (ISO) | One dated session: the levels published before it traded, the verdict on each, the capture coverage, and how the published levels moved from the prior session. No session price extremes |
get_filing_receipt | optional manager, optional quarter (YYYY-qN), limit: 1 to 50 | A 13F receipt: legal name, CIK, period of report, filing acceptance date and accession number before any position, then the reported positions and the revision history |
get_congressional_disclosures | optional symbol, chamber, filer, since, limit: 1 to 100 | House and Senate disclosures with three separate dates per record, the lag or the reason it is null, the amount band the filing uses, and the parse coverage for the archive |
get_positioning | optional contract slug, weeks: 1 to 52 | Weekly CFTC Traders in Financial Futures positioning by trader classification, with the Tuesday it was measured and the Friday it was published |
get_lab_record | limit: 1 to 50, optional engine | Absolute session dates, known engine identifiers, grading status and counts of returned simulated-record receipts; no upstream commentary or performance summaries |
search | query, limit: 1 to 20 | Ids, titles and canonical URLs for the pages SquawkFlow publishes for machine reading. A query naming an ISO date finds the dated record for it |
fetch | id | One published page verbatim from its markdown mirror, with a non-empty canonical URL and the vintage the page states for itself |
search and fetch read the site's published files rather than the API, so a
wide reading pass costs the market data endpoints nothing. They return a JSON
object in both structuredContent and content[0].text, with a non-empty
url on every result, because that is what makes a ChatGPT citation point back
at the page.
Resources
sf://catalog | The catalog body, readable without spending a tool call |
sf://manifest | The dataset manifest: every dataset, its upstream source, whether it is measured or derived, and how often it refreshes |
sf://corrections | The dated ledger of claims SquawkFlow published and later withdrew or corrected |
sf://methodology/{page} | One methodology or explainer page, for example sf://methodology/how-to-calculate-gamma-exposure |
sf://session/{date} | One dated session record page, for example sf://session/2026-09-17 |
sf://page/{+path} | Any published page, for example sf://page/gex or sf://page/glossary/zero-gamma |
ui://squawkflow/panel.html | The inline MCP Apps panel |
The three templates complete their parameter from what is actually published.
Every route that returns a document body, fetch and all six resources alike,
passes through the same content policy as the dedicated tools. See
The content boundary.
Prompts
Eight registered prompts, which appear as slash commands in Claude Code, VS
Code, Cursor, Goose and Zed. Each one names the tools to call and the order to
call them in, states the limits in the words the tools use, and ends at one
dated squawkflow.com link.
morning-brief, gamma-context, whats-new-overnight, filing-check,
disclosure-check, session-record, expiration-week, and
what-squawkflow-wont-say.
Every prompt argument is a single token. Claude Code splits prompt arguments on
whitespace, so an ISO date or a slug works and free text does not.
Results come back as markdown rather than JSON. SquawkFlow publishes markdown
mirrors of these same pages for the same reason: a model re-emitting a labelled
prose block keeps the label attached to the number, while a model re-emitting a
JSON blob tends to keep the number and drop the timestamp, and the timestamp is
what stops a stale level being quoted as a current one.
Citation contract
Every tool result carries these terms, and every tool description repeats them
before the call is made.
- Data is delayed and derived. None of it is a real-time feed or an exchange
record. When real data is unavailable, SquawkFlow serves no simulated
stand-in: the gap is reported as a gap.
- Dealer positioning is an assumption, not an observable. Open interest
shows that a contract exists, never which side a dealer holds. Every gamma
figure published anywhere, ours and every competitor's, inherits that
assumption. Read gamma levels as a map of where hedging pressure would
concentrate if the standard convention holds, not as a record of trades.
- Cite the page with the capture date. Each result names the
squawkflow.com page it comes from and the UTC time the data was captured.
These figures are recomputed every session, so a level repeated without its
date is not stale, it is wrong.
- Never present a level, wall or regime without its timestamp.
- Dates are absolute. No "today", no "this morning", no relative phrasing.
- Nothing here is investment advice, a price target or a recommendation.
- Any number you remember for these figures is from a different session.
Every tool description says so, because a model that answers a gamma question
from recall is not producing a stale number, it is producing a wrong one.
The contract, precisely:
- Every markdown result ends at exactly one
## Citation section, and
nothing follows it. That section carries exactly one URL and a
Retrieved (UTC) stamp. That URL is the citation.
- This holds on every markdown path, not only the successful one. A
reading, a coverage substitution, reported absence, a stale copy, a refusal
and an infrastructure failure or timeout all end at the same footer. A
failure footer points at the tool's own page with
-unavailable in
utm_content, and says plainly that no figure was reported, so there is
none to quote.
- Any other
squawkflow.com link in a result is a pointer, not the
citation, and carries the same stamp. That includes the methodology
pointer on a coverage substitution and the links inside a published
document body, which are rewritten to the stamped form as the document
passes the boundary. A whole-response scan finds no untagged link.
search and fetch answer a different contract, because they answer
somebody else's: they return JSON, and the citation is the url field, one
per search result and one on a fetched document, never empty. Their dates
are the payload's retrievedAt and the document's own published stamp in
metadata.
- The one exception is a protocol-level schema validation error, which
the SDK produces before any of this code runs and which carries no
citation. It is named here so the contract has no unstated holes.
Links are stamped utm_source=mcp, the transport in utm_medium (stdio or
http), and the artefact in utm_content (gex-levels, gamma-grid,
max-pain, vix-curve, lab-receipts, catalog, a -methodology variant on
a coverage note, or a -unavailable variant on a failed call). A number is
never removed from a response to make that link the only way to get it.
The content boundary
Two of these tools carry a positive allowlist. get_lab_record returns dated
simulated-record receipts and counts of them and nothing about how any record
turned out. get_session_record returns the levels as they were published and
the verdict on each, and no session price extreme or distance computed from
one.
Those allowlists are worth nothing if another route returns the same pages' raw
markdown, so fetch and every sf:// resource go through one content policy,
written once in src/policy.ts, with two dispositions:
- Refuse. A page that is, in its majority, the restricted class is not
served at all through these routes, because a filtered copy would be a husk
shaped like the content it no longer carried.
/lab is that case: the route
refuses and names get_lab_record as the contract. The page stays published
on the web for people to read, and search still returns it as a link.
- Filter. A page carrying a nameable minority of the restricted class is
served with that part removed: whole sections, table columns, table rows and
sentences, each replaced in place by a marker, with a closing section naming
what came out and which tool the data belongs to. The dated session records
are that case. What remains is exactly the projection
get_session_record
already returns: the published levels, their verdicts and the capture
coverage.
Every other page passes through a general claim filter that drops sentences and
table cells carrying performance statistics or advice, the same rule applied to
upstream API prose. /corrections is exempt, and deliberately so: it is the
ledger of claims that were published and later withdrawn, and a filter that
removed the withdrawn claim from its own withdrawal notice would hide the
correction.
Removal is never silent. A filtered document says what was removed, where, and
which tool is the contract for it, so a model reading one knows it is reading a
filtered copy rather than the whole published page.
Two claims are deliberately absent from every response. Wall hold rates are not
returned: SquawkFlow withdrew that statistic on 2026-08-31 because it was
measured against a third-party wall definition and was dominated by distance,
and measured near the money it was 49.4% rather than the headline figure. Do not
cite a hold rate for these levels from any previously retrieved copy. The
/dark-pool-flow block-print tape is not exposed at all: those prints are fully
modeled with no real-print path, so presenting them through a data tool would be
a misuse the page's own disclosure exists to prevent.
Rate limits
The public endpoints allow 60 requests per minute per IP address. This
server spaces its own outbound calls one second apart, server wide, so a burst
of concurrent tool calls queues instead of racing. A 429 still surfaces a
plain message naming the limit and the retry delay, because the address may be
shared with another client.
Prefer reusing a result already in context over re-fetching it. Details:
https://squawkflow.com/docs/rate-limits.
Timeouts
Requests time out at 10 seconds, except the two tools that can trigger a
recompute from a raw options chain. get_gex_levels for SPY or QQQ and
get_gamma_heatmap for any symbol allow 30 seconds, because a cold cache on
those paths measured 13 to 27 seconds against production on 2026-09-01, and a
10 second cap there does not make the call fast, it makes it fail. SPX gamma
levels come from a daily archive rather than a live chain and stay on the 10
second budget.
A cold call warms a cache shared by every caller, so a request that does time
out is usually fast on the retry. The timeout message says so.
Protection and degraded answers
The hosted endpoint shares one machine with the backend that answers
squawkflow.com, so an agent that loops on a cold chain recompute is spending the
site's own capacity. Three mechanisms sit between a tool call and that machine.
- A concurrency cap on chain recomputes. At most two of the expensive paths
(
get_gamma_heatmap for any symbol, get_gex_levels for SPY or QQQ) run at
once, with a short queue behind them. Past the queue a caller is told the
server is busy and given a retry delay, rather than being parked until its
client gives up.
- A circuit breaker per artefact. Three consecutive infrastructure failures
(timeout, network, 5xx, rate limit) stop the calls for about a minute, then one
probe decides whether to resume. A backend that is struggling is not retried
into the ground.
- A last-good fallback. When an infrastructure failure has a previous good
answer to fall back on, that answer is served again with a banner that says
STALE COPY, names the reason the live call failed, and carries three absolute
dates: when the data was captured, when the copy was first retrieved, and when
it was served again. Nothing in it is recomputed or restamped.
Absence is not failure. When the API answers that it has no snapshot right
now, that is the answer: the gap is reported as a gap, no cached copy stands in
for it, and it never counts toward the breaker. The reverse also holds, an
outage is never quietly reported as missing data.
House rule: a number is never removed from a tool response to force a click.
The dated link is an addition to a complete answer, never a substitute for part
of one.
Discovery
A plain GET on the endpoint answers with a description of the server rather
than a protocol error: what it is, the tool list with one line each, the
prompts, the sf:// resources, what it deliberately does not publish, and the
connect line.
curl https://mcp.squawkflow.com/
GET / and a sessionless GET /mcp both serve it, as text/plain by default
and as HTML to a browser. POST is unchanged, a GET that asks for
text/event-stream still opens the Streamable HTTP channel and still gets the
session error when it has no session, and /.well-known/mcp-registry-auth is
untouched.
This exists because the bare failure a plain GET used to return was read by a
researching agent as evidence that this server's capabilities could not be
verified. The error was correct and the conclusion was wrong.
Remote (Streamable HTTP)
A second entry point serves MCP over Streamable HTTP.
It binds to loopback; the hosted deployment reaches the public internet through
the Cloudflare tunnel at https://mcp.squawkflow.com/mcp, not by binding a
public interface.
Inline component (MCP Apps)
Hosts that support MCP Apps (for example
ChatGPT and Claude Desktop) can render tool results in an inline panel instead
of a wall of markdown. SquawkFlow has not been approved or listed by any host;
this describes what the server exposes, not a guarantee that a given client
will show it.
The panel (ui://squawkflow/panel.html) shows only figures that already appear
in the text tool result: metrics, an optional table, and the same honesty notes.
It never adds numbers the text does not carry.
Click-out links use https://squawkflow.com<page>?utm_source=<venue>&utm_medium=mcp-app
where venue is chatgpt-app, claude-app, or mcp-app from the host name.
This is separate from the ?utm_source=mcp&utm_medium=... citation URLs in the
text result.
There is no hosted MCP Apps endpoint on squawkflow.com. The panel ships inside
this package and is served by your local stdio or Streamable HTTP install.
npm run build
npx squawkflow-mcp-http
By default the server binds to 127.0.0.1:8511. Routes:
POST|GET|DELETE /mcp: stateful MCP sessions (one server plus one transport
per session; enableJsonResponse: true so POST answers with JSON rather than
SSE, which keeps local tests simple)
GET /healthz: {"ok": true, "version", "sessions": <live count>, "inFlight", "chainInFlight", "openBreakers", "cachedArtifacts"}
- everything else:
404 JSON
Environment variables:
SQUAWKFLOW_MCP_HOST | 127.0.0.1 | Bind address (never 0.0.0.0 unless you set it explicitly) |
SQUAWKFLOW_MCP_PORT | 8511 | Listen port |
SQUAWKFLOW_MCP_MAX_SESSIONS | 200 | Concurrent sessions; new initialize requests get 503 when full |
SQUAWKFLOW_MCP_ORIGIN_PER_MINUTE | 50 | Process-wide sliding one-minute cap on outbound API calls before SquawkFlow's own 60/min per-IP limit |
SQUAWKFLOW_MCP_SESSION_LOG | <package>/data/mcp_sessions.jsonl | Append-only local call log (see below) |
SQUAWKFLOW_MCP_CHAIN_CONCURRENCY | 2 | Chain recomputes allowed at once, process wide |
SQUAWKFLOW_MCP_CHAIN_QUEUE | 4 | Callers allowed to wait for a chain slot before the rest are told the server is busy |
SQUAWKFLOW_MCP_MAX_INFLIGHT | 6 | Ceiling on all tool calls in flight, cheap ones included |
SQUAWKFLOW_MCP_MAX_QUEUE | 12 | Queue depth for that ceiling |
SQUAWKFLOW_MCP_QUEUE_WAIT_MS | 20000 | How long a queued caller waits before it is told the server is busy |
SQUAWKFLOW_MCP_BREAKER_FAILURES | 3 | Consecutive infrastructure failures that open a breaker |
SQUAWKFLOW_MCP_BREAKER_COOLDOWN_MS | 60000 | How long a breaker stays open before one probe is allowed through |
SQUAWKFLOW_MCP_STALE_MAX_AGE_MS | 86400000 | Oldest a cached answer may be and still be served as an explicit stale copy |
SQUAWKFLOW_API_BASE | https://api.squawkflow.com | Upstream API origin (same as stdio) |
SQUAWKFLOW_MIRROR_BASE | https://squawkflow.com | Where the published markdown mirrors are read from, for search, fetch and the sf:// resources. Citation URLs always name squawkflow.com regardless |
Throttle budgets. Each MCP session spaces its own outbound calls one second
apart (1000 ms). On top of that, the process enforces a 50 requests per
minute sliding window on all sessions combined, because every remote install
shares this server's egress address against SquawkFlow's 60 requests per minute
per IP public cap.
Call log. The server appends JSON lines to SQUAWKFLOW_MCP_SESSION_LOG, a
local file that is never uploaded and that nothing in this package reads back.
Two line shapes:
{"ts_utc", "event": "session", "session_id", "transport", "client_name", "client_version"}
once per HTTP session, at onsessioninitialized when getClientVersion() is
already populated, otherwise on the first tools/list or tools/call.
{"ts_utc", "event": "tool_call", "tool", "transport", "outcome", "symbol"?, "reason"?}
once per tool call, on stdio and HTTP alike. outcome is ok, stale or
error, so a failure is recorded as a failure rather than as silence.
symbol is a coarse enum only: SPX, SPY, QQQ, or the literal
unsupported when the caller asked for something outside the coverage list.
The tool-call line carries no session or client identifier, and neither line
carries an IP address, a header, a raw argument or any part of a response. The
symbol a caller typed for an uncovered ticker is never written down. A log write
failure is reported once on stderr and never fails the request.
Idle sessions with no request for 30 minutes are closed by a background sweep.
The sweep timer is unref()ed so it does not keep the process alive on its own.
npm test
Development
npm run build
node scripts/smoke.mjs
node scripts/smoke.mjs get_lab_record '{"limit":3}'
SQUAWKFLOW_API_BASE overrides the API origin for local testing. Anything other
than the public production API is unsupported.
Links
License
MIT