secedgar-mcp-server
SEC EDGAR 申报文件与财务数据
文档
@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, andaccession_number;tags_triednames the tags walked, and an empty result fails asno_concept_data,no_frame_data, orno_period_data
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, with thetagandtaxonomyit came from; 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
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 itsframeandperiod_end;failed_companies(reasonnot_found,ambiguous, orno_company_facts) andgapsreport what's missing, andcaveatsflag differing period ends and unit mismatches
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, financial trends, filing review, material events, peer comparison via
secedgar_fetch_frames) 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, 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:stdio -
Run 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.