# secedgar-mcp-server [Health: Active]

**Category:** 💻 Developer Tools  
**Repository:** https://github.com/cyanheads/secedgar-mcp-server  
**GitHub Stars:** 10  
**Views:** 0  
**Installs:** 0  
**Upvotes:** 0  
**Directory Page:** https://allmcps.com/mcp/secedgar-mcp-server

## Description
Query SEC EDGAR filings, XBRL financials, and company data through MCP. STDIO & Streamable HTTP.

## Claude Desktop Quick Installation
Install path detected from listing signals. Uses `bunx` (confidence: high):

```json
"mcpServers": {
  "secedgar-mcp-server": {
    "command": "bunx",
    "args": ["@cyanheads/secedgar-mcp-server@latest"]
  }
}
```

## Documentation & README

<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">

[![npm](https://img.shields.io/npm/v/@cyanheads/secedgar-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/secedgar-mcp-server) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/secedgar-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.0.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.0-blueviolet.svg?style=flat-square)](https://bun.sh/)

</div>

<div align="center">

[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/secedgar-mcp-server/releases/latest/download/secedgar-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=secedgar-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvc2VjZWRnYXItbWNwLXNlcnZlciJdLCJlbnYiOnsiRURHQVJfVVNFUl9BR0VOVCI6IllvdXJOYW1lIHlvdXItZW1haWxAZXhhbXBsZS5jb20ifX0=) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22secedgar-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads/secedgar-mcp-server%22%5D%2C%22env%22%3A%7B%22EDGAR_USER_AGENT%22%3A%22YourName%20your-email%40example.com%22%7D%7D)

[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)

</div>

<div align="center">

**Public Hosted Server:** [https://secedgar.caseyjhand.com/mcp](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 include `series_id` and `class_id` for 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_through` discloses the scan depth, and the full filtered history materializes as a `df_<id>` dataframe when it exceeds the inline `filing_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:320193` or `ticker:AAPL`) — scoped server-side by CIK, so filings made under a former company name (same CIK) are included
- Browse mode: omit `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 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 `source` field (`efts` / `submissions` / `full-index`), preserved into the `df_<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, 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 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`, and `location` exist only on `source: efts` rows, 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 with `secedgar_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 `binary` in the document catalog and rejected with a `binary_document` error instead of being returned as decoded bytes
- **Offset paging** for large documents (10-K, S-1/A can exceed 1M chars): pass `next_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 navigation
- **Section targeting** via the `section` param: 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 `limit` caps the inline series to the most-recent N periods; the full series stays queryable via the `df_<id>` dataframe
- Quarterly results carry a `caveats` 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 way
- A further `caveats` 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 short
- See `secedgar://concepts` resource 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 `gaps` with 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_financials` when 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 at `limit`) — query it with `secedgar_dataframe_query` to 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. `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 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: false` for 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_filing` counts raw info-table rows; `total_positions` counts distinct positions after consolidation (both before `limit`)
- Page through a large information table with `offset` — the response echoes the effective `offset` and returns `next_offset` while 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 of `limit` rows) — query it with `secedgar_dataframe_query` for full-filing aggregation or cross-quarter joins on `cusip` + `reporting_period`

---

### `secedgar_find_holders`

Reverse 13F lookup: which institutional managers reported a position in an issuer, for one reporting quarter.

- Searching by `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)
- A CUSIP is not derivable from a ticker anywhere in EDGAR — read one off any `secedgar_get_institutional_holdings` result, or fall back to the name path
- `quarter` 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 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_filings` reports the full count and `dataset.truncated` flags 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_cik` to `secedgar_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 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_filing`
- Amendments carry the current position and are included by default; `include_amendments=false` leaves 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_days` states the gap. Target an earlier period with `report_date`, chosen from the `available_report_periods` in 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 `limit` 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 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_filings` and `secedgar_company_search` cannot 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 `12` 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 filter
- `item_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 end
- A date window pages into the older submissions archive, reaching 8-K filings that predate the ~1000-filing recent window; `history_scanned_through` discloses the scan depth
- The full decode table is in the `secedgar://filing-types` resource
- The full filtered set materializes as a `df_<id>` dataframe with item codes on every row — item frequency over time is one `secedgar_dataframe_query` away

---

### `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 effective `offset` and returns `next_offset` while 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 with `secedgar_dataframe_query`
- `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 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
- `periods` 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_query`
- A company that fails to resolve is reported in `failed_companies` with a machine-readable reason 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 interpolated
- `caveats` 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 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 `NetIncomeLoss` to the supported friendly names
- Surfaces `related_tags` for 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`, and `secedgar://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_drop` is 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_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.

## 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`](https://github.com/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; committed `former-names.json` asset 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`, and `secedgar_find_holders` materialize their full result as a DuckDB-backed canvas dataframe queryable via `secedgar_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:

```json
{
  "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.

```json
{
  "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):

```json
{
  "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:

```sh
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
```

### Prerequisites

- [Bun v1.3.0](https://bun.sh/) or higher.

### Installation

1. **Clone the repository:**

```sh
git clone https://github.com/cyanheads/secedgar-mcp-server.git
```

2. **Navigate into the directory:**

```sh
cd secedgar-mcp-server
```

3. **Install dependencies:**

```sh
bun install
```

4. **Build:**

```sh
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:**

  ```sh
  bun run rebuild
  bun run start:http   # or start:stdio
  ```

- **Run checks and tests:**
  ```sh
  bun run devcheck     # Lints, formats, type-checks
  bun run test         # Runs test suite
  ```

### Docker

```sh
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:

```sh
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`](https://github.com/cyanheads/secedgar-mcp-server/blob/HEAD/CLAUDE.md) and [`AGENTS.md`](https://github.com/cyanheads/secedgar-mcp-server/blob/HEAD/AGENTS.md) for development guidelines and architectural rules. The short version:

- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for logging, `ctx.state` for storage
- Register new tools and resources in the `createApp()` arrays

## Contributing

Issues and pull requests are welcome. Run checks and tests before submitting:

```sh
bun run devcheck
bun run test
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](https://github.com/cyanheads/secedgar-mcp-server/blob/HEAD/LICENSE) file for details.

