Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Public Hosted Server: https://secedgar.caseyjhand.com/mcp
SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside from a required SEC User-Agent header. Resolve companies by ticker, name, or CIK, search filings back to 1993, pull XBRL financials and cross-company comparisons by concept, and trace ownership — insider transactions, 13F institutional holdings, 13D/13G blockholders, and fund holdings — from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| 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 (2001+) plus archive-backed browse for pre-2001 ranges |
secedgar_get_filing | Fetch a specific filing's metadata and document content |
secedgar_get_financials | Get historical XBRL financial data for a company |
secedgar_get_snapshot | One-call financial profile — the latest value of every supported concept, grouped by statement |
secedgar_get_material_events | 8-K filings with item codes decoded and filterable — earnings, officer changes, non-reliance |
secedgar_get_insider_transactions | Form 4 / 4-A insider transactions (buys, sells, grants, exercises) parsed from ownership XML |
secedgar_get_institutional_holdings | 13F-HR quarterly institutional holdings parsed from the information table |
secedgar_find_holders | Reverse 13F lookup — which institutional managers reported holding an issuer |
secedgar_get_beneficial_owners | 5%+ blockholders of an issuer, parsed 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 | Fetch SEC XBRL frames for one concept × one period across all reporting companies |
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 — off by default since TTL already handles cleanup, and uncallable until the flag is set |
| Resource | Description |
|---|---|
secedgar://concepts | Common XBRL financial concepts grouped by statement, mapping friendly names to XBRL tags |
secedgar://filing-types | Common SEC filing types with descriptions, cadence, and use cases, plus the full 8-K item-code decode tables for both numbering regimes |
| Prompt | Description |
|---|---|
secedgar_company_analysis | Guides a structured analysis of a public company's SEC filings: identify recent filings, extract financial trends, surface risk factors, and note material events |
secedgar_company_search toolBRK-B or BRK.B), and current/former names both resolve (Facebook → Meta Platforms, Square → Block)company_tickers_mf.json; fund results carry series_id and class_idBeacon Financial Corporation → Beacon Financial Corp), but Corp/Inc/Co/Ltd stay distinct — separate registrants can differ only by which one they useMicrosfot → MICROSOFT CORP / MSFT)forms (exact form match — list 10-K/A to include amendments); filed_after/filed_before and under-filled form filters page into the older submissions archive, reaching filings past the ~1000-entry recent window — history_scanned_through reports the scan depth, and the full filtered history stages as df_<id> when it exceeds filing_limitsecedgar_search_filings toolcik:320193 / ticker:AAPL, either share-class form for multi-class tickers) — server-side scoped by CIK, so former-name filings on the same entity are includedquery) lists by form type and/or entity, optionally narrowed by date; a bare date range must pair with forms or entity targetingticker:/cik: entity scope, since it works by reading up to 50 candidate documents (scan reports candidates/scanned/matched, costing ~5s for a full scan)source (efts/submissions/full-index); period_ending, ticker, file_description, sic, and location exist only on efts rowsfiled_after + filed_before, both inclusive, both required together) and form filtering (forms, amendments included), pagination up to 10,000 results; response includes form distribution for narrowing follow-up searchesdf_<id> when it exceeds the inline limitsecedgar_get_filing toolcontent_limit (1K–200K characters, default 50K)binary in the document catalog and rejected with a binary_document error rather than returned as decoded bytesnext_offset as offset to continue; first-page truncated responses include a detected outline (headings + offsets)section jumps directly to a named heading by substring match ignoring case, whitespace style, and quote style ("risk factors", "item 7") — a miss returns the detected outline; extracted text is cached per accession + document (bounded LRU, 8 entries) so paged calls are cheapsecedgar_get_financials tool"revenue", "net_income", "eps_diluted") auto-resolve to XBRL tags, including historical tag changes (e.g. ASC 606 revenue recognition) — see secedgar://concepts for the full mappingperiod_type (annual/quarterly/all)limit caps the inline series to the most-recent N periods; the full series stays queryable via df_<id>caveats names every calendar quarter absent from the frame-tagged series — SEC reports fiscal Q4 as the 10-K residual, so the calendar quarter it spans carries no discrete quarterly value (calendar-year filers included), and a filer whose other fiscal quarters span non-calendar durations can lose a second quarter the same waycaveats entry appears when the concept resolved to an XBRL tag SEC has retired from the taxonomy — that only happens when no current tag reports for the filer, and the series can then stop years shortsecedgar_get_snapshot toolsecedgar_get_financials callssecedgar_get_financials, so the two agree for any concept they both covergaps with the XBRL tags that were tried — never zero-filled or interpolatedtaxonomy: "ifrs-full", covering the income statement, balance sheet, cash flow, and per-share concepts; each line reports the taxonomy its value came fromsecedgar_get_financials when a time series is neededsecedgar_get_material_events toolitems (e.g. ["2.02"] results of operations, ["5.02"] officer departures, ["4.02"] non-reliance) — the only surface that scopes by what the event actually was, since secedgar_search_filings and secedgar_company_search cannot see item codes12 is the ancestor of 2.02, 9 of 7.01); decoding keys off the code's shape so a filing straddling the changeover is never mis-decodeditem_distribution counts every code across the scanned window before the filter, so a zero-hit filter still surfaces the items that are presenthistory_scanned_through discloses the scan depthsecedgar://filing-types resourcedf_<id> with item codes on every row — item frequency over time is one secedgar_dataframe_query awaysecedgar_get_insider_transactions toolsecedgar_search_filings (forms: ["3", "5"]) plus secedgar_get_filingcompany is the issuer — a ticker, CIK, or company name; a name matching several companies resolves to the top-ranked match, so pass a ticker or CIK when the issuer must be exacttransaction_type (purchase, sale, all); scans newest filings firstdf_<id> (the inline list is a preview capped at limit) — query it to aggregate net buy/sell by insidersecedgar_get_institutional_holdings toolcompany (CIK, ticker, or full legal name, e.g. 0000102909 for Vanguard) to see what it holds; for the reverse direction — which managers hold a given company — use secedgar_find_holders, whose filer_cik results feed straight back into this toolconsolidate: false for raw filing rowsquarter (e.g. "2025-Q4")total_holdings_in_filing counts raw info-table rows, total_positions counts distinct positions after consolidation (both before limit); page with offset, which returns next_offset while rows remaindf_<id> for full-filing aggregation or cross-quarter joins on cusip + reporting_periodsecedgar_find_holders toolcusip matches the identifier the 13F information table itself carries (the precise path); the name path both under-matches (managers write names differently) and over-matches (unrelated issuers sharing a word)secedgar_get_institutional_holdings result, or fall back to the name pathquarter targets a reporting period ("2026-Q1"); omit it for the newest quarter whose 45-day filing deadline has passed — the applied quarter and its filing window are echoed backtotal_filings reports the full count and dataset.truncated flags when more existfiler_cik to secedgar_get_institutional_holdingssecedgar_get_beneficial_owners toolform_kindSC 13D / SC 13G text filings with structured XML; earlier stakes are readable but not parseable, and legacy_filings_before_coverage reports how many the issuer hasinclude_amendments=false leaves only the filings that opened a positiondf_<id>, one row per reporting person, so it joins the insider and 13F dataframes on issuer CIKsecedgar_get_fund_holdings toolVOO), a fund series ID (S000002839), or a CIK — name the registrant by CIK unless the fund itself trades under that namereport_period_date — reports publish roughly two months after the period they cover, so holdings are the portfolio as of that date, not as of today; publication_lag_days states the gap, and report_date targets an earlier period from available_report_periodslimit rows from offset; the full report registers as df_<id> for aggregation and for joining the 13F and insider dataframes on CUSIPsecedgar_fetch_frames toolsecedgar_get_financials, or a raw XBRL tagCY2023), quarterly (CY2024Q2), and instant (CY2023Q4I) periodsoffset, which returns next_offset while companies remaindf_<id>related_tags flags alternate-definition tags some filers use as their primary line (e.g. cash → restricted-cash-inclusive total, equity → NCI-inclusive total), so a whole-universe screen on the base tag isn't silently under-inclusive — query those separatelyunqueried_tags lists the others to query and combine with an analysis-specific prioritysecedgar_compare_companies toolsecedgar_get_financials (one company over time) and secedgar_fetch_frames (one period across the market)secedgar_get_financialsperiods bounds the inline matrix (1-12, default 4), shrinking further when companies × concepts × periods is too large for one response; the full aligned series always materializes as df_<id>failed_companies and the comparison proceeds with the rest; a company that does not report a concept is reported in gaps with the tags that were tried — never interpolatedcaveats surface a filer missing calendar quarters, a concept that resolved to a retired tag for one company, differing period ends inside one aligned period, and unit mismatches across companiessecedgar_search_concepts toolincome_statement, balance_sheet, cash_flow, per_share, entity_info) or taxonomyNetIncomeLoss to the supported friendly namesrelated_tags for concepts with a high-coverage alternate-definition tag (e.g. restricted-cash-inclusive cash) so callers can discover them before screeningtaxonomy: "ifrs-full" narrows the catalog to concepts with an IFRS tag confirmed against live 20-F filers — a concept with no IFRS equivalent is left out rather than mapped to a guesssecedgar_get_financials, secedgar_fetch_frames, and secedgar://conceptssecedgar_dataframe_describe tooldf_XXXXX_XXXXX) registered by the data-returning secedgar_* tools — any response carrying a dataset field holds onename describes a single dataframe; omit to list every dataframe for the tenantsecedgar_dataframe_querysecedgar_dataframe_query toolsecedgar_* toolsinformation_schema, pg_catalog, sqlite_master, duckdb_*) are denied at the bridge layer so callers can't enumerate dataframes they don't already hold a handle forrow_limit caps rows materialized in the response (default 1000, max 10000); a capped result reports row_count_capped: true with row_count as that cap rather than a total — a SQL LIMIT exactly equal to the cap is indistinguishable from an exact result and reported as suchregister_as persists the result as a new dataframe (df_XXXXX_XXXXX) with a fresh TTL, to chain analyses without re-running the source queryvalue, COUNT/SUM results) serialize as JSON strings to preserve precision past 2^53 — cast to DOUBLE in projections for inline arithmeticsecedgar_dataframe_drop tooldropped: false when nothing matchedEDGAR_DATAFRAME_DROP_ENABLED=true — off by default since the per-table TTL already reclaims canvas tables, and this is the only destructive tool on the serverdisabledTool(): absent from tools/list and uncallable, but shown on the HTTP landing page in a disabled group naming the reason and the flag that enables itsecedgar://concepts resourcetext/markdownsecedgar_get_financials and secedgar_fetch_frames to their underlying XBRL tagssecedgar://filing-types resourcetext/markdownforms filter for secedgar_search_filings and secedgar_company_search, or the items filter for secedgar_get_material_eventssecedgar_company_analysis promptcompany (name, ticker, or CIK) required; focus_areas free-text optional (e.g. "revenue growth, debt levels, insider activity") — omitted, it performs a general analysissecedgar_* tool, closing with a peer comparison via secedgar_fetch_framesfocus_areas mentioning insider, institutional, or blockholder/activist terms adds the matching ownership step — insider transactions (Form 4/4-A), institutional holdings (secedgar_find_holders then secedgar_get_institutional_holdings), or 5%+ blockholders (13D/13G) — the generic word "ownership" adds all threeBuilt 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:
EDGAR_RATE_LIMIT_COOLDOWN_SECONDS), refusing calls locally with a retryAfter countdown so the block can clear, then sends a single probe before resuming. Reads served from the opt-in local mirror keep answering throughoutcompany_tickers_mf.json), current and former company names, or raw CIK numbers, with local caching, corporate-suffix normalization, and near-match trigram suggestions on zero-result queriescompany_tickers + XBRL company-facts (EDGAR_MIRROR_ENABLED) serves CIK resolution and financials from disk instead of the live APIAgent-friendly output:
df_<id>); inspect its columns with secedgar_dataframe_describe, then query with secedgar_dataframe_querysource fields on merged filing-search rows (efts/submissions/full-index), typed caveats entries for series staleness and fiscal-Q4 gaps, and gaps/failed_companies rows instead of silent omissionsecedgar_compare_companies returns every resolved company alongside failed_companies and per-concept gaps rather than failing the whole requestcompany, filed_after/filed_before, and forms mean the same thing on every tool that takes them, and each tool also accepts the other common spellings (ticker, cik, ticker_or_cik, start_date/end_date, date_from/date_to, form_types), so a name carried over from another tool is not rejectedhistory_scanned_through, publication_lag_days, and dataset.truncated let agents reason about scan depth, report lag, and completenessA 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"
}
}
}
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
"AppName contact@email.com" format works; no account or key required.git clone https://github.com/cyanheads/secedgar-mcp-server.git
cd secedgar-mcp-server
bun install
bun run build
All configuration is validated at startup via Zod schemas in src/config/server-config.ts. Key environment variables:
| Variable | Description | Default |
|---|---|---|
EDGAR_USER_AGENT | Required. User-Agent header for SEC compliance. Format: "AppName contact@email.com". SEC blocks IPs without a valid User-Agent. | — |
EDGAR_RATE_LIMIT_RPS | Max requests/second to SEC APIs. Do not exceed 10. | 10 |
EDGAR_RATE_LIMIT_COOLDOWN_SECONDS | Seconds to stop sending to SEC after a 429. Calls are refused locally with a retryAfter countdown, then one probe request goes out. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. | 600 |
EDGAR_TICKER_CACHE_TTL | Seconds to cache the company tickers lookup file. | 3600 |
EDGAR_DATASET_TTL_SECONDS | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. | 86400 |
EDGAR_DATAFRAME_DROP_ENABLED | Set to true to expose secedgar_dataframe_drop — the only destructive tool on this server. Off by default; TTL handles cleanup, and the tool is still listed on the HTTP landing page as disabled, with the flag that enables it. | false |
EDGAR_MIRROR_ENABLED | Enable the local SQLite mirror of company_tickers + XBRL company-facts so CIK resolution and financials read from disk instead of the live API. Node/Bun only (skipped on Workers). Bootstrap once with bun run mirror:init. | false |
EDGAR_MIRROR_PATH | Directory holding the mirror SQLite databases. | ./data/edgar-mirror |
EDGAR_MIRROR_REFRESH_CRON | Cron for the in-process nightly refresh (HTTP transport only). Recommended 0 9 * * *. Omit to refresh out-of-band via bun run mirror:refresh. | — |
EDGAR_MIRROR_FALLBACK_LIVE | When the mirror misses (not yet synced, or a filing newer than the last refresh), fall back to the live SEC API. Set false for strict mirror-only reads. | true |
CANVAS_PROVIDER_TYPE | Canvas engine. Defaults to duckdb; set to none to disable the canvas. | 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 |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
See .env.example for the full list of optional overrides.
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 build -t secedgar-mcp-server .
docker run -e EDGAR_USER_AGENT="MyApp my@email.com" -p 3010:3010 secedgar-mcp-server
The image ships the mirror CLI, so the local mirror (EDGAR_MIRROR_ENABLED) can be bootstrapped, inspected, and refreshed inside a running container:
docker exec <container> bun run mirror:verify # sync status + sample reads
docker exec <container> bun run mirror:init # one-time bootstrap (downloads the SEC bulk archive)
docker exec <container> bun run mirror:refresh # re-ingest when the archive has been rebuilt
| Directory | Purpose |
|---|---|
src/mcp-server/tools/definitions/ | Tool definitions (*.tool.ts). Fourteen SEC EDGAR tools plus three dataframe_* tools for SQL analytics. |
src/mcp-server/resources/definitions/ | Resource definitions. XBRL concepts and filing types. |
src/mcp-server/prompts/definitions/ | Prompt definitions. Company analysis prompt. |
src/services/edgar/ | SEC EDGAR API client, XBRL concept mapping, HTML-to-text conversion. |
src/services/canvas-bridge/ | Adapter over the framework DataCanvas: df_<id> minting, all-nullable schema derivation, per-table TTL bookkeeping, bridge-layer system-catalog SQL deny. |
src/config/ | Server-specific environment variable parsing and validation with Zod. |
tests/ | Unit and integration tests, mirroring the src/ structure. |
See CLAUDE.md and AGENTS.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for logging, ctx.state for storagecreateApp() arraysgaps insteadIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @cyanheads/secedgar-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cyanheads-secedgar-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/secedgar-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup referenceio.github.cyanheads/secedgar-mcp-server works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.