npm License Docker MCP SDK TypeScript Bun
Install in Claude Desktop Install in Cursor Install in VS Code
Public Hosted Server: https://secedgar.caseyjhand.com/mcp
Overview
SEC EDGAR filings, financials, and ownership data. Look up companies by ticker, name, or CIK, search filings back to 1993, compare financials across companies, and trace ownership through insider trades, 13F holdings, 13D/13G blockholders, and fund portfolios. No API key required.
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 fiscal years or calendar quarters |
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 |
Skills
| Skill | Description |
|---|---|
secedgar-guide | Which tool answers each ownership question, where CIKs, accession numbers, CUSIPs, and fund series IDs come from, and the coverage limits to check before reporting data absent |
The skill ships in skills/secedgar-guide/SKILL.md. Plugin hosts load it from the package, and MCP clients that support the Skills extension read it from the server as skill://secedgar-guide/SKILL.md.
Capability reference
secedgar_company_search tool
querytakes a ticker (equities, ETFs, mutual funds), a current or former company name, or a CIK;include_filings(default on) adds up tofiling_limitfilings (default 10; a value up to 1,000 is served at the 50 inline cap), filtered byforms,filed_after, andfiled_before- Returns SIC code, exchanges, fiscal year end, and state of incorporation, plus
series_id/class_idfor a fund ticker; fails asno_match(with near matches indata.suggestions) ormultiple_matches - A date or form filter can reach into the archive (up to 10 pages);
history_scanned_throughreports how far it got
secedgar_search_filings tool
- Full-text
query(phrases,OR,-exclusion,wildcard*,ticker:/cik:scoping), or browse byformsand/or entity with no query;filed_after/filed_beforework alone (a lonefiled_afterruns through today, a lonefiled_beforestarts at 2001-01-01, or at 1993-01-01 before 2001);limitup to 100 (default 20; a value up to 1,000 is served at 100),offsetup to 9,999 (only withsort: "relevance"on a 2001-onward search) - Full text covers 2001 onward; earlier ranges back to 1993 come from the archives, where free text needs
ticker:/cik:scope and reads up to 50 documents (scanreports the counts) - One row per filing, tagged with
source(efts,submissions,full-index); full-text rows listmatched_documents(filenames forsecedgar_get_filing); the response carriestotal,form_distribution,total_is_exact, andtotal_documentsfor full-text searches
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; up to 1,000,000 is served at 200,000)- Page with
offset/next_offsetuntilcontent_truncatedis false, or jump withsection(a heading such asitem 1orpart i), then dropsectionto page on, since it outranksoffset; the first page of a truncated document carries anoutlineof up to 50 headings with offsets documentssplits the filing into primary, exhibits, and auxiliary;binaryentries (scans, PDFs, archives) fail asbinary_document, and asectionmiss returnssection_not_foundwith the outline- If the archive doesn't serve a filing's primary document (common in 2000–2001), the full submission
<accession>.txtis read instead and the notice says so
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;unitpicks one SEC unit key (ZAR,USD,USD/EUR) when a concept is reported in several;limit1–100 trims the inline series- Returns a deduplicated series, newest first, one value per calendar period with
form,filed,accession_number, andtag, all in oneunit(other units are named incaveats);tags_triedlists the tags checked, and an empty result fails asno_concept_data,no_frame_data,no_period_data, orno_unit_data - A
conceptthat is neither a friendly name nor an UpperCamelCase tag fails asunknown_concept, with a formula for standard combinations (free_cash_flow,ebitda,working_capital) or up to three closest friendly names
secedgar_get_snapshot tool
company,taxonomyus-gaap(default) orifrs-full, andperiod_typeannual,quarterly, orboth(default); one companyfacts read covers every supported concept, and nothing is staged as a dataframe- Each
linesentry reports the latestannualandquarterlyvalue for a duration concept, or the latestinstantvalue for balance-sheet and entity-info concepts, each with thetagit came from, under the line'staxonomy; concepts the filer doesn't report land ingapswithtags_tried
secedgar_get_material_events tool
companyplus up to 20itemscodes, dotted (2.02) since 2004-08-23 and single integers (12) before; the two regimes don't overlap, so pair them across the changeover;filed_after/filed_beforework alone and reach into the archive;limit1–1,000, served at 100 (default 20)- Each filing decodes its
itemstocode,label, andregime(current/legacy);item_distributioncounts every code in the window, andtotal_8k_scannedagainsttotal_matchedshows what the filter removed - A date window reads up to 10 archive pages;
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–1,000, served at 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, up to 100 filings;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 filings beyond those parsed, andfilings_other_issuercounts Form 4s the company filed as a reporting owner of another issuer, which add no rows. Price text preserves the numeric digits in fixed-point notation, with at least two decimal places.
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; up to 1,000 is served at 500) withoffset/next_offset;consolidate(default true) merges sub-lines into positions sorted by value- Older quarters are found in the archive (up to 10 pages); a quarter covered only by a 13F-NT notice fails as
no_filings_foundnaming the notice's accession number and period - Holdings, paging, consolidation, and the dataframe cover the quarter composed from the original 13F-HR and its same-period 13F-HR/A amendments, applied in
acceptanceDateTimeorder: aRESTATEMENTreplaces everything composed so far (earlierNEW HOLDINGSincluded); aNEW HOLDINGSamendment appends its rows, unless its table repeats every composed row (CUSIP, class, put/call, discretion, shares, share type), in which case it applies as a restatement. Values normalize by each accession's own filing date. Only tables from the last readable restatement on are read amendments(newest filed first) discloses each accession, filing date, and cover-page type (UNAVAILABLEwith a reason when unreadable), plusapplied_as:restatement,new_holdings,superseded, ornot_applied(type or table unreadable, named in the notice); inspect any withsecedgar_get_filing.composed_accessionslists the filings the rows come from, in application order.amendment_search_complete=falsewarns that bounded or unavailable archive discovery may have missed amendments- Holdings carry issuer, CUSIP,
market_value_usdin whole USD, shares or principal,put_call, andsource_accession_numbers(a consolidated position lists each filing it came from; also a dataframe column);total_holdings_in_filingcounts the original 13F-HR's raw rows,total_holdings_composedthe composed raw rows, andtotal_positionsdistinct composed positions;accession_numberandfiling_datename the original 13F-HR; 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; up to 1,000 is served at 100) 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 - When matching documents exceed the fetch budget, the notice gives an exact
secedgar_search_filingscontinuation with the query, dates and document offset. Filterresults.period_endingto the quarter end, then deduplicate across the sample'sfiler_cikand search results'cik, keeping the latestfiling_date. Advance by 100 documents per full page; EDGAR's paging ceiling is 10,000 documents, and lower-bound totals may hide more beyond it
secedgar_get_beneficial_owners tool
issueris the company being held;form_kindall(default),13D, or13G;include_amendments(default true);limit1–20 matching parsed filings (default 10; up to 1,000 is served at 20). The newest-first scan examines at most 50 documents, excluding schedules naming another issuer; exclusions and failed documents do not consumelimit- 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 total_structured_filingspartitions intofilings_examined+filings_not_examined; examined documents partition intofilings_parsed+filings_excluded_other_issuer+filings_failed.length. Failed accessions carrydocument_not_found(404) orunparseable(including missing issuer identity), and setpartial.truncatedmeans indexed schedules remain unexamined after the result limit or scan ceiling, independently of failures
secedgar_get_fund_holdings tool
fundas a ticker (VOO), series ID (S000002839), or CIK; a multi-series trust fails asseries_requiredwith the series listed, so passseries_id;report_datepicks a period fromavailable_report_periods;limit1–100 (default 20; up to 1,000 is served at 100) 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. Positioncurrencyis its denomination; valuations stay in USD. Percentage text preserves the numeric digits in fixed-point notation. - 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,taxonomyus-gaap(default) ordei,unit(defaultUSD),sortdesc/asc;limit1–100 (default 25; up to 1,000 is served at 100) withoffset/next_offset- One call queries one tag:
unqueried_tagsandrelated_tagslist variants to fetch separately;value_distribution.max_to_p95_ratioflags scale outliers,period_end_rangeshows fiscal-year mixing, andcaveatsflags fiscal-Q4 gaps - A
conceptthat is neither a friendly name nor an UpperCamelCase tag fails asunknown_concept; a well-formed tag with no frame isno_data
secedgar_compare_companies tool
- 2–10
companies× 1–8concepts;taxonomyus-gaap(default) orifrs-full;period_typeannual(default) orquarterly;periods1–12 (default 4; up to 1,000 is served at 12), trimmed further when the inline matrix gets too large cellsalign each value on a calendarperiodand keep itsframe,period_end, and sourcetag; a call with no comparable annual data failsno_comparable_datafailed_companies(reasonnot_found,ambiguous, orno_company_facts) andgapsreport what's missing, andcaveatsflag differing period ends and unit mismatches between companies- An unrecognized concept is listed in
unknown_conceptswith a hint; the call fails asunknown_conceptonly when every concept is unrecognized
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) andpreviewfor fewer inline rows, each served at 10,000 up to 100,000, 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); an unqualifiedcurrent_schemais refused asdenied_functioneven when it names a column, so read such a column as<table>.current_schema
secedgar_dataframe_drop tool
nameof the dataframe to drop; idempotent, returningdropped: falsewhen nothing matched- Off unless
EDGAR_DATAFRAME_DROP_ENABLED=true; when disabled it is absent fromtools/list
secedgar://concepts resource
- The friendly-name catalog grouped by statement, as
text/markdown, with the us-gaap, IFRS, and alternate-definition tags for each concept - The names are what
secedgar_get_financials,secedgar_compare_companies, andsecedgar_fetch_framesaccept as concepts
secedgar://filing-types resource
- Common SEC forms with cadence and use cases, as
text/markdown - Includes the 8-K item-code tables for both numbering regimes, the vocabulary of
secedgar_get_material_events'itemsfilter
secedgar_company_analysis prompt
- Arguments:
companyrequired;focus_areasoptional free text - Returns one user message with a numbered workflow (company search, a financial profile via
secedgar_get_snapshotwith trends viasecedgar_get_financials, filing review, material events, and a peer comparison viasecedgar_compare_companieswithsecedgar_fetch_framesfor a market-wide ranking) and a findings template; insider, institutional, or blockholder terms infocus_areasadd those ownership steps, and "ownership" adds all three
Features
Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
EDGAR-specific:
- Requests are paced under SEC's 10 req/s limit; after a 429, calls fail locally as
rate_limitedwith aretryAftercountdown forEDGAR_RATE_LIMIT_COOLDOWN_SECONDS, then one probe goes out - CIK resolution from tickers (fund tickers included), current and former company names, or raw CIKs, with near-match suggestions on a miss
- Friendly XBRL concept names that handle historical tag changes;
secedgar_get_financials,secedgar_get_snapshot, andsecedgar_compare_companiesshare the same dedup and tag priority, so their numbers agree, andcaveatsflag gaps in the series - 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
Connect to the hosted instance, no install:
claude mcp add --transport http secedgar-mcp-server https://secedgar.caseyjhand.com/mcp
Or run it locally:
claude mcp add secedgar-mcp-server -e EDGAR_USER_AGENT="YourAppName your-email@example.com" -- npx -y @cyanheads/secedgar-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"secedgar-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/secedgar-mcp-server"],
"env": { "EDGAR_USER_AGENT": "YourAppName your-email@example.com" }
}
}
}
SEC requires a User-Agent in "AppName contact@email.com" form on every request; no account or key is needed. For Docker or a source build, see Running the server.
Configuration
| Variable | Description | Default |
|---|---|---|
EDGAR_USER_AGENT | Required. User-Agent sent to SEC, as "AppName contact@email.com". SEC blocks IPs without one. A value that can't be sent as an HTTP header (an em dash or curly quote, an inner line break) stops the server at startup with an error naming this variable. | — |
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. | 600 |
EDGAR_TICKER_CACHE_TTL | Seconds to cache the company and fund ticker files. | 3600 |
EDGAR_DATASET_TTL_SECONDS | TTL for canvas dataframes, renewed on every dataframe operation. | 86400 |
EDGAR_DATAFRAME_DROP_ENABLED | Set true to expose secedgar_dataframe_drop, the only destructive tool. | false |
EDGAR_CLAMP_OVERSIZED_LIMITS | Serve an over-cap limit, content_limit, periods, preview, or row_limit at its cap instead of rejecting it. Set false to reject above the cap. | true |
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 |
LOG_TOOL_FAILURE_PAYLOADS | Log each failed tool call's arguments and result, redacted by key name and capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). A secret inside a free-form value is not redacted. | false |
OTEL_ENABLED | Enable OpenTelemetry instrumentation (spans, metrics, completion logs). | false |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP base URL for traces and metrics. | — |
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT | OTLP log export endpoint (e.g. http://localhost:4318/v1/logs). | — |
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
git clone https://github.com/cyanheads/secedgar-mcp-server.git cd secedgar-mcp-server && bun install cp .env.example .env # set EDGAR_USER_AGENT -
Build and run:
bun run rebuild bun run start:http # or start:stdio; HTTP listens at http://localhost:3010/mcp -
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker run --rm -e EDGAR_USER_AGENT="YourAppName your-email@example.com" -p 3010:3010 ghcr.io/cyanheads/secedgar-mcp-server:latest
The image defaults to HTTP transport on port 3010, stateless session mode, and logs to /var/log/secedgar-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "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"]. To build locally, docker build -t secedgar-mcp-server .; OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
The image 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.