<div align="center"> <h1>@cyanheads/secedgar-mcp-server</h1> <p><b>Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.</b> <div>16 Tools (+1 opt-in) • 2 Resources • 1 Prompt</div> </p> </div>
<div align="center">
     
</div>
<div align="center">
  

</div>
<div align="center">
Public Hosted Server: https://secedgar.caseyjhand.com/mcp
</div>
---
Tools
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 |
secedgar_company_search
Entry point for most EDGAR workflows — resolve tickers, names, or CIKs to entity details.
- Supports ticker symbols (
AAPL,VOO), company names (Apple), or CIK numbers (320193) - ETFs and mutual funds resolve by ticker via
company_tickers_mf.json; fund results includeseries_idandclass_idfor downstream scoping - Current and former company names both resolve (
Facebook→ Meta Platforms,Square→ Block) - Near-match suggestions on zero-result name search (e.g.
Microsfot→MICROSOFT CORP / MSFT) - Optionally includes recent filings with form type filtering
- Date filtering (
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_throughdiscloses the scan depth, and the full filtered history materializes as adf_<id>dataframe when it exceeds the inlinefiling_limit - Returns entity metadata: SIC code, exchanges, fiscal year end, state of incorporation
---
secedgar_search_filings
Search 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.
- Exact phrases (
"material weakness"), boolean operators (revenue OR income), wildcards (account*) - Entity targeting within query string (
cik:320193orticker:AAPL) — scoped server-side by CIK, so filings made under a former company name (same CIK) are included - Browse mode: omit
queryto 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 targeting - Pre-2001 date ranges (back to 1993) route to the archives: an entity-scoped range reads the filer's full submissions history; an unscoped forms/date range browses the quarterly full-index. Each row carries a
sourcefield (efts/submissions/full-index), preserved into thedf_<id>dataframe - Pre-2001 free text is matched by reading documents, so it needs
ticker:/cik:scope to bound the work: the form + date pre-filter picks candidates, up to 50 are read, andscanreports 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 form - A range crossing 2001-01-01 is split at the boundary and merged: the full-text index serves 2001 onward, the archives serve the rest.
period_ending,ticker,file_description,sic, andlocationexist only onsource: eftsrows, so a merged result carries them on some rows and not others - Date range filtering, form type filtering, pagination up to 10,000 results
- Returns form distribution for narrowing follow-up searches
- When the entity-scoped window exceeds the inline limit, the already-fetched EFTS window is materialized as a
df_<id>dataframe — query it withsecedgar_dataframe_query
---
secedgar_get_filing
Fetch a specific filing's metadata and document content by accession number.
- Accepts accession numbers in dash or no-dash format
- Converts HTML filings to readable plain text
- Configurable content limit (1K–200K characters, default 50K)
- Can fetch specific exhibits by document name
- Binary entries — scanned pages, PDF exhibits, packaged archives and spreadsheets — are marked
binaryin the document catalog and rejected with abinary_documenterror instead of being returned as decoded bytes - Offset paging for large documents (10-K, S-1/A can exceed 1M chars): pass
next_offsetfrom a truncated response asoffseton the next call to continue reading; first-page truncated responses include a detectedoutline(headings with offsets) for targeted navigation - Section targeting via the
sectionparam: jumps directly to a named heading by case-insensitive substring match (e.g."risk factors","item 7","certain relationships"); on a miss, the error carries the detected outline so you can pick the correct heading - Extracted text is cached per
accession + document(bounded LRU, 8 entries), making subsequent paged calls cheap
---
secedgar_get_financials
Get historical XBRL financial data for a company with friendly concept name resolution.
- Friendly names like
"revenue","net_income","eps_diluted"auto-resolve to correct XBRL tags - Handles historical tag changes (e.g., ASC 606 revenue recognition)
- Automatic deduplication to one value per standard calendar period
- Filter by annual, quarterly, or all periods
- Optional
limitcaps the inline series to the most-recent N periods; the full series stays queryable via thedf_<id>dataframe - Quarterly results carry a
caveatsentry 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 way - A further
caveatsentry 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 short - See
secedgar://conceptsresource for the full mapping
---
secedgar_get_snapshot
Build a company financial profile in one call instead of a run of secedgar_get_financials calls.
- Reads the filer's complete companyfacts payload once, then resolves every supported concept against it
- Same frame dedup and tag priority as
secedgar_get_financials, so the two agree for any concept they both cover - Duration concepts (income statement, cash flow, per-share) report their latest full year and latest single quarter; balance-sheet and entity-info concepts report their latest point-in-time value
- Concepts the filer does not report are listed under
gapswith the XBRL tags that were tried — never zero-filled or interpolated - IFRS filers resolve through the mapped IFRS tag variants via
taxonomy: "ifrs-full", which covers the income statement, balance sheet, cash flow, and per-share concepts; each line reports the taxonomy its value came from - Compact single-record profile — no dataframe; reach for
secedgar_get_financialswhen you need a time series
---
secedgar_get_insider_transactions
Surface 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.
- Reporting person, relationship to issuer (director, officer + title, 10% owner), and transaction date
- Transaction code mapped to a readable type (purchase, sale, gift, award, exercise, …); shares signed by acquired/disposed
- Price per share and shares owned after each transaction; covers non-derivative (open-market) and derivative (option/RSU) lines
- Filter by
transaction_type(purchase,sale,all); scans newest filings first - The full set of transactions parsed from the scanned recent filings is materialized as a
df_<id>dataframe (the inline list is a preview capped atlimit) — query it withsecedgar_dataframe_queryto aggregate net buy/sell by insider
---
secedgar_get_institutional_holdings
Surface 13F-HR quarterly institutional holdings by parsing the information table.
- Pass the institutional filer (CIK or full legal name, e.g.
0000102909for Vanguard) to see what it holds; for the reverse direction — which managers hold a given company — usesecedgar_find_holders, whosefiler_cikresults feed straight back into this tool - Each holding: issuer name, CUSIP, market value (whole USD), shares/principal, and put/call; raw rows also carry investment discretion
- Sub-lines for the same security (one per manager/account) are consolidated into distinct positions sorted by value by default — pass
consolidate: falsefor raw filing rows - Resolves the filing-manager name and reporting quarter from the cover page; target a specific quarter with
quarter(e.g."2025-Q4") total_holdings_in_filingcounts raw info-table rows;total_positionscounts distinct positions after consolidation (both beforelimit)- Page through a large information table with
offset— the response echoes the effectiveoffsetand returnsnext_offsetwhile rows remain, so every position stays reachable even when the canvas is disabled - The full parsed holdings set is materialized as a
df_<id>dataframe (the inline list is one page oflimitrows) — query it withsecedgar_dataframe_queryfor full-filing aggregation or cross-quarter joins oncusip+reporting_period
---
secedgar_find_holders
Reverse 13F lookup: which institutional managers reported a position in an issuer, for one reporting quarter.
- Searching by
cusipmatches the identifier the 13F information table itself carries — the precise path. Louisiana-Pacific Q1 2026 returns 451 filings by CUSIP546347105against 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) - A CUSIP is not derivable from a ticker anywhere in EDGAR — read one off any
secedgar_get_institutional_holdingsresult, or fall back to the name path quartertargets 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 back- Filings are kept by the period they report, not the date they were filed, so amendments restating an older quarter (roughly 6% of any window) do not land in the wrong quarter's holder list
- Up to 500 filer rows are fetched per call;
total_filingsreports the full count anddataset.truncatedflags when more exist - The list is unranked. EDGAR search relevance carries no signal about position size — read a manager's actual position by passing its
filer_ciktosecedgar_get_institutional_holdings
---
secedgar_get_beneficial_owners
The 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.
- 13D is the activist form and carries the filer's stated purpose of the transaction; 13G is the passive form and has no purpose item at all, which is the substantive difference between a stake that intends to influence control and one that does not. Filter with
form_kind - Every reporting person is listed separately. Voting power, dispositive power, and percent of class are reported per person even on a joint filing where several funds and their controlling principal report the same underlying shares — summing those percentages double-counts the position
- Coverage starts 2024-12-18, when SEC replaced the legacy
SC 13D/SC 13Gtext filings with structured XML under the currentSCHEDULE 13D/SCHEDULE 13Gnames. Earlier stakes are readable but not parseable, andlegacy_filings_before_coveragereports how many the issuer has — reach them withsecedgar_search_filingsand read them withsecedgar_get_filing - Amendments carry the current position and are included by default;
include_amendments=falseleaves only the filings that opened a position - The full parsed set registers as a
df_<id>dataframe at one row per reporting person, so it joins the insider and 13F dataframes on issuer CIK
---
secedgar_get_fund_holdings
What 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.
- Input is the fund: a ticker (
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) - An NPORT-P covers exactly one fund series and a registrant trust files one report per series per period, so a trust running several funds needs the specific fund named. A registrant that resolves to more than one series comes back with the series listed, each with its ticker; one whose series carry no ticker is routed by reading the series off its newest report, because a trust's own filing history interleaves funds whose fiscal quarters end on different months
- Every result is dated to
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_daysstates the gap. Target an earlier period withreport_date, chosen from theavailable_report_periodsin any response - Positions carry the security name, CUSIP/ISIN/LEI where the filer reports them, share balance, USD value, and percent of net assets, alongside fund-level net assets, total assets, and total liabilities
- Positions come back largest first by percent of net assets, one page of
limitrows fromoffset. A broad index fund reports thousands — Vanguard Total Stock Market's most recent report carries 3,524 — so the full report registers as adf_<id>dataframe for aggregation and for joining the 13F and insider dataframes on CUSIP
---
secedgar_get_material_events
A 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.
- Filter with
items(e.g.["2.02"]for results of operations,["5.02"]for officer departures,["4.02"]for non-reliance);secedgar_search_filingsandsecedgar_company_searchcannot see items at all - Two numbering regimes are both accepted and decoded: the dotted scheme in force since 2004-08-23, and the single integers before it (legacy
12is the ancestor of2.02,9of7.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 filter item_distributioncounts 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 end- A date window pages into the older submissions archive, reaching 8-K filings that predate the ~1000-filing recent window;
history_scanned_throughdiscloses the scan depth - The full decode table is in the
secedgar://filing-typesresource - The full filtered set materializes as a
df_<id>dataframe with item codes on every row — item frequency over time is onesecedgar_dataframe_queryaway
---
secedgar_fetch_frames
Fetch SEC XBRL frames for one concept × one period across all reporting companies.
- Same friendly concept names as
secedgar_get_financials - Supports annual (
CY2023), quarterly (CY2024Q2), and instant (CY2023Q4I) periods - Inline response returns one page of the ranked companies (sort + limit), with ticker enrichment
- Walk further down the ranking with
offset— the response echoes the effectiveoffsetand returnsnext_offsetwhile companies remain, so ranks past the first page stay reachable even when the canvas is disabled - The full frames response (all reporters, typically 2k–10k rows) is materialized as a
df_<id>dataframe — query it withsecedgar_dataframe_query related_tagsflags 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 separately
---
secedgar_compare_companies
Compare 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).
- One companyfacts read per company, resolved through the same frame dedup and tag priority as
secedgar_get_financials - Balance-sheet and entity-info concepts align on the calendar year or quarter their point-in-time snapshot falls in, so they sit in the same matrix as income-statement lines; each cell keeps its underlying XBRL frame
periodsbounds 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 adf_<id>dataframe for growth rates and spreads viasecedgar_dataframe_query- A company that fails to resolve is reported in
failed_companieswith a machine-readable reason and the comparison proceeds with the rest - A company that does not report a concept is reported in
gapswith the tags that were tried — never interpolated caveatssurface 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 companies
---
secedgar_search_concepts
Discover supported XBRL concept names before querying financials or cross-company comparisons.
- Search by friendly name, label, or raw XBRL tag
- Filter by statement group (
income_statement,balance_sheet,cash_flow,per_share,entity_info) or taxonomy - Reverse-lookup raw tags like
NetIncomeLossto the supported friendly names - Surfaces
related_tagsfor concepts with a high-coverage alternate-definition tag (e.g. restricted-cash-inclusive cash) so callers can discover them before screening - Filtering by
taxonomy: "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 guess - Returns the same catalog used by
secedgar_get_financials,secedgar_fetch_frames, andsecedgar://concepts
---
secedgar_dataframe_describe / secedgar_dataframe_query / secedgar_dataframe_drop
In-conversation SQL analytics over the dataframes that secedgar_fetch_frames, secedgar_compare_companies, secedgar_search_filings, secedgar_get_financials, secedgar_get_material_events, secedgar_get_insider_transactions, secedgar_get_institutional_holdings, and secedgar_find_holders materialize on a shared DuckDB-backed canvas. Each data-returning call adds a dataset field with a df_XXXXX_XXXXX handle; pass that handle to secedgar_dataframe_query for joins, aggregates, window functions, percentiles — standard DuckDB SQL.
- Read-only by default. Writes, DDL, DROP, COPY, PRAGMA, ATTACH, and external-file table functions are rejected by the framework SQL gate. System catalogs (
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_dropis the only destructive tool and is opt-in (EDGAR_DATAFRAME_DROP_ENABLED=true); TTL handles cleanup otherwise. - Per-table TTL. Each dataframe ages on its own clock (default 24h, override with
EDGAR_DATASET_TTL_SECONDS). The canvas itself uses the framework's sliding TTL. register_aschaining.secedgar_dataframe_querycan persist its result as a new dataframe (df_XXXXX_XXXXX) with a fresh TTL — pipe analyses without re-running the source query.
Resources
| 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 |
Prompts
| 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 |
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool definitions — single file per tool, framework handles registration and validation
- Structured output schemas with automatic formatting for human-readable display
- Unified error handling across all tools
- Pluggable auth (
none,jwt,oauth) - Structured logging with request-scoped context
- Runs locally (stdio/HTTP) from the same codebase
SEC EDGAR–specific:
- Rate-limited HTTP client respecting SEC's 10 req/s limit with automatic inter-request delay
- CIK resolution from tickers (including ETFs and mutual funds via
company_tickers_mf.json), company names (current and former), or raw CIK numbers with local caching; near-match trigram suggestions on zero-result name queries; committedformer-names.jsonasset for prior-name resolution (Facebook→ Meta,Square→ Block) - Friendly XBRL concept name mapping with historical tag change handling
- Searchable concept catalog with statement-group metadata and reverse XBRL tag lookup
- HTML-to-text conversion for filing documents via
html-to-text - In-conversation SQL analytics:
secedgar_fetch_frames,secedgar_compare_companies,secedgar_search_filings,secedgar_get_financials,secedgar_get_material_events,secedgar_get_insider_transactions,secedgar_get_institutional_holdings, andsecedgar_find_holdersmaterialize their full result as a DuckDB-backed canvas dataframe queryable viasecedgar_dataframe_query - No API keys required — SEC EDGAR is a free, public API
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"
}
}
}
}
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.3.0 or higher.
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
- Build:
bun run build
Configuration
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. | 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 |
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 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
Project structure
| 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. |
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 and resources in the
createApp()arrays
Contributing
Issues and pull requests 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.











