Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Install
npx -y @cyanheads/secedgar-mcp-serverEDGAR_USER_AGENTrequired — User-Agent header for SEC compliance. Format: "AppName contact@email.com".EDGAR_RATE_LIMIT_COOLDOWN_SECONDSoptional — Seconds to stop sending to SEC after a 429: calls are refused locally, then one probe request goes out.MCP_LOG_LEVELoptional — Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').
@cyanheads/secedgar-mcp-server
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Public Hosted Server: https://secedgar.caseyjhand.com/mcp
Overview
SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only the User-Agent header SEC requires. Resolve companies by ticker, name, or CIK, search filings back to 1993, pull XBRL financials and cross-company comparisons by concept, and trace ownership through insider transactions, 13F holdings, 13D/13G blockholders, and fund portfolios. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
| Tool | Description |
|---|---|
secedgar_company_search |
Find companies and retrieve entity info with optional recent filings |
secedgar_search_filings |
Search EDGAR filings since 1993: full text from 2001, archive browse before that |
secedgar_get_filing |
Fetch a filing's metadata and document text, paged or by section |
secedgar_get_financials |
Get historical XBRL financial data for one company and concept |
secedgar_get_snapshot |
One-call financial profile: the latest value of every supported concept |
secedgar_get_material_events |
8-K filings with item codes decoded and filterable |
secedgar_get_insider_transactions |
Form 4 / 4-A insider transactions parsed from ownership XML |
secedgar_get_institutional_holdings |
13F-HR quarterly holdings of one institutional manager |
secedgar_find_holders |
Reverse 13F lookup: which managers reported holding an issuer |
secedgar_get_beneficial_owners |
5%+ blockholders of an issuer from structured SCHEDULE 13D / 13G filings |
secedgar_get_fund_holdings |
ETF and mutual fund portfolio holdings from the quarterly NPORT-P report |
secedgar_fetch_frames |
One XBRL concept × one period across every reporting company |
secedgar_compare_companies |
Compare named companies across several concepts, aligned on calendar periods |
secedgar_search_concepts |
Discover supported XBRL concept names or reverse-lookup a raw tag |
secedgar_dataframe_describe |
List canvas dataframes with provenance, TTL, and schema |
secedgar_dataframe_query |
Run a single-statement SELECT across dataframes |
secedgar_dataframe_drop |
Drop a canvas dataframe by name; opt-in via EDGAR_DATAFRAME_DROP_ENABLED=true |
Resources
| Resource | Description |
|---|---|
secedgar://concepts |
XBRL financial concepts grouped by statement, mapping friendly names to XBRL tags |
secedgar://filing-types |
Common SEC filing types, plus the 8-K item-code tables for both numbering regimes |
secedgar_search_concepts serves the same concept catalog to tool-only clients.
Prompts
| Prompt | Description |
|---|---|
secedgar_company_analysis |
Structured analysis of a company's SEC filings: financial trends, risk factors, material events |
Capability reference
secedgar_company_search tool
querytakes a ticker (equities, ETFs, and mutual funds;BRK-BorBRK.B), a current or former company name, or a CIK;include_filings(default on) adds up tofiling_limitfilings (1–50, default 10), filtered by exactformsmatch andfiled_after/filed_before- Returns SIC code, exchanges, fiscal year end, and state of incorporation, plus
series_id/class_idfor a fund ticker; fails asno_match(near matches indata.suggestions) ormultiple_matches - A date filter or an under-filled form filter scans past the recent window (the last year or 1,000 filings, whichever holds more) into the archive, up to 10 archive pages, and
history_scanned_throughreports how far it reached
secedgar_search_filings tool
- Full-text
query(phrases,OR,-exclusion,wildcard*,ticker:/cik:scoping), or browse byformsand/or entity with no query;filed_afterandfiled_beforemust be given together;limitup to 100, andoffset(up to 9,999) pages server-side only undersort: "relevance"on a 2001-onward search - Full text covers 2001 onward. Earlier ranges, back to 1993, come from the archives, and pre-2001 free text needs
ticker:/cik:scope and reads up to 50 documents (scanreports candidates, scanned, and matched) - A range crossing 2001-01-01 is split and merged, each row tagged with
source(efts,submissions,full-index); the response carriestotal,total_is_exact, andform_distribution
secedgar_get_filing tool
accession_numberin dash or 18-digit form, optionalcikto speed the lookup,documentfor an exhibit,include_xbrlfor XBRL artifacts;content_limit1,000–200,000 characters per page (default 50,000)- Page with
offset/next_offsetuntilcontent_truncatedis false, or jump withsection(substring match on detected headings); the first page of a truncated document carries anoutlineof up to 50 headings with offsets documentssplits the filing into primary, exhibits, and auxiliary; entries markedbinary(scans, PDFs, archives) fail asbinary_document, and asectionmiss returnssection_not_foundwith the outlineform,filing_date, andperiod_endingcome from the company's submissions feed for a recent filing and from the filing's own SEC header for an older one
secedgar_get_financials tool
company(ticker or CIK) plusconceptas a friendly name or raw XBRL tag;taxonomyus-gaap(default),ifrs-full, ordei;period_typeannual,quarterly, orall, defaulting to annual with a fallback to the full series for instant concepts;limit1–100 trims the inline series- A deduplicated series, newest first, one value per calendar period with its source
form,fileddate,accession_number, andtag;tags_triednames the tags walked, and an empty result fails asno_concept_data,no_frame_data, orno_period_data - A
conceptthat is neither a friendly name nor an UpperCamelCase tag fails asunknown_conceptbefore any SEC request, with a formula for standard combinations (free_cash_flow,ebitda,working_capital) or up to three closest friendly names
secedgar_get_snapshot tool
company,taxonomyus-gaap(default) orifrs-full, andperiod_typeannual,quarterly, orboth(default); one companyfacts read covers every supported concept, and nothing is staged as a dataframe- Each
linesentry reports the latestannualandquarterlyvalue for a duration concept, or the latestinstantvalue for balance-sheet and entity-info concepts, each with thetagit came from, under the line'staxonomy; concepts the filer doesn't report land ingapswithtags_tried
secedgar_get_material_events tool
companyplus up to 20itemscodes, dotted (2.02) since 2004-08-23 and single integers (12) before; the two regimes don't overlap, so pair them across the changeover;filed_after/filed_beforework alone and reach into the archive;limit1–100 (default 20)- Each filing decodes its
itemstocode,label, andregime(current/legacy);item_distributioncounts every code in the window before the filter, andtotal_8k_scannedagainsttotal_matchedshows what the filter removed - A date window reads every archive page overlapping it, up to 10; without one, the archive is read only to fill
limit, stopping on the page that fills it;history_scanned_throughanddataset.truncatedreport what went unread
secedgar_get_insider_transactions tool
companyis the issuer;transaction_typepurchase(code P),sale(code S), orall(default);limit1–100 (default 20); does not cover Forms 3 or 5- Without a date window it scans up to 100 of the newest Form 4 / 4-A filings;
filed_after/filed_before(inclusive, either alone) read any period since mid-2003, paging into the archive (up to 10 pages) when the window predates the recent submissions window, and with a canvas every in-window filing is parsed, up to 100;history_scanned_throughnames the oldest filing parsed - Each transaction carries the reporting person, relationship,
transaction_codeandtransaction_type,is_derivative, unsignedshares_tradedwithdirection(acquire/dispose), price per share, and shares owned after;dataset.truncatedflags Form 4 filings beyond those parsed
secedgar_get_institutional_holdings tool
companyis the 13F filer (a CIK is most reliable), not a portfolio company, which issecedgar_find_holders' job;quarterasYYYY-QN, defaulting to the newest filing;limit1–500 (default 20) withoffset/next_offset;consolidate(default true) merges sub-lines into positions sorted by value- A
quarterolder than the recent submissions window is found in the archive, read forward from the quarter end (up to 10 pages); a quarter the manager covered with a 13F-NT notice — or, with noquarter, a manager whose recent filings are notices only — fails asno_filings_foundnaming the notice's accession number and period - Holdings carry issuer, CUSIP,
market_value_usdin whole USD, shares or principal, andput_call;total_holdings_in_filingcounts raw rows andtotal_positionsdistinct positions; a shared legal name fails asambiguous_entity
secedgar_find_holders tool
issueras a ticker, CIK, or name, plus an optional 9-charactercusip, the precise match key (a name phrase-match both over- and under-matches);quarterasYYYY-QN, defaulting to the newest quarter past its 45-day filing deadline;limit1–100 (default 20) from up to 500 fetched filings- Rows carry
filer_cik,accession_number, andform, whilesearch_mode(cusip/name),total_filings,fetched, andholders_in_quartersize the result; the list is unranked, so pass afiler_ciktosecedgar_get_institutional_holdingsto read the position
secedgar_get_beneficial_owners tool
issueris the company being held;form_kindall(default),13D, or13G;include_amendments(default true);limit1–20 filings (default 10), each a separate document fetch- Each filing lists
reporting_personswith voting power, dispositive power, andpercent_of_classper person, which joint filers report for the same shares, so they don't sum; a 13D carriespurpose_of_transaction, a 13G has none - Coverage starts 2024-12-18 with the structured XML schedules;
legacy_filings_before_coveragecounts the issuer's olderSC 13D/SC 13Gtext filings
secedgar_get_fund_holdings tool
fundas a ticker (VOO), series ID (S000002839), or CIK;series_idpicks one fund of a multi-series trust, which otherwise fails asseries_requiredwith the series listed;report_datetargets a period fromavailable_report_periods;limit1–100 (default 20) withoffset/next_offset- Positions come largest first by
percent_of_net_assets, with name, CUSIP / ISIN / LEI,balanceandunits,value_usd, and asset and issuer category, alongside fund net assets, total assets, and total liabilities - Holdings are as of
report_period_date, roughly two months beforefiling_date;publication_lag_daysstates the gap
secedgar_fetch_frames tool
conceptas a friendly name or raw tag,periodasCY2023,CY2024Q2, orCY2023Q4I,unit(defaultUSD),sortdesc/asc;limit1–100 (default 25) withoffset/next_offsetdown the ranking- One call queries one tag:
unqueried_tagslists same-meaning variants to fetch separately, andrelated_tagslists alternate-definition tags some filers report instead value_distribution.max_to_p95_ratioflags scale-factor outliers,period_end_rangeshows fiscal-year mixing, andcaveatsnames the fiscal-Q4 gap in quarterly frames, the proxy-statement rows in annualNetIncomeLossframes, and the 10-Q trailing-twelve-month rows an annual frame can hold while its year is still open- SEC publishes frames for us-gaap and dei tags only:
taxonomyus-gaap(default) ordeipicks the namespace for a raw tag (EntityCommonStockSharesOutstandingis dei), a friendly name keeps its own mapped taxonomy (shares_outstandingreads dei), and an explicitdeireads a friendly name's tags from dei, as insecedgar_get_financials; IFRS filers are read per company withtaxonomyifrs-full - A
conceptthat is neither a friendly name nor an UpperCamelCase tag fails asunknown_conceptbefore the frames request, with the same formula or closest-name hint assecedgar_get_financials; a well-formed tag with no frame isno_data
secedgar_compare_companies tool
- 2–10
companies× 1–8concepts;taxonomyus-gaap(default) orifrs-full;period_typeannual(default) orquarterly;periods1–12 (default 4), trimmed further when the inline matrix gets too large cellsalign each value on a calendarperiodand keep itsframe,period_end, and sourcetag;failed_companies(reasonnot_found,ambiguous, orno_company_facts) andgaps(no value in any period) report what's missing, andcaveatsflag differing period ends, unit mismatches, and, once per concept, the companies whose values all predate the inline window, each with its newest period- A concept that is neither a friendly name nor an UpperCamelCase tag is listed once in
unknown_conceptswith its hint, never as a gap per company; the call fails asunknown_conceptonly when every concept is one - Inputs naming the same concept (
revenueandRevenue, or one raw tag spelled twice) are compared once under the first spelling, with a caveat naming the merged inputs; a friendly name and a raw tag it maps to (revenueandRevenues) stay separate
secedgar_search_concepts tool
searchis a substring over friendly name, label, and tags, so a raw tag likeNetIncomeLossreverse-maps to its friendly name;groupandtaxonomyfilter; no arguments returns the full catalog- Each concept lists
tags,ifrs_tags(only where an IFRS element was confirmed in live 20-F filings),related_tags,unit, andgroup
secedgar_dataframe_describe tool
- Optional
namefor one dataframe; omit it to list the tenant's active dataframes, newest first - Each entry carries
source_tool,query_params,created_at/expires_at,row_count,truncated, and thecolumn_schemathat SQL forsecedgar_dataframe_queryhas to match
secedgar_dataframe_query tool
- One DuckDB SELECT in
sql(joins, aggregates, window functions, CTEs);row_limit1–10,000 (default 1,000),previewfor fewer inline rows, andregister_as(df_XXXXX_XXXXX) to save the result as a new dataframe - Returns
columns,rows,row_count, androw_count_capped, which when true meansrow_countis the cap, not a total; BIGINT columns serialize as strings - Writes, DDL, file-reading functions, multiple statements, and system catalogs are rejected with typed reasons (
non_select_statement,denied_function,multi_statement,system_catalog_access, and others)
secedgar_dataframe_drop tool
nameof the dataframe to drop; idempotent, returningdropped: falsewhen nothing matched- Off unless
EDGAR_DATAFRAME_DROP_ENABLED=true; disabled, it is absent fromtools/listand uncallable, but still listed on the HTTP landing page with the flag that enables it
secedgar://concepts resource
- The friendly-name catalog grouped by statement, as
text/markdown, with the us-gaap, IFRS, and alternate-definition tags for each concept - The names are what
secedgar_get_financials,secedgar_compare_companies, andsecedgar_fetch_framesaccept as concepts
secedgar://filing-types resource
- Common SEC forms with cadence and use cases, as
text/markdown - Includes the 8-K item-code tables for both numbering regimes, the vocabulary of
secedgar_get_material_events'itemsfilter
secedgar_company_analysis prompt
- Arguments:
companyrequired;focus_areasoptional free text - Returns one user message with a numbered workflow (company search, a financial profile via
secedgar_get_snapshotwith trends viasecedgar_get_financials, filing review, material events, and a peer comparison viasecedgar_compare_companieswithsecedgar_fetch_framesfor a market-wide ranking) and a findings template; insider, institutional, or blockholder terms infocus_areasadd those ownership steps, and "ownership" adds all three
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
EDGAR-specific:
- One process-wide queue paces SEC requests under the 10 req/s limit. A 429 is never retried: every SEC call is refused locally as
rate_limitedwith aretryAftercountdown forEDGAR_RATE_LIMIT_COOLDOWN_SECONDS, then a single probe goes out. Reads served from the local mirror keep answering - CIK resolution from tickers (fund tickers included), current and former company names, or raw CIKs, with corporate-suffix normalization and near-match suggestions on a miss
- Friendly XBRL concept names that handle historical tag changes.
secedgar_get_financials,secedgar_get_snapshot, andsecedgar_compare_companiesshare one frame dedup and tag priority, so their numbers agree; a period whose frame SEC assigned to a proxy statement's pay-versus-performance figure is answered from the filer's own report instead, an annual frame holding a 10-Q's trailing-twelve-month figure is left out of the annual series, and each reportscaveatsfor calendar quarters missing from the frame-tagged series (SEC files fiscal Q4 only as the 10-K residual) and for series that stop years short - Filing documents converted from HTML to text, with heading detection and offset paging for oversized filings
- Opt-in local SQLite mirror of company tickers and XBRL company-facts (
EDGAR_MIRROR_ENABLED) that serves CIK resolution and financials from disk
Agent-friendly output:
- In-conversation SQL: any tool whose response carries a
datasetfield has staged its full result as a DuckDB dataframe (df_<id>), while the inline list stays capped atlimit; inspect it withsecedgar_dataframe_describe, then query it withsecedgar_dataframe_query - Discriminated outputs and explicit gaps:
sourceon filing-search rows,search_mode, 8-K itemregime, typedfailed_companiesreasons, andgapswithtags_triedin place of zero-filled values - Completeness disclosure:
history_scanned_through,total_is_exact,publication_lag_days, anddataset.truncatedtell agents how deep a scan went and what it left out - One parameter name per concept:
company,filed_after/filed_before, andformsmean the same thing on every tool, and common alternate spellings (ticker,cik,start_date,end_date,form_types, and others) are accepted as aliases
Getting started
Public Hosted Instance
A public instance is available at https://secedgar.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"secedgar-mcp-server": {
"type": "streamable-http",
"url": "https://secedgar.caseyjhand.com/mcp"
}
}
}
Self-Hosted / Local
Add the following to your MCP client configuration file.
{
"mcpServers": {
"secedgar-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/secedgar-mcp-server@latest"],
"env": {
"EDGAR_USER_AGENT": "YourAppName your-email@example.com",
"MCP_TRANSPORT_TYPE": "stdio"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"secedgar-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/secedgar-mcp-server@latest"],
"env": {
"EDGAR_USER_AGENT": "YourAppName your-email@example.com",
"MCP_TRANSPORT_TYPE": "stdio"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"secedgar-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"-e", "EDGAR_USER_AGENT=YourAppName your-email@example.com",
"ghcr.io/cyanheads/secedgar-mcp-server:latest"
]
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
Prerequisites
- Bun v1.4.0 or higher (or Node.js v24+).
- A User-Agent string in SEC's
"AppName contact@email.com"format; no account or key required.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/secedgar-mcp-server.git
- Navigate into the directory:
cd secedgar-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env and set EDGAR_USER_AGENT
Configuration
| Variable | Description | Default |
|---|---|---|
EDGAR_USER_AGENT |
Required. User-Agent sent to SEC, as "AppName contact@email.com". SEC blocks IPs without one. |
— |
EDGAR_RATE_LIMIT_RPS |
Max requests per second to SEC (1–10). | 10 |
EDGAR_RATE_LIMIT_COOLDOWN_SECONDS |
Seconds to refuse calls locally after a 429 before one probe goes out. SEC lifts a block only after ten quiet minutes, so a shorter value probes into it. | 600 |
EDGAR_TICKER_CACHE_TTL |
Seconds to cache the company and fund ticker files. A failed fund-file load is retried after a minute (or the rate-limit cool-down) instead of standing for the whole TTL. | 3600 |
EDGAR_DATASET_TTL_SECONDS |
Per-table TTL for canvas dataframes, a sliding window renewed on every dataframe operation. | 86400 |
EDGAR_DATAFRAME_DROP_ENABLED |
Set true to expose secedgar_dataframe_drop, the only destructive tool. |
false |
EDGAR_MIRROR_ENABLED |
Enable the local SQLite mirror of company tickers and XBRL company-facts. Node/Bun only; bootstrap once with bun run mirror:init. |
false |
EDGAR_MIRROR_PATH |
Directory holding the mirror databases. | ./data/edgar-mirror |
EDGAR_MIRROR_REFRESH_CRON |
In-process refresh cron (HTTP transport only), e.g. 0 9 * * *. Omit to refresh with bun run mirror:refresh. |
— |
EDGAR_MIRROR_FALLBACK_LIVE |
Fall back to the live SEC API on a mirror miss. Set false for mirror-only reads. |
true |
CANVAS_PROVIDER_TYPE |
Canvas engine; none disables dataframes. |
duckdb |
MCP_TRANSPORT_TYPE |
Transport: stdio or http. |
stdio |
MCP_HTTP_PORT |
HTTP server port. | 3010 |
MCP_AUTH_MODE |
Authentication: none, jwt, or oauth. |
none |
MCP_LOG_LEVEL |
Log level (debug, info, warning, error, etc.). |
info |
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run the production version:
bun run rebuild bun run start:http # or start:stdioRun checks and tests:
bun run devcheck # Lints, formats, type-checks bun run test # Runs test suite
Docker
docker build -t secedgar-mcp-server .
docker run -e EDGAR_USER_AGENT="MyApp my@email.com" -p 3010:3010 secedgar-mcp-server
The image defaults to HTTP on port 3010 and ships the mirror CLI, so a running container can bootstrap and refresh its own mirror:
docker exec <container> bun run mirror:verify # sync status + sample reads
docker exec <container> bun run mirror:init # one-time bootstrap from the SEC bulk archive
docker exec <container> bun run mirror:refresh # re-ingest after SEC rebuilds the archive
Project structure
| Directory | Purpose |
|---|---|
src/index.ts |
createApp() entry point: registers resources and prompts, starts the SEC client, canvas, and optional mirror. |
src/config |
Server environment variable parsing and validation with Zod. |
src/mcp-server/tools/definitions |
Tool definitions (*.tool.ts) and the buildToolDefinitions() registration list. |
src/mcp-server/resources/definitions |
Resource definitions: XBRL concepts and filing types. |
src/mcp-server/prompts/definitions |
Prompt definitions: company analysis. |
src/services/edgar |
Paced SEC client, CIK resolution, XBRL concept mapping and series dedup, ownership / 13D / 13G / NPORT-P parsers, 8-K item tables, HTML-to-text. |
src/services/edgar/mirror |
Opt-in local SQLite mirror of company tickers and XBRL company-facts. |
src/services/canvas-bridge |
Adapter over the framework DataCanvas: df_<id> naming, per-table TTL, system-catalog SQL deny. |
scripts |
Build, devcheck, and lint tooling, plus the mirror:* commands. |
tests |
Unit and integration tests, mirroring the src/ structure. |
Development guide
See CLAUDE.md and AGENTS.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor logging,ctx.statefor storage - Register new tools in
buildToolDefinitions()(src/mcp-server/tools/definitions/index.ts), and resources and prompts in thecreateApp()arrays insrc/index.ts - Wrap external SEC EDGAR calls: validate the raw response → normalize to a domain type → return the output schema; never fabricate a missing XBRL field — report it under
gapsinstead
Contributing
Issues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.