The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Secedgar MCP Server listing page.
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.
Public Hosted Server: https://secedgar.caseyjhand.com/mcp
Fourteen tools for querying SEC EDGAR data, plus three for SQL analytics over the DuckDB-backed canvas dataframes those tools materialize:
| 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 |
secedgar_company_searchEntry point for most EDGAR workflows — resolve tickers, names, or CIKs to entity details.
AAPL, VOO), company names (Apple), or CIK numbers (320193); a multi-class share ticker resolves in either form (BRK-B or BRK.B)company_tickers_mf.json; fund results include series_id and class_id for downstream scopingFacebook → Meta Platforms, Square → Block)Beacon Financial Corporation → Beacon Financial Corp); Corp, Inc, Co, and Ltd stay distinct from one another, since separate registrants differ only by which one they use (TORO CO vs TORO CORP.)Microsfot → MICROSOFT CORP / MSFT, CSWI → CSW INDUSTRIALS, INC. / CSW)filed_after / filed_before) and under-filled form filters page into the older submissions archive, reaching filings that predate the ~1000-entry recent window (e.g. a 2005 10-K); history_scanned_through discloses the scan depth, and the full filtered history materializes as a df_<id> dataframe when it exceeds the inline filing_limitsecedgar_search_filingsSearch EDGAR filings since 1993. Full-text search covers 2001-present (the EFTS index floor); pre-2001 date ranges are served from the archives — pre-2001 full-text matching requires entity scope.
"material weakness"), boolean operators (revenue OR income), wildcards (account*)cik:320193 or ticker:AAPL) — scoped server-side by CIK, so filings made under a former company name (same CIK) are included; a multi-class share ticker resolves in either form (ticker:BRK-B or ticker:BRK.B)query to list filings by form type (forms=["S-1"]) and/or entity (ticker:/cik:), optionally narrowed by date — a bare date range is not a valid search and must be paired with forms or entity targetingsource field (efts / submissions / full-index), preserved into the df_<id> dataframeticker:/cik: scope to bound the work: the form + date pre-filter picks candidates, up to 50 are read, and scan reports candidates / scanned / matched rather than presenting a partial read as a complete one. SEC's request rate is the cost — roughly 5s for a full 50-document scan. Each read covers the whole accession .txt (pre-1997 filings expose no per-document URL), so a match can sit in an attached exhibit rather than the body of the requested formperiod_ending, ticker, file_description, sic, and location exist only on source: efts rows, so a merged result carries them on some rows and not othersdf_<id> dataframe — query it with secedgar_dataframe_querysecedgar_get_filingFetch a specific filing's metadata and document content by accession number.
binary in the document catalog and rejected with a binary_document error instead of being returned as decoded bytesnext_offset from a truncated response as offset on the next call to continue reading; first-page truncated responses include a detected outline (headings with offsets) for targeted navigationsection param: jumps directly to a named heading by substring match that ignores case, whitespace style, and quote style (e.g. "risk factors", "item 7", "certain relationships"), so a heading copied from the outline resolves whether it carries the filing's non-breaking spaces and curly quotes or plain ones; on a miss, the error carries the detected outline so you can pick the correct headingaccession + document (bounded LRU, 8 entries), making subsequent paged calls cheapsecedgar_get_financialsGet historical XBRL financial data for a company with friendly concept name resolution.
"revenue", "net_income", "eps_diluted" auto-resolve to correct XBRL tagslimit caps the inline series to the most-recent N periods; the full series stays queryable via the df_<id> dataframecaveats entry naming every calendar quarter absent from the frame-tagged series — SEC reports fiscal Q4 as the 10-K residual, so the calendar quarter that fiscal Q4 spans has no discrete quarterly value (calendar-year filers included), and a filer whose other fiscal quarters span non-calendar durations loses a second quarter the same waycaveats entry 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 stop years shortsecedgar://concepts resource for the full mappingsecedgar_get_snapshotBuild a company financial profile in one call instead of a run of secedgar_get_financials calls.
secedgar_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", which covers the income statement, balance sheet, cash flow, and per-share concepts; each line reports the taxonomy its value came fromsecedgar_get_financials when you need a time seriessecedgar_get_insider_transactionsSurface Form 4 / 4-A insider activity for a company by parsing ownership XML. Form 3 initial statements and Form 5 annual statements are not covered — reach those with secedgar_search_filings (forms: ["3", "5"]) plus secedgar_get_filing.
transaction_type (purchase, sale, all); scans newest filings firstdf_<id> dataframe (the inline list is a preview capped at limit) — query it with secedgar_dataframe_query to aggregate net buy/sell by insidersecedgar_get_institutional_holdingsSurface 13F-HR quarterly institutional holdings by parsing the information table.
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)offset — the response echoes the effective offset and returns next_offset while rows remain, so every position stays reachable even when the canvas is disableddf_<id> dataframe (the inline list is one page of limit rows) — query it with secedgar_dataframe_query for full-filing aggregation or cross-quarter joins on cusip + reporting_periodsecedgar_find_holdersReverse 13F lookup: which institutional managers reported a position in an issuer, for one reporting quarter.
cusip matches the identifier the 13F information table itself carries — the precise path. Louisiana-Pacific Q1 2026 returns 451 filings by CUSIP 546347105 against 43 by the phrase "LOUISIANA-PACIFIC CORP"; the name path both under-matches (managers write the name differently) and over-matches (an unrelated issuer 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_ownersThe 5%-and-over stakes in an issuer — the blockholder layer between Form 4 insiders and 13F portfolios. Input is the issuer, the company being held.
form_kindSC 13D / SC 13G text filings with structured XML under the current SCHEDULE 13D / SCHEDULE 13G names. Earlier stakes are readable but not parseable, and legacy_filings_before_coverage reports how many the issuer has — reach them with secedgar_search_filings and read them with secedgar_get_filinginclude_amendments=false leaves only the filings that opened a positiondf_<id> dataframe at one row per reporting person, so it joins the insider and 13F dataframes on issuer CIKsecedgar_get_fund_holdingsWhat an ETF or mutual fund owns, from the NPORT-P portfolio report it files each quarter — the inverse of the ownership tools, which answer who owns a company.
VOO), an SEC fund series ID (S000002839), or a CIK. Fund trusts are indexed by ticker and series rather than by name, so name the registrant by CIK unless the fund itself trades under that name (SPDR S&P 500 ETF Trust)report_period_date. Reports publish roughly two months after the period they cover, so the holdings are the portfolio as of that date, not as of today; publication_lag_days states the gap. Target an earlier period with report_date, chosen from the available_report_periods in any responselimit rows from offset. A broad index fund reports thousands — Vanguard Total Stock Market's most recent report carries 3,524 — so the full report registers as a df_<id> dataframe for aggregation and for joining the 13F and insider dataframes on CUSIPsecedgar_get_material_eventsA company's 8-K history with item codes decoded and filterable — the only surface that can scope by what the event actually was rather than by form.
items (e.g. ["2.02"] for results of operations, ["5.02"] for officer departures, ["4.02"] for non-reliance); secedgar_search_filings and secedgar_company_search cannot see items at all12 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-decoded, and a window spanning it needs both codes in the filteritem_distribution counts every code across the scanned window before the filter, so a zero-hit filter comes back with the items that are present rather than a dead endhistory_scanned_through discloses the scan depthsecedgar://filing-types resourcedf_<id> dataframe with item codes on every row — item frequency over time is one secedgar_dataframe_query awaysecedgar_fetch_framesFetch SEC XBRL frames for one concept × one period across all reporting companies.
secedgar_get_financialsCY2023), quarterly (CY2024Q2), and instant (CY2023Q4I) periodsoffset — the response echoes the effective offset and returns next_offset while companies remain, so ranks past the first page stay reachable even when the canvas is disableddf_<id> dataframe — query it with secedgar_dataframe_queryrelated_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 separatelysecedgar_compare_companiesCompare 2-10 named companies across 1-8 concepts, aligned on calendar periods — the middle shape between secedgar_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) and the window shrinks further when companies x concepts x periods is too large to return in one response; the full aligned series is always materialized as a df_<id> dataframe for growth rates and spreads via secedgar_dataframe_queryfailed_companies with a machine-readable reason and the comparison proceeds with the restgaps with the tags that were tried — never interpolatedcaveats surface a filer missing one or two calendar quarters, a concept that resolved to a retired XBRL tag for one company, period ends that differ inside one aligned period, and concepts whose unit differs across companiessecedgar_search_conceptsDiscover supported XBRL concept names before querying financials or cross-company comparisons.
income_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 filings; 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 / secedgar_dataframe_query / secedgar_dataframe_dropIn-conversation SQL analytics over the dataframes that the data-returning secedgar_* tools materialize on a shared DuckDB-backed canvas. Any call whose response carries a dataset field holds a df_XXXXX_XXXXX handle: read its columns with secedgar_dataframe_describe, then analyze it with secedgar_dataframe_query — joins, aggregates, window functions, percentiles, standard DuckDB SQL.
information_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 for. secedgar_dataframe_drop is the only destructive tool and is opt-in (EDGAR_DATAFRAME_DROP_ENABLED=true); TTL handles cleanup otherwise.EDGAR_DATASET_TTL_SECONDS). The canvas itself uses the framework's sliding TTL.register_as chaining. secedgar_dataframe_query can persist its result as a new dataframe (df_XXXXX_XXXXX) with a fresh TTL — pipe analyses without re-running the source query.row_limit bounds the query, row_count_capped comes back true and row_count is that cap rather than a total — raise row_limit (max 10000) or use register_as to materialize the whole result, whose count is then exact.| URI | 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 |
Built on @cyanheads/mcp-ts-core:
none, jwt, oauth)SEC EDGAR–specific:
company_tickers_mf.json), company names (current and former), or raw CIK numbers with local caching; corporate-suffix normalization on the name passes and dotted share-class tickers (BRK.B) resolved to SEC's hyphenated form; near-match trigram suggestions on zero-result name and ticker queries; committed former-names.json asset for prior-name resolution (Facebook → Meta, Square → Block)html-to-textsecedgar_* tools materialize their full result as a DuckDB-backed canvas dataframe — inspect its columns with secedgar_dataframe_describe, then query it with secedgar_dataframe_queryA public instance is available at https://secedgar.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
Add the following to your MCP client configuration file.
Or with npx (no Bun required):
For Streamable HTTP, set the transport and start the server:
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_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 (e.g. when running on Cloudflare Workers, where DuckDB has no V8-isolate build). | 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 |
Build and run the production version:
Run checks and tests:
The image ships the mirror CLI, so the local mirror (EDGAR_MIRROR_ENABLED) can be bootstrapped, inspected, and refreshed inside a running container:
| Directory | Purpose |
|---|---|
src/mcp-server/tools/definitions/ | Tool definitions (*.tool.ts). Ten 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() arraysIssues and pull requests are welcome. Run checks and tests before submitting:
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.