@cyanheads/secedgar-mcp-server 0.15.6 → 0.15.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -19
- package/CLAUDE.md +23 -19
- package/README.md +8 -5
- package/changelog/0.15.x/0.15.7.md +36 -0
- package/changelog/template.md +7 -7
- package/dist/config/server-config.d.ts +1 -0
- package/dist/config/server-config.d.ts.map +1 -1
- package/dist/config/server-config.js +7 -0
- package/dist/config/server-config.js.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/resources/definitions/filing-types.resource.js +2 -2
- package/dist/mcp-server/resources/definitions/filing-types.resource.js.map +1 -1
- package/dist/mcp-server/tools/definitions/company-search.tool.d.ts +8 -1
- package/dist/mcp-server/tools/definitions/company-search.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/company-search.tool.js +24 -8
- package/dist/mcp-server/tools/definitions/company-search.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js +8 -0
- package/dist/mcp-server/tools/definitions/compare-companies.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.d.ts +31 -2
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.js +47 -5
- package/dist/mcp-server/tools/definitions/dataframe-query.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.js +8 -0
- package/dist/mcp-server/tools/definitions/fetch-frames.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/find-holders.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.js +9 -1
- package/dist/mcp-server/tools/definitions/find-holders.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js +8 -0
- package/dist/mcp-server/tools/definitions/get-beneficial-owners.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/get-filing.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-filing.tool.js +8 -0
- package/dist/mcp-server/tools/definitions/get-filing.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-financials.tool.js +10 -0
- package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.d.ts +8 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js +9 -1
- package/dist/mcp-server/tools/definitions/get-fund-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.d.ts +9 -2
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js +19 -9
- package/dist/mcp-server/tools/definitions/get-insider-transactions.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts +10 -3
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js +25 -15
- package/dist/mcp-server/tools/definitions/get-institutional-holdings.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/get-material-events.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js +18 -0
- package/dist/mcp-server/tools/definitions/get-material-events.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js +10 -0
- package/dist/mcp-server/tools/definitions/get-snapshot.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/index.d.ts +135 -12
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts +14 -4
- package/dist/mcp-server/tools/definitions/search-filings.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/search-filings.tool.js +37 -18
- package/dist/mcp-server/tools/definitions/search-filings.tool.js.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.d.ts +4 -1
- package/dist/services/canvas-bridge/canvas-bridge.d.ts.map +1 -1
- package/dist/services/canvas-bridge/canvas-bridge.js +4 -1
- package/dist/services/canvas-bridge/canvas-bridge.js.map +1 -1
- package/dist/services/edgar/edgar-api-service.d.ts +29 -10
- package/dist/services/edgar/edgar-api-service.d.ts.map +1 -1
- package/dist/services/edgar/edgar-api-service.js +124 -28
- package/dist/services/edgar/edgar-api-service.js.map +1 -1
- package/package.json +7 -7
- package/server.json +17 -3
package/AGENTS.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** secedgar-mcp-server
|
|
4
|
-
**Version:** 0.15.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.15.7
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
|
|
8
8
|
Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-only, no API keys required. Full design: `docs/sec-edgar-mcp-design.md`.
|
|
@@ -44,14 +44,14 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
44
44
|
|
|
45
45
|
| Name | Description | Key Inputs |
|
|
46
46
|
|:-----|:------------|:-----------|
|
|
47
|
-
| `secedgar_company_search` | Find companies and retrieve entity info with optional recent filings | `query`, `include_filings?`, `
|
|
48
|
-
| `secedgar_search_filings` | Search EDGAR filings since 1993 — full-text (2001+) plus archive-backed browse for pre-2001 ranges | `query?`, `forms?`, `
|
|
47
|
+
| `secedgar_company_search` | Find companies and retrieve entity info with optional recent filings | `query`, `include_filings?`, `forms?`, `filing_limit?`, `filed_after?`, `filed_before?` |
|
|
48
|
+
| `secedgar_search_filings` | Search EDGAR filings since 1993 — full-text (2001+) plus archive-backed browse for pre-2001 ranges | `query?`, `forms?`, `filed_after?`, `filed_before?`, `limit?` |
|
|
49
49
|
| `secedgar_get_filing` | Fetch a specific filing's metadata and document content | `accession_number`, `cik?`, `content_limit?`, `document?` |
|
|
50
50
|
| `secedgar_get_financials` | Get historical XBRL financial data for a company | `company`, `concept`, `taxonomy?`, `period_type?`, `limit?` |
|
|
51
51
|
| `secedgar_get_snapshot` | One-call financial profile — latest value of every supported concept from a single companyfacts read | `company`, `taxonomy?`, `period_type?` |
|
|
52
52
|
| `secedgar_get_material_events` | 8-K filings with item codes decoded, filterable by item | `company`, `items?`, `filed_after?`, `filed_before?`, `limit?` |
|
|
53
|
-
| `secedgar_get_insider_transactions` | Form 4 / 4-A insider transactions parsed from ownership XML | `
|
|
54
|
-
| `secedgar_get_institutional_holdings` | 13F-HR quarterly institutional holdings parsed from the information table | `
|
|
53
|
+
| `secedgar_get_insider_transactions` | Form 4 / 4-A insider transactions parsed from ownership XML | `company`, `transaction_type?`, `limit?` |
|
|
54
|
+
| `secedgar_get_institutional_holdings` | 13F-HR quarterly institutional holdings parsed from the information table | `company`, `quarter?`, `limit?`, `offset?`, `consolidate?` |
|
|
55
55
|
| `secedgar_find_holders` | Reverse 13F lookup — which institutional managers reported holding an issuer | `issuer`, `cusip?`, `quarter?`, `limit?` |
|
|
56
56
|
| `secedgar_get_beneficial_owners` | 5%+ blockholders of an issuer from structured SCHEDULE 13D/13G XML | `issuer`, `form_kind?`, `include_amendments?`, `limit?` |
|
|
57
57
|
| `secedgar_get_fund_holdings` | Fund portfolio holdings from the quarterly NPORT-P report | `fund`, `series_id?`, `report_date?`, `limit?`, `offset?` |
|
|
@@ -79,7 +79,8 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
79
79
|
|
|
80
80
|
## Domain Notes
|
|
81
81
|
|
|
82
|
-
- **Rate limit:** 10 req/s per IP
|
|
82
|
+
- **Rate limit:** 10 req/s per IP. Every SEC request goes through one process-wide `createPacer` queue in `EdgarApiService` that spaces request *starts* `1000 / EDGAR_RATE_LIMIT_RPS` ms apart, with no concurrency cap and no wait budget — an ordinary burst drains at the configured rate and is never shed, and this server's own traffic can never exceed it, so `rateLimitRps` stays at 10 and headroom for a co-tenant on the outbound IP is the operator's call via `EDGAR_RATE_LIMIT_RPS`. A 429 is **not** retried: SEC holds the block until the request rate has stayed under the threshold for ten minutes and every request sent meanwhile restarts that clock, so the ~3s retry budget (`MAX_RETRIES` 3 × `BASE_BACKOFF_MS` 1000, exponential) cannot outlast it (#112). Instead a 429 closes the service's **block gate** for `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` (default 600). While it is closed nothing is sent: a call fails locally as `RateLimited` with `retryAfter` set to the whole seconds remaining (never below 1), and the gate is re-read at dispatch, so a call already queued in the pacer when the 429 landed — or a 5xx retry sleeping through its backoff — is refused too. The first call after the cool-down is sent alone as a probe while every other call waits on it: any HTTP answer but a 429 reopens normal pacing, a probe 429 closes the gate for the full cool-down again, and a probe that gets no HTTP answer (a network error) proves nothing, so the next call probes in its place. **The gate lives in the service, not in the pacer's `cooldown` option** — that gate holds queued calls until it reopens and then releases them all at the pacing rate, so it can neither refuse locally nor keep the reopening to one request. The upstream 429 and the local refusal carry one `data.reason: 'rate_limited'`, declared (`thrownBy: 'service'`) on every tool that reaches SEC, and one recovery hint naming SEC's ten-minute window and the seconds to wait; SEC's HTML block page stays out of the payload (`captureBody: false`). Mirror-served reads (the ticker base behind `resolveCik`, and `tryGetCompanyFacts` / `tryGetCompanyConcept` / `tryGetFrames` through `mirrorOrLive`) never reach `rawFetch`, so they keep answering during a block. 500/502/503/504 still retry with the backoff. `createApp({ teardown })` disposes the pacer through `disposeEdgarApiService()` (#116)
|
|
83
|
+
- **Parameter names:** one name per concept across tools — `company` (the entity a tool reads; on `get_institutional_holdings` that is the 13F filer), `filed_after` / `filed_before` (inclusive filing-date bounds), `forms`. Every tool carrying one of those keys declares `inputAliases` for the other spellings in use — `ticker`, `cik`, `ticker_or_cik` → `company`; `start_date`, `date_from` → `filed_after`; `end_date`, `date_to` → `filed_before`; `form_types` → `forms` — so a caller carrying a name from one tool to the next is not rejected (#115). An alias is declared only where it maps to exactly one key of that tool and its value shape fits: a singular `form_type` cannot fill the `forms` array, so it is left out rather than widening the schema. `issuer`, `fund`, `companies`, and `query` name different roles and are not aliased. `search_filings` still filters by date only with both bounds; `company_search` and `get_material_events` accept either alone
|
|
83
84
|
- **User-Agent required:** `"AppName contact@email.com"` on every SEC request or IP gets blocked
|
|
84
85
|
- **CIK zero-padding:** URLs require 10-digit zero-padded CIK (`String(cik).padStart(10, '0')`)
|
|
85
86
|
- **ETF/mutual-fund tickers:** `loadTickerCache` fetches `company_tickers_mf.json` alongside `company_tickers.json` and merges fund symbols (VOO, SCHD, JEPI…) into `byTicker`. Fund CIKs are registrant trusts (1:many with series), so MF entries go into `byTicker` only — not `byCik`. Resolved MF matches carry `seriesId`/`classId` from the live MF file. On a mirror-served ticker base the live MF fetch is merged in when `EDGAR_MIRROR_FALLBACK_LIVE` is on, so fund tickers resolve even if the mirror predates MF ingestion (#43); strict mirror-only (`FALLBACK_LIVE=false`) uses the mirror's own MF rows (ticker→CIK, no series/class).
|
|
@@ -97,7 +98,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
97
98
|
- **Heading detection:** `detectHeadings` runs three line-anchored patterns — the all-caps forms, the mixed-case same-line `Item N. Title` / `Part II` forms (#71), and a bare `Item N` marker whose title lands on a later line. Every whitespace match is horizontal-only: `\s` matches `\n`, which fused runs of consecutive all-caps lines into composite entries (`"TABLE OF CONTENTS\n\nPART I"`) (#105). Horizontal-only is not space-or-tab — the all-caps arm alternates `[^\S\n]` in rather than listing literal members, because filings put NBSP inside all-caps headings too (`MICROSOFT<NBSP>CORPORATION`). The Item suffix spans `16A`–`16K` and the marker's period is optional — 20-F filers write the lettered markers unpunctuated. **A bare marker is admitted on the shape of what follows, not just on a title being there:** a blank line between marker and title admits the pair; without one, the pair is admitted only when no bare page-number line sits under the title, since marker/title/page-number on three tight lines is the TOC-cell artifact #71's guard exists to reject. That test is one-way — it rejects a TOC row, it does not certify a heading — so a marker line anywhere else (a page footer, an unwrapped paragraph) is admitted too, taking the next line as its title; the entry carries the marker's own offset, so `section` still lands in the right place. A heading text repeated `RUNNING_HEADER_MIN_OCCURRENCES` (5) times at distinct offsets is page furniture — C3is' FY2025 20-F repeats `TABLE OF CONTENTS` 129 times and a 10-Q stamps `PART I` on all 47 of its pages, while a heading printed once in the TOC and once in the body occurs twice. **Furniture is then handled by shape, two ways:** a bare marker (`Item 1`, `PART I`) carries no navigable text, so no occurrence of it earns an entry and every one is dropped; anything else keeps its FIRST occurrence and drops the rest, inverting the keep-later rule — a filing that runs a section's own heading across every page of that section (`NOTES TO THE CONSOLIDATED FINANCIAL STATEMENTS` through an S-1's financial statements) prints it first at the section start, and keep-later would land it on the section's last page instead. So the outline carries no fused composite and at most one entry per running header, at its first occurrence. **A bare marker is counted on its own line, never on the joined heading:** a page footer's marker takes its title from whatever prose the next page opens with, so every composite it produces is a different string and only the marker line repeats — count composites instead and a 10-Q's per-page `Item 1` footer yields one bogus entry per page, crowding the real Items past `maxEntries`. That count is per rendered form (`Item 1` and `Item 1.` are separate), since a filing stamping the bare form on every page still writes the punctuated one for the real heading. Dedup (keep the later, i.e. body, occurrence) keys on `foldForHeadingMatch` plus a terminal-punctuation strip, never raw lowercase: the same filing prints one marker with different space characters and a trailing period in its TOC and its body (`Item 3.` with U+0020 vs U+2009; `…Policies` vs `…Policies.`), and a raw key leaves both in the outline. Trailing bare page numbers are stripped from Item headings for the same reason. Lettered sub-item markers (a bare `A.`/`D.` inside an Item) are deliberately not detected — single letters recur throughout filing prose.
|
|
98
99
|
- **Section matching:** `get_filing`'s `section` compares through `foldForHeadingMatch` (`filing-to-text.ts`) on **both** operands — Unicode whitespace runs collapse to one space, U+2018/U+2019 → `'`, U+201C/U+201D → `"`, then lowercase. Comparison-time only: `FilingHeading.heading`, the `outline` output, `content[]`, and every character offset keep the document's own bytes. EDGAR headings carry NBSP runs and typographic quotes, so a caller re-sending an outline heading through a UI that normalizes either one never byte-matched the heading it came from — which broke the `section_not_found` recovery loop the error's own hint points at (#106). Still a plain substring test, not fuzzy matching — `"item 8"` resolves only to Item 8 — but run on folded operands, so a single-spaced needle now also reaches a heading whose marker carries an NBSP run or a double space (`Item 9.` in a 2004 10-K). Folding only one operand does not fix the round trip.
|
|
99
100
|
- **Binary filing documents:** a filing index routinely carries scanned pages, PDF exhibits, `Financial_Report.xlsx`, and `-xbrl.zip` — `filingToExtract` runs `html-to-text` over whatever bytes arrive and never inspects a content type, so requesting one used to return decoded binary as `content` with no error (#96). `get-filing.tool.ts` detects them by filename extension (`binaryTypeFromName`), because that is the only signal available before the body is fetched: the canonical `GRAPHIC` type lives in the submission header, which the archive path fetches *after* the document, and requiring it would trade today's parallel header/submissions fetch for a sequential one. Two halves, both needed — `documents[]` entries carry `binary: true` (`isBinaryDocument` also honors a header `GRAPHIC` type), and `resolveFilingArchive` throws `binary_document` before `tryGetFilingDocument` runs, since the `document` input is an unconstrained string and a catalog marker alone cannot stop a caller. Extension-driven and bucket-independent on purpose: a PDF exhibit is typed `EX-99.*` and files under `exhibits`, not among the graphics.
|
|
100
|
-
- **Dataframes:** `secedgar_search_filings`, `secedgar_get_financials`, `secedgar_fetch_frames`, `secedgar_compare_companies`, `secedgar_get_insider_transactions`, `secedgar_get_institutional_holdings`, `secedgar_get_material_events`, `secedgar_find_holders`, `secedgar_get_beneficial_owners`, and `secedgar_get_fund_holdings` materialize their full result set as `df_<id>` on a shared DuckDB-backed canvas (one per tenant). The two ownership tools register the full parsed set (inline response is a preview capped at `limit`); `get_insider_transactions` scans at least `INSIDER_CANVAS_FILING_SCAN` recent filings when canvas is available (so small inline limits don't under-scan the window), capped at 100, and flags `dataset.truncated` when more Form 4 filings exist beyond the scanned window (#63); its `shares_traded` is an unsigned magnitude paired with a `direction` (acquire/dispose) column, so net activity is `SUM(CASE WHEN direction='dispose' THEN -shares_traded ELSE shares_traded END)` (#46); `get_financials`' dataframe materializes the source-filing fiscal keys as `source_filing_fy`/`source_filing_fp` (renamed from bare `fiscal_year`/`fiscal_period` — every comparative period restated in one filing carries that filing's fy/fp, so `period_end` is the time key for ORDER BY/GROUP BY/windowing) (#72); `compare_companies`' dataframe holds the FULL aligned series across every period while the inline matrix is capped at `periods`, and each row carries both the aligned `period` key and the underlying XBRL `frame` (they differ for point-in-time concepts) (#85). Each row set carries an all-nullable schema (framework's `inferSchemaFromRows` always emits `nullable: true` since mcp-ts-core 0.10.4). Per-table TTL is delegated to the framework via `RegisterTableOptions.ttlMs` (resolved in mcp-ts-core#140); bridge still tracks metadata expiry in `ctx.state` for provenance. Every producer emits the describe-then-query pointer on the branch that registers the dataframe, composed once by `dataframeGuidance(dataset)` in the bridge so the wording cannot drift; where a truncation call already fires there it rides as that call's `guidance`, since `ctx.enrich.notice` is last-wins across `notice`/`truncated` and a second call would clobber the first (#104). `secedgar_dataframe_query` detects a `row_limit`-bound result from `QueryResult.truncated` rather than `rowCount > rows.length` — `row_limit` is pushed into the query as the provider's cap, so a capped result comes back with `rowCount === rows.length` and the arithmetic cannot see it; `rowCount` is then the cap and not a total, so that branch names the cap instead of printing "of N rows", `row_count_capped` carries the same disclosure into `format()`, and the fallback comparison stays for the `registerAs` path, where `truncated` is never set. A SQL `LIMIT` exactly equal to the cap is indistinguishable from a result that holds that many rows and is reported as exact (#109). `secedgar_dataframe_query` runs framework's SQL gate with `denySystemCatalogs: true` to block `information_schema`, `pg_catalog`, `sqlite_master`, and `duckdb_*` catalog access. Raw DuckDB execution errors (missing table, syntax) are caught in `bridge.query` and re-thrown as structured `missing_table`/`invalid_sql` reasons, with DuckDB's "Did you mean …?" hint stripped so internal catalog names don't leak (#47). `get_beneficial_owners` registers one row per reporting person rather than one per filing, since powers and percent of class are per person; `get_fund_holdings` registers every position in the report while the inline list is one `limit`-sized page from `offset`, and its rows carry the fund keys (`series_id`, `registrant_cik`, `report_period_date`, `accession_number`) so a portfolio joins the 13F and insider rows on `cusip`.
|
|
101
|
+
- **Dataframes:** `secedgar_search_filings`, `secedgar_get_financials`, `secedgar_fetch_frames`, `secedgar_compare_companies`, `secedgar_get_insider_transactions`, `secedgar_get_institutional_holdings`, `secedgar_get_material_events`, `secedgar_find_holders`, `secedgar_get_beneficial_owners`, and `secedgar_get_fund_holdings` materialize their full result set as `df_<id>` on a shared DuckDB-backed canvas (one per tenant). The two ownership tools register the full parsed set (inline response is a preview capped at `limit`); `get_insider_transactions` scans at least `INSIDER_CANVAS_FILING_SCAN` recent filings when canvas is available (so small inline limits don't under-scan the window), capped at 100, and flags `dataset.truncated` when more Form 4 filings exist beyond the scanned window (#63); its `shares_traded` is an unsigned magnitude paired with a `direction` (acquire/dispose) column, so net activity is `SUM(CASE WHEN direction='dispose' THEN -shares_traded ELSE shares_traded END)` (#46); `get_financials`' dataframe materializes the source-filing fiscal keys as `source_filing_fy`/`source_filing_fp` (renamed from bare `fiscal_year`/`fiscal_period` — every comparative period restated in one filing carries that filing's fy/fp, so `period_end` is the time key for ORDER BY/GROUP BY/windowing) (#72); `compare_companies`' dataframe holds the FULL aligned series across every period while the inline matrix is capped at `periods`, and each row carries both the aligned `period` key and the underlying XBRL `frame` (they differ for point-in-time concepts) (#85). Each row set carries an all-nullable schema (framework's `inferSchemaFromRows` always emits `nullable: true` since mcp-ts-core 0.10.4). Per-table TTL is delegated to the framework via `RegisterTableOptions.ttlMs` (resolved in mcp-ts-core#140); bridge still tracks metadata expiry in `ctx.state` for provenance. Every producer emits the describe-then-query pointer on the branch that registers the dataframe, composed once by `dataframeGuidance(dataset)` in the bridge so the wording cannot drift; where a truncation call already fires there it rides as that call's `guidance`, since `ctx.enrich.notice` is last-wins across `notice`/`truncated` and a second call would clobber the first (#104). `secedgar_dataframe_query` detects a `row_limit`-bound result from `QueryResult.truncated` rather than `rowCount > rows.length` — `row_limit` is pushed into the query as the provider's cap, so a capped result comes back with `rowCount === rows.length` and the arithmetic cannot see it; `rowCount` is then the cap and not a total, so that branch names the cap instead of printing "of N rows", `row_count_capped` carries the same disclosure into `format()`, and the fallback comparison stays for the `registerAs` path, where `truncated` is never set. A SQL `LIMIT` exactly equal to the cap is indistinguishable from a result that holds that many rows and is reported as exact (#109). `secedgar_dataframe_query` runs framework's SQL gate with `denySystemCatalogs: true` to block `information_schema`, `pg_catalog`, `sqlite_master`, and `duckdb_*` catalog access. Raw DuckDB execution errors (missing table, syntax) are caught in `bridge.query` and re-thrown as structured `missing_table`/`invalid_sql` reasons, with DuckDB's "Did you mean …?" hint stripped so internal catalog names don't leak (#47). The framework's own classifications pass through untouched and are declared on the tool's contract (`thrownBy: 'service'`, since the handler never names them): a SELECT that prepares and then fails on the data — a `Conversion` / `Invalid Input` / `Out of Range` error, e.g. `CAST('abc' AS INTEGER)` — is `ValidationError` with `sql_execution_error` (mcp-ts-core 0.13.4; `DatabaseError` with no reason before), and the gate's `multi_statement`, `denied_function`, and `plan_operator_not_allowed` rejections arrive with their own recovery hints. `get_beneficial_owners` registers one row per reporting person rather than one per filing, since powers and percent of class are per person; `get_fund_holdings` registers every position in the report while the inline list is one `limit`-sized page from `offset`, and its rows carry the fund keys (`series_id`, `registrant_cik`, `report_period_date`, `accession_number`) so a portfolio joins the 13F and insider rows on `cusip`.
|
|
101
102
|
- **Beneficial ownership (13D/13G):** SEC replaced the legacy `SC 13D` / `SC 13G` text filings with structured XML under the `SCHEDULE 13D` / `SCHEDULE 13G` names on **2024-12-18**; the changeover is total, so a legacy-name filter matches nothing recent and a current-name filter matches nothing older. The two schedules are **separate schemas, not one shape with optional fields** — 13D repeats `reportingPersons/reportingPersonInfo` with the power fields flat on each person and the percentage in `percentOfClass`; 13G repeats `coverPageHeaderReportingPersonDetails` with the powers nested inside `reportingPersonBeneficiallyOwnedNumberOfShares`, the percentage in `classPercent`, no reporting-person CIK anywhere, and no purpose-of-transaction field at all (its item 4 is structured ownership data, which is what makes it the passive form). `beneficial-ownership-parser.ts` therefore branches per schema and dispatches on the submission type, falling back to whichever repeated reporting-person element the document carries. **Ownership powers and percent of class are per reporting person, including on joint filings** — a fund, its adviser, and its controlling principal each report the same underlying shares, so the parsed owner list is never collapsed to a group total and the percentages must not be summed. Discovery runs off the issuer's own `getSubmissions()`: SEC cross-lists a schedule under every CIK party to it, so no entity-scoped EFTS step is needed.
|
|
102
103
|
- **NPORT-P series routing:** a fund's public portfolio report covers **exactly one series**, a registrant trust holds many (iShares Trust: 390 listed series, 200+ reports for a single period), and the series identity appears **only inside the document** — neither the submissions feed nor EFTS carries `seriesId` on an NPORT-P row. Routing is therefore series-first: EDGAR's company browse accepts a series ID in place of a CIK (`getFundSeriesFilings`), which is the one SEC surface mapping a series to its accessions and which also names the registrant, so a bare series ID resolves in one request. The browse feed's registrant outranks any registrant the `fund` input implied, since `series_id` can name a series of an entirely different trust and the archive path is keyed on the registrant. The bounded header scan (`tryGetFilingDocumentHead` + `parseNportHeader`, cap `SERIES_SCAN_LIMIT`) is the fallback and the disambiguator for a trust whose series carry no listed ticker; SEC's archive host **ignores `Range`** (a ranged request answers 200 with the whole body), so the saving is client-side stream cancellation only — and the cutoff cannot cut below one read chunk, which Bun sets at 262,144 bytes, so `HEADER_MAX_BYTES` binds only on a document arriving in several chunks. **A registrant's own NPORT-P list is never the candidate set for a series-structured trust**: series fiscal quarters are staggered, so the list interleaves funds and one report at the newest period does not mean one fund — it means the fund whose quarter ends latest. The scan therefore runs on the newest batch even when that batch holds one report, and a series found there re-routes through the browse feed; only a report naming no series at all (a closed-end fund, which also carries no `classId`) is answered from the registrant's list. Candidates are ordered by **reported period, not filing date**: an `NPORT-P/A` restating an old period is filed after every report of the periods that followed it. Reports publish on a ~60-day lag, so `report_period_date` and `publication_lag_days` are first-class output and the `as_of` enrichment states both.
|
|
103
104
|
- **Local mirror (opt-in, `EDGAR_MIRROR_ENABLED`):** routes `resolveCik`, `tryGetCompanyConcept`, `tryGetCompanyFacts`, and `tryGetFrames` to a local SQLite mirror (framework `MirrorService`) of `company_tickers.json` + the `companyfacts.zip` bulk archive; the live API is the fallback on a miss (`EDGAR_MIRROR_FALLBACK_LIVE`). Bootstrap out-of-band with `bun run mirror:init`; refresh nightly via cron (HTTP) or `bun run mirror:refresh`. The three `mirror:*` commands also ship in the production Docker image — run them against a deployed container with `docker exec <container> bun run mirror:<init|refresh|verify>`. Node/Bun only — skipped on Workers. Frames are assembled from the company-facts store, so `loc` (business location) is absent. No FTS5 — every routed lookup is exact/indexed (cik+taxonomy+tag point, taxonomy+tag scan, cik scan for the whole-company read, ticker/CIK). Mirror ingests MF fund symbols from `company_tickers_mf.json`; on the mirror path a live MF merge supplements them when `FALLBACK_LIVE` is on (adding series/class and covering a mirror synced before MF ingestion), so funds resolve there too (#43).
|
|
@@ -110,9 +111,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
110
111
|
|:--------|:---------|:--------|:------------|
|
|
111
112
|
| `EDGAR_USER_AGENT` | **Yes** | — | User-Agent for SEC compliance. Format: `"AppName contact@email.com"` |
|
|
112
113
|
| `EDGAR_RATE_LIMIT_RPS` | No | `10` | Max requests/second to SEC APIs. Do not exceed 10. |
|
|
114
|
+
| `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` | No | `600` | Seconds to stop sending to SEC after a 429. Calls are refused locally for this long, then the first one goes out alone as a probe. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. |
|
|
113
115
|
| `EDGAR_TICKER_CACHE_TTL` | No | `3600` | Seconds to cache company_tickers.json |
|
|
114
116
|
| `EDGAR_DATASET_TTL_SECONDS` | No | `86400` | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. |
|
|
115
|
-
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` on mcp-ts-core 0.13.
|
|
117
|
+
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` on mcp-ts-core 0.13.6 carries no per-tool definitions at all, so nothing changes there (#103). |
|
|
116
118
|
| `EDGAR_MIRROR_ENABLED` | No | `false` | Enable the local SQLite mirror of company_tickers + XBRL company-facts. Node/Bun only (skipped on Workers). Bootstrap once with `bun run mirror:init`. |
|
|
117
119
|
| `EDGAR_MIRROR_PATH` | No | `./data/edgar-mirror` | Directory holding the mirror SQLite databases (tickers + companyfacts). |
|
|
118
120
|
| `EDGAR_MIRROR_REFRESH_CRON` | No | — | In-process nightly refresh cron (HTTP transport only). Recommended `0 9 * * *`. Omit to refresh out-of-band via `bun run mirror:refresh`. |
|
|
@@ -125,7 +127,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
125
127
|
|
|
126
128
|
| Module | Path | Purpose |
|
|
127
129
|
|:-------|:-----|:--------|
|
|
128
|
-
| `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` |
|
|
130
|
+
| `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` | Paced HTTP client with the rate-limit block gate, CIK resolution, all SEC API calls |
|
|
129
131
|
| `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list |
|
|
130
132
|
| `concept-series` | `src/services/edgar/concept-series.ts` | Shared frame dedup + tag selection (priority or per-filer coverage); one value per standard calendar period; series-staleness caveats |
|
|
131
133
|
| `fiscal-periods` | `src/services/edgar/fiscal-periods.ts` | Fiscal-Q4 frame caveat and per-filer missing-quarter detection |
|
|
@@ -162,7 +164,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
162
164
|
|
|
163
165
|
Handlers throw — the framework catches, classifies, and formats.
|
|
164
166
|
|
|
165
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire; pass an explicit `{ recovery: { hint } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
167
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire (mirrored into `content[]` unless the message already contains it verbatim); pass an explicit `{ recovery: { hint } }` when runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). Mark an entry raised below the handler — a module-level helper, a service, the canvas bridge — with `thrownBy: 'service'` so `error-contract-unthrown` skips it; lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
166
168
|
|
|
167
169
|
```ts
|
|
168
170
|
errors: [
|
|
@@ -177,7 +179,7 @@ async handler(input, ctx) {
|
|
|
177
179
|
}
|
|
178
180
|
```
|
|
179
181
|
|
|
180
|
-
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`.
|
|
182
|
+
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with `(reason <reason> · not retryable)` whenever `data.reason` or `data.retryable` is set, so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly.
|
|
181
183
|
|
|
182
184
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype code): use error factories or plain `Error`.
|
|
183
185
|
|
|
@@ -197,12 +199,12 @@ See framework CLAUDE.md and the `api-errors` skill for the full reference.
|
|
|
197
199
|
|
|
198
200
|
```text
|
|
199
201
|
src/
|
|
200
|
-
index.ts # createApp() entry point — stateless sessionMode, teardown closes the mirror
|
|
202
|
+
index.ts # createApp() entry point — stateless sessionMode, teardown stops the SEC pacer and closes the mirror
|
|
201
203
|
config/
|
|
202
204
|
server-config.ts # EDGAR env vars (Zod schema)
|
|
203
205
|
services/
|
|
204
206
|
edgar/
|
|
205
|
-
edgar-api-service.ts #
|
|
207
|
+
edgar-api-service.ts # Paced HTTP client + rate-limit block gate, CIK resolution
|
|
206
208
|
concept-map.ts # Friendly name → XBRL tag mapping + taxonomy/tag resolution
|
|
207
209
|
concept-series.ts # Shared frame dedup, tag selection, staleness caveats
|
|
208
210
|
fiscal-periods.ts # Fiscal Q4 / missing-quarter caveats
|
|
@@ -274,7 +276,7 @@ Available skills:
|
|
|
274
276
|
| `setup` | Post-init project orientation |
|
|
275
277
|
| `design-mcp-server` | Design tool surface, resources, and services for a new server |
|
|
276
278
|
| `add-tool` | Scaffold a new tool definition |
|
|
277
|
-
| `add-app-tool` | Scaffold an MCP App tool + UI resource
|
|
279
|
+
| `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
|
|
278
280
|
| `add-resource` | Scaffold a new resource definition |
|
|
279
281
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
280
282
|
| `add-service` | Scaffold a new service integration |
|
|
@@ -285,7 +287,7 @@ Available skills:
|
|
|
285
287
|
| `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
|
|
286
288
|
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
287
289
|
| `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
|
|
288
|
-
| `release-pr-review` | Review pass on an open release PR — simplifier + correctness review,
|
|
290
|
+
| `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
|
|
289
291
|
| `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
|
|
290
292
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
291
293
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
@@ -295,10 +297,10 @@ Available skills:
|
|
|
295
297
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
296
298
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
297
299
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
298
|
-
| `api-context` | Context interface, logger, state,
|
|
300
|
+
| `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
|
|
299
301
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
300
302
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
301
|
-
| `api-mirror` | MirrorService: persistent local mirror of a bulk upstream dataset
|
|
303
|
+
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
|
302
304
|
| `api-services` | LLM, Speech, Graph services |
|
|
303
305
|
| `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
|
|
304
306
|
| `api-testing` | createMockContext, test patterns |
|
|
@@ -335,6 +337,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
335
337
|
| `bun run start:stdio` | Production mode (stdio) |
|
|
336
338
|
| `bun run start:http` | Production mode (HTTP) |
|
|
337
339
|
|
|
340
|
+
**CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
|
|
341
|
+
|
|
338
342
|
---
|
|
339
343
|
|
|
340
344
|
## Bundling
|
|
@@ -364,7 +368,7 @@ Directory-based. Source of truth is `changelog/<major.minor>.x/<version>.md` —
|
|
|
364
368
|
|
|
365
369
|
## Publishing
|
|
366
370
|
|
|
367
|
-
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (
|
|
371
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
368
372
|
|
|
369
373
|
After a version bump and final commit, publish to both npm and GHCR:
|
|
370
374
|
|
package/CLAUDE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** secedgar-mcp-server
|
|
4
|
-
**Version:** 0.15.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.15.7
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.6`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
7
|
|
|
8
8
|
Query SEC EDGAR filings, XBRL financials, and company data through MCP. Read-only, no API keys required. Full design: `docs/sec-edgar-mcp-design.md`.
|
|
@@ -44,14 +44,14 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
44
44
|
|
|
45
45
|
| Name | Description | Key Inputs |
|
|
46
46
|
|:-----|:------------|:-----------|
|
|
47
|
-
| `secedgar_company_search` | Find companies and retrieve entity info with optional recent filings | `query`, `include_filings?`, `
|
|
48
|
-
| `secedgar_search_filings` | Search EDGAR filings since 1993 — full-text (2001+) plus archive-backed browse for pre-2001 ranges | `query?`, `forms?`, `
|
|
47
|
+
| `secedgar_company_search` | Find companies and retrieve entity info with optional recent filings | `query`, `include_filings?`, `forms?`, `filing_limit?`, `filed_after?`, `filed_before?` |
|
|
48
|
+
| `secedgar_search_filings` | Search EDGAR filings since 1993 — full-text (2001+) plus archive-backed browse for pre-2001 ranges | `query?`, `forms?`, `filed_after?`, `filed_before?`, `limit?` |
|
|
49
49
|
| `secedgar_get_filing` | Fetch a specific filing's metadata and document content | `accession_number`, `cik?`, `content_limit?`, `document?` |
|
|
50
50
|
| `secedgar_get_financials` | Get historical XBRL financial data for a company | `company`, `concept`, `taxonomy?`, `period_type?`, `limit?` |
|
|
51
51
|
| `secedgar_get_snapshot` | One-call financial profile — latest value of every supported concept from a single companyfacts read | `company`, `taxonomy?`, `period_type?` |
|
|
52
52
|
| `secedgar_get_material_events` | 8-K filings with item codes decoded, filterable by item | `company`, `items?`, `filed_after?`, `filed_before?`, `limit?` |
|
|
53
|
-
| `secedgar_get_insider_transactions` | Form 4 / 4-A insider transactions parsed from ownership XML | `
|
|
54
|
-
| `secedgar_get_institutional_holdings` | 13F-HR quarterly institutional holdings parsed from the information table | `
|
|
53
|
+
| `secedgar_get_insider_transactions` | Form 4 / 4-A insider transactions parsed from ownership XML | `company`, `transaction_type?`, `limit?` |
|
|
54
|
+
| `secedgar_get_institutional_holdings` | 13F-HR quarterly institutional holdings parsed from the information table | `company`, `quarter?`, `limit?`, `offset?`, `consolidate?` |
|
|
55
55
|
| `secedgar_find_holders` | Reverse 13F lookup — which institutional managers reported holding an issuer | `issuer`, `cusip?`, `quarter?`, `limit?` |
|
|
56
56
|
| `secedgar_get_beneficial_owners` | 5%+ blockholders of an issuer from structured SCHEDULE 13D/13G XML | `issuer`, `form_kind?`, `include_amendments?`, `limit?` |
|
|
57
57
|
| `secedgar_get_fund_holdings` | Fund portfolio holdings from the quarterly NPORT-P report | `fund`, `series_id?`, `report_date?`, `limit?`, `offset?` |
|
|
@@ -79,7 +79,8 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
79
79
|
|
|
80
80
|
## Domain Notes
|
|
81
81
|
|
|
82
|
-
- **Rate limit:** 10 req/s per IP
|
|
82
|
+
- **Rate limit:** 10 req/s per IP. Every SEC request goes through one process-wide `createPacer` queue in `EdgarApiService` that spaces request *starts* `1000 / EDGAR_RATE_LIMIT_RPS` ms apart, with no concurrency cap and no wait budget — an ordinary burst drains at the configured rate and is never shed, and this server's own traffic can never exceed it, so `rateLimitRps` stays at 10 and headroom for a co-tenant on the outbound IP is the operator's call via `EDGAR_RATE_LIMIT_RPS`. A 429 is **not** retried: SEC holds the block until the request rate has stayed under the threshold for ten minutes and every request sent meanwhile restarts that clock, so the ~3s retry budget (`MAX_RETRIES` 3 × `BASE_BACKOFF_MS` 1000, exponential) cannot outlast it (#112). Instead a 429 closes the service's **block gate** for `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` (default 600). While it is closed nothing is sent: a call fails locally as `RateLimited` with `retryAfter` set to the whole seconds remaining (never below 1), and the gate is re-read at dispatch, so a call already queued in the pacer when the 429 landed — or a 5xx retry sleeping through its backoff — is refused too. The first call after the cool-down is sent alone as a probe while every other call waits on it: any HTTP answer but a 429 reopens normal pacing, a probe 429 closes the gate for the full cool-down again, and a probe that gets no HTTP answer (a network error) proves nothing, so the next call probes in its place. **The gate lives in the service, not in the pacer's `cooldown` option** — that gate holds queued calls until it reopens and then releases them all at the pacing rate, so it can neither refuse locally nor keep the reopening to one request. The upstream 429 and the local refusal carry one `data.reason: 'rate_limited'`, declared (`thrownBy: 'service'`) on every tool that reaches SEC, and one recovery hint naming SEC's ten-minute window and the seconds to wait; SEC's HTML block page stays out of the payload (`captureBody: false`). Mirror-served reads (the ticker base behind `resolveCik`, and `tryGetCompanyFacts` / `tryGetCompanyConcept` / `tryGetFrames` through `mirrorOrLive`) never reach `rawFetch`, so they keep answering during a block. 500/502/503/504 still retry with the backoff. `createApp({ teardown })` disposes the pacer through `disposeEdgarApiService()` (#116)
|
|
83
|
+
- **Parameter names:** one name per concept across tools — `company` (the entity a tool reads; on `get_institutional_holdings` that is the 13F filer), `filed_after` / `filed_before` (inclusive filing-date bounds), `forms`. Every tool carrying one of those keys declares `inputAliases` for the other spellings in use — `ticker`, `cik`, `ticker_or_cik` → `company`; `start_date`, `date_from` → `filed_after`; `end_date`, `date_to` → `filed_before`; `form_types` → `forms` — so a caller carrying a name from one tool to the next is not rejected (#115). An alias is declared only where it maps to exactly one key of that tool and its value shape fits: a singular `form_type` cannot fill the `forms` array, so it is left out rather than widening the schema. `issuer`, `fund`, `companies`, and `query` name different roles and are not aliased. `search_filings` still filters by date only with both bounds; `company_search` and `get_material_events` accept either alone
|
|
83
84
|
- **User-Agent required:** `"AppName contact@email.com"` on every SEC request or IP gets blocked
|
|
84
85
|
- **CIK zero-padding:** URLs require 10-digit zero-padded CIK (`String(cik).padStart(10, '0')`)
|
|
85
86
|
- **ETF/mutual-fund tickers:** `loadTickerCache` fetches `company_tickers_mf.json` alongside `company_tickers.json` and merges fund symbols (VOO, SCHD, JEPI…) into `byTicker`. Fund CIKs are registrant trusts (1:many with series), so MF entries go into `byTicker` only — not `byCik`. Resolved MF matches carry `seriesId`/`classId` from the live MF file. On a mirror-served ticker base the live MF fetch is merged in when `EDGAR_MIRROR_FALLBACK_LIVE` is on, so fund tickers resolve even if the mirror predates MF ingestion (#43); strict mirror-only (`FALLBACK_LIVE=false`) uses the mirror's own MF rows (ticker→CIK, no series/class).
|
|
@@ -97,7 +98,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
97
98
|
- **Heading detection:** `detectHeadings` runs three line-anchored patterns — the all-caps forms, the mixed-case same-line `Item N. Title` / `Part II` forms (#71), and a bare `Item N` marker whose title lands on a later line. Every whitespace match is horizontal-only: `\s` matches `\n`, which fused runs of consecutive all-caps lines into composite entries (`"TABLE OF CONTENTS\n\nPART I"`) (#105). Horizontal-only is not space-or-tab — the all-caps arm alternates `[^\S\n]` in rather than listing literal members, because filings put NBSP inside all-caps headings too (`MICROSOFT<NBSP>CORPORATION`). The Item suffix spans `16A`–`16K` and the marker's period is optional — 20-F filers write the lettered markers unpunctuated. **A bare marker is admitted on the shape of what follows, not just on a title being there:** a blank line between marker and title admits the pair; without one, the pair is admitted only when no bare page-number line sits under the title, since marker/title/page-number on three tight lines is the TOC-cell artifact #71's guard exists to reject. That test is one-way — it rejects a TOC row, it does not certify a heading — so a marker line anywhere else (a page footer, an unwrapped paragraph) is admitted too, taking the next line as its title; the entry carries the marker's own offset, so `section` still lands in the right place. A heading text repeated `RUNNING_HEADER_MIN_OCCURRENCES` (5) times at distinct offsets is page furniture — C3is' FY2025 20-F repeats `TABLE OF CONTENTS` 129 times and a 10-Q stamps `PART I` on all 47 of its pages, while a heading printed once in the TOC and once in the body occurs twice. **Furniture is then handled by shape, two ways:** a bare marker (`Item 1`, `PART I`) carries no navigable text, so no occurrence of it earns an entry and every one is dropped; anything else keeps its FIRST occurrence and drops the rest, inverting the keep-later rule — a filing that runs a section's own heading across every page of that section (`NOTES TO THE CONSOLIDATED FINANCIAL STATEMENTS` through an S-1's financial statements) prints it first at the section start, and keep-later would land it on the section's last page instead. So the outline carries no fused composite and at most one entry per running header, at its first occurrence. **A bare marker is counted on its own line, never on the joined heading:** a page footer's marker takes its title from whatever prose the next page opens with, so every composite it produces is a different string and only the marker line repeats — count composites instead and a 10-Q's per-page `Item 1` footer yields one bogus entry per page, crowding the real Items past `maxEntries`. That count is per rendered form (`Item 1` and `Item 1.` are separate), since a filing stamping the bare form on every page still writes the punctuated one for the real heading. Dedup (keep the later, i.e. body, occurrence) keys on `foldForHeadingMatch` plus a terminal-punctuation strip, never raw lowercase: the same filing prints one marker with different space characters and a trailing period in its TOC and its body (`Item 3.` with U+0020 vs U+2009; `…Policies` vs `…Policies.`), and a raw key leaves both in the outline. Trailing bare page numbers are stripped from Item headings for the same reason. Lettered sub-item markers (a bare `A.`/`D.` inside an Item) are deliberately not detected — single letters recur throughout filing prose.
|
|
98
99
|
- **Section matching:** `get_filing`'s `section` compares through `foldForHeadingMatch` (`filing-to-text.ts`) on **both** operands — Unicode whitespace runs collapse to one space, U+2018/U+2019 → `'`, U+201C/U+201D → `"`, then lowercase. Comparison-time only: `FilingHeading.heading`, the `outline` output, `content[]`, and every character offset keep the document's own bytes. EDGAR headings carry NBSP runs and typographic quotes, so a caller re-sending an outline heading through a UI that normalizes either one never byte-matched the heading it came from — which broke the `section_not_found` recovery loop the error's own hint points at (#106). Still a plain substring test, not fuzzy matching — `"item 8"` resolves only to Item 8 — but run on folded operands, so a single-spaced needle now also reaches a heading whose marker carries an NBSP run or a double space (`Item 9.` in a 2004 10-K). Folding only one operand does not fix the round trip.
|
|
99
100
|
- **Binary filing documents:** a filing index routinely carries scanned pages, PDF exhibits, `Financial_Report.xlsx`, and `-xbrl.zip` — `filingToExtract` runs `html-to-text` over whatever bytes arrive and never inspects a content type, so requesting one used to return decoded binary as `content` with no error (#96). `get-filing.tool.ts` detects them by filename extension (`binaryTypeFromName`), because that is the only signal available before the body is fetched: the canonical `GRAPHIC` type lives in the submission header, which the archive path fetches *after* the document, and requiring it would trade today's parallel header/submissions fetch for a sequential one. Two halves, both needed — `documents[]` entries carry `binary: true` (`isBinaryDocument` also honors a header `GRAPHIC` type), and `resolveFilingArchive` throws `binary_document` before `tryGetFilingDocument` runs, since the `document` input is an unconstrained string and a catalog marker alone cannot stop a caller. Extension-driven and bucket-independent on purpose: a PDF exhibit is typed `EX-99.*` and files under `exhibits`, not among the graphics.
|
|
100
|
-
- **Dataframes:** `secedgar_search_filings`, `secedgar_get_financials`, `secedgar_fetch_frames`, `secedgar_compare_companies`, `secedgar_get_insider_transactions`, `secedgar_get_institutional_holdings`, `secedgar_get_material_events`, `secedgar_find_holders`, `secedgar_get_beneficial_owners`, and `secedgar_get_fund_holdings` materialize their full result set as `df_<id>` on a shared DuckDB-backed canvas (one per tenant). The two ownership tools register the full parsed set (inline response is a preview capped at `limit`); `get_insider_transactions` scans at least `INSIDER_CANVAS_FILING_SCAN` recent filings when canvas is available (so small inline limits don't under-scan the window), capped at 100, and flags `dataset.truncated` when more Form 4 filings exist beyond the scanned window (#63); its `shares_traded` is an unsigned magnitude paired with a `direction` (acquire/dispose) column, so net activity is `SUM(CASE WHEN direction='dispose' THEN -shares_traded ELSE shares_traded END)` (#46); `get_financials`' dataframe materializes the source-filing fiscal keys as `source_filing_fy`/`source_filing_fp` (renamed from bare `fiscal_year`/`fiscal_period` — every comparative period restated in one filing carries that filing's fy/fp, so `period_end` is the time key for ORDER BY/GROUP BY/windowing) (#72); `compare_companies`' dataframe holds the FULL aligned series across every period while the inline matrix is capped at `periods`, and each row carries both the aligned `period` key and the underlying XBRL `frame` (they differ for point-in-time concepts) (#85). Each row set carries an all-nullable schema (framework's `inferSchemaFromRows` always emits `nullable: true` since mcp-ts-core 0.10.4). Per-table TTL is delegated to the framework via `RegisterTableOptions.ttlMs` (resolved in mcp-ts-core#140); bridge still tracks metadata expiry in `ctx.state` for provenance. Every producer emits the describe-then-query pointer on the branch that registers the dataframe, composed once by `dataframeGuidance(dataset)` in the bridge so the wording cannot drift; where a truncation call already fires there it rides as that call's `guidance`, since `ctx.enrich.notice` is last-wins across `notice`/`truncated` and a second call would clobber the first (#104). `secedgar_dataframe_query` detects a `row_limit`-bound result from `QueryResult.truncated` rather than `rowCount > rows.length` — `row_limit` is pushed into the query as the provider's cap, so a capped result comes back with `rowCount === rows.length` and the arithmetic cannot see it; `rowCount` is then the cap and not a total, so that branch names the cap instead of printing "of N rows", `row_count_capped` carries the same disclosure into `format()`, and the fallback comparison stays for the `registerAs` path, where `truncated` is never set. A SQL `LIMIT` exactly equal to the cap is indistinguishable from a result that holds that many rows and is reported as exact (#109). `secedgar_dataframe_query` runs framework's SQL gate with `denySystemCatalogs: true` to block `information_schema`, `pg_catalog`, `sqlite_master`, and `duckdb_*` catalog access. Raw DuckDB execution errors (missing table, syntax) are caught in `bridge.query` and re-thrown as structured `missing_table`/`invalid_sql` reasons, with DuckDB's "Did you mean …?" hint stripped so internal catalog names don't leak (#47). `get_beneficial_owners` registers one row per reporting person rather than one per filing, since powers and percent of class are per person; `get_fund_holdings` registers every position in the report while the inline list is one `limit`-sized page from `offset`, and its rows carry the fund keys (`series_id`, `registrant_cik`, `report_period_date`, `accession_number`) so a portfolio joins the 13F and insider rows on `cusip`.
|
|
101
|
+
- **Dataframes:** `secedgar_search_filings`, `secedgar_get_financials`, `secedgar_fetch_frames`, `secedgar_compare_companies`, `secedgar_get_insider_transactions`, `secedgar_get_institutional_holdings`, `secedgar_get_material_events`, `secedgar_find_holders`, `secedgar_get_beneficial_owners`, and `secedgar_get_fund_holdings` materialize their full result set as `df_<id>` on a shared DuckDB-backed canvas (one per tenant). The two ownership tools register the full parsed set (inline response is a preview capped at `limit`); `get_insider_transactions` scans at least `INSIDER_CANVAS_FILING_SCAN` recent filings when canvas is available (so small inline limits don't under-scan the window), capped at 100, and flags `dataset.truncated` when more Form 4 filings exist beyond the scanned window (#63); its `shares_traded` is an unsigned magnitude paired with a `direction` (acquire/dispose) column, so net activity is `SUM(CASE WHEN direction='dispose' THEN -shares_traded ELSE shares_traded END)` (#46); `get_financials`' dataframe materializes the source-filing fiscal keys as `source_filing_fy`/`source_filing_fp` (renamed from bare `fiscal_year`/`fiscal_period` — every comparative period restated in one filing carries that filing's fy/fp, so `period_end` is the time key for ORDER BY/GROUP BY/windowing) (#72); `compare_companies`' dataframe holds the FULL aligned series across every period while the inline matrix is capped at `periods`, and each row carries both the aligned `period` key and the underlying XBRL `frame` (they differ for point-in-time concepts) (#85). Each row set carries an all-nullable schema (framework's `inferSchemaFromRows` always emits `nullable: true` since mcp-ts-core 0.10.4). Per-table TTL is delegated to the framework via `RegisterTableOptions.ttlMs` (resolved in mcp-ts-core#140); bridge still tracks metadata expiry in `ctx.state` for provenance. Every producer emits the describe-then-query pointer on the branch that registers the dataframe, composed once by `dataframeGuidance(dataset)` in the bridge so the wording cannot drift; where a truncation call already fires there it rides as that call's `guidance`, since `ctx.enrich.notice` is last-wins across `notice`/`truncated` and a second call would clobber the first (#104). `secedgar_dataframe_query` detects a `row_limit`-bound result from `QueryResult.truncated` rather than `rowCount > rows.length` — `row_limit` is pushed into the query as the provider's cap, so a capped result comes back with `rowCount === rows.length` and the arithmetic cannot see it; `rowCount` is then the cap and not a total, so that branch names the cap instead of printing "of N rows", `row_count_capped` carries the same disclosure into `format()`, and the fallback comparison stays for the `registerAs` path, where `truncated` is never set. A SQL `LIMIT` exactly equal to the cap is indistinguishable from a result that holds that many rows and is reported as exact (#109). `secedgar_dataframe_query` runs framework's SQL gate with `denySystemCatalogs: true` to block `information_schema`, `pg_catalog`, `sqlite_master`, and `duckdb_*` catalog access. Raw DuckDB execution errors (missing table, syntax) are caught in `bridge.query` and re-thrown as structured `missing_table`/`invalid_sql` reasons, with DuckDB's "Did you mean …?" hint stripped so internal catalog names don't leak (#47). The framework's own classifications pass through untouched and are declared on the tool's contract (`thrownBy: 'service'`, since the handler never names them): a SELECT that prepares and then fails on the data — a `Conversion` / `Invalid Input` / `Out of Range` error, e.g. `CAST('abc' AS INTEGER)` — is `ValidationError` with `sql_execution_error` (mcp-ts-core 0.13.4; `DatabaseError` with no reason before), and the gate's `multi_statement`, `denied_function`, and `plan_operator_not_allowed` rejections arrive with their own recovery hints. `get_beneficial_owners` registers one row per reporting person rather than one per filing, since powers and percent of class are per person; `get_fund_holdings` registers every position in the report while the inline list is one `limit`-sized page from `offset`, and its rows carry the fund keys (`series_id`, `registrant_cik`, `report_period_date`, `accession_number`) so a portfolio joins the 13F and insider rows on `cusip`.
|
|
101
102
|
- **Beneficial ownership (13D/13G):** SEC replaced the legacy `SC 13D` / `SC 13G` text filings with structured XML under the `SCHEDULE 13D` / `SCHEDULE 13G` names on **2024-12-18**; the changeover is total, so a legacy-name filter matches nothing recent and a current-name filter matches nothing older. The two schedules are **separate schemas, not one shape with optional fields** — 13D repeats `reportingPersons/reportingPersonInfo` with the power fields flat on each person and the percentage in `percentOfClass`; 13G repeats `coverPageHeaderReportingPersonDetails` with the powers nested inside `reportingPersonBeneficiallyOwnedNumberOfShares`, the percentage in `classPercent`, no reporting-person CIK anywhere, and no purpose-of-transaction field at all (its item 4 is structured ownership data, which is what makes it the passive form). `beneficial-ownership-parser.ts` therefore branches per schema and dispatches on the submission type, falling back to whichever repeated reporting-person element the document carries. **Ownership powers and percent of class are per reporting person, including on joint filings** — a fund, its adviser, and its controlling principal each report the same underlying shares, so the parsed owner list is never collapsed to a group total and the percentages must not be summed. Discovery runs off the issuer's own `getSubmissions()`: SEC cross-lists a schedule under every CIK party to it, so no entity-scoped EFTS step is needed.
|
|
102
103
|
- **NPORT-P series routing:** a fund's public portfolio report covers **exactly one series**, a registrant trust holds many (iShares Trust: 390 listed series, 200+ reports for a single period), and the series identity appears **only inside the document** — neither the submissions feed nor EFTS carries `seriesId` on an NPORT-P row. Routing is therefore series-first: EDGAR's company browse accepts a series ID in place of a CIK (`getFundSeriesFilings`), which is the one SEC surface mapping a series to its accessions and which also names the registrant, so a bare series ID resolves in one request. The browse feed's registrant outranks any registrant the `fund` input implied, since `series_id` can name a series of an entirely different trust and the archive path is keyed on the registrant. The bounded header scan (`tryGetFilingDocumentHead` + `parseNportHeader`, cap `SERIES_SCAN_LIMIT`) is the fallback and the disambiguator for a trust whose series carry no listed ticker; SEC's archive host **ignores `Range`** (a ranged request answers 200 with the whole body), so the saving is client-side stream cancellation only — and the cutoff cannot cut below one read chunk, which Bun sets at 262,144 bytes, so `HEADER_MAX_BYTES` binds only on a document arriving in several chunks. **A registrant's own NPORT-P list is never the candidate set for a series-structured trust**: series fiscal quarters are staggered, so the list interleaves funds and one report at the newest period does not mean one fund — it means the fund whose quarter ends latest. The scan therefore runs on the newest batch even when that batch holds one report, and a series found there re-routes through the browse feed; only a report naming no series at all (a closed-end fund, which also carries no `classId`) is answered from the registrant's list. Candidates are ordered by **reported period, not filing date**: an `NPORT-P/A` restating an old period is filed after every report of the periods that followed it. Reports publish on a ~60-day lag, so `report_period_date` and `publication_lag_days` are first-class output and the `as_of` enrichment states both.
|
|
103
104
|
- **Local mirror (opt-in, `EDGAR_MIRROR_ENABLED`):** routes `resolveCik`, `tryGetCompanyConcept`, `tryGetCompanyFacts`, and `tryGetFrames` to a local SQLite mirror (framework `MirrorService`) of `company_tickers.json` + the `companyfacts.zip` bulk archive; the live API is the fallback on a miss (`EDGAR_MIRROR_FALLBACK_LIVE`). Bootstrap out-of-band with `bun run mirror:init`; refresh nightly via cron (HTTP) or `bun run mirror:refresh`. The three `mirror:*` commands also ship in the production Docker image — run them against a deployed container with `docker exec <container> bun run mirror:<init|refresh|verify>`. Node/Bun only — skipped on Workers. Frames are assembled from the company-facts store, so `loc` (business location) is absent. No FTS5 — every routed lookup is exact/indexed (cik+taxonomy+tag point, taxonomy+tag scan, cik scan for the whole-company read, ticker/CIK). Mirror ingests MF fund symbols from `company_tickers_mf.json`; on the mirror path a live MF merge supplements them when `FALLBACK_LIVE` is on (adding series/class and covering a mirror synced before MF ingestion), so funds resolve there too (#43).
|
|
@@ -110,9 +111,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
110
111
|
|:--------|:---------|:--------|:------------|
|
|
111
112
|
| `EDGAR_USER_AGENT` | **Yes** | — | User-Agent for SEC compliance. Format: `"AppName contact@email.com"` |
|
|
112
113
|
| `EDGAR_RATE_LIMIT_RPS` | No | `10` | Max requests/second to SEC APIs. Do not exceed 10. |
|
|
114
|
+
| `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` | No | `600` | Seconds to stop sending to SEC after a 429. Calls are refused locally for this long, then the first one goes out alone as a probe. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. |
|
|
113
115
|
| `EDGAR_TICKER_CACHE_TTL` | No | `3600` | Seconds to cache company_tickers.json |
|
|
114
116
|
| `EDGAR_DATASET_TTL_SECONDS` | No | `86400` | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. |
|
|
115
|
-
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` on mcp-ts-core 0.13.
|
|
117
|
+
| `EDGAR_DATAFRAME_DROP_ENABLED` | No | `false` | Set to `true` to expose `secedgar_dataframe_drop`. TTL handles cleanup otherwise. Off, the tool is registered through `disabledTool()`: absent from `tools/list` and uncallable, but rendered on the HTTP landing page in a `disabled` group carrying the reason and `EDGAR_DATAFRAME_DROP_ENABLED=true`, so an operator can see the capability exists. The list `createApp()` receives — and `buildServerManifest()`'s `definitionCounts.tools` — is 17 either way; `/.well-known/mcp.json` on mcp-ts-core 0.13.6 carries no per-tool definitions at all, so nothing changes there (#103). |
|
|
116
118
|
| `EDGAR_MIRROR_ENABLED` | No | `false` | Enable the local SQLite mirror of company_tickers + XBRL company-facts. Node/Bun only (skipped on Workers). Bootstrap once with `bun run mirror:init`. |
|
|
117
119
|
| `EDGAR_MIRROR_PATH` | No | `./data/edgar-mirror` | Directory holding the mirror SQLite databases (tickers + companyfacts). |
|
|
118
120
|
| `EDGAR_MIRROR_REFRESH_CRON` | No | — | In-process nightly refresh cron (HTTP transport only). Recommended `0 9 * * *`. Omit to refresh out-of-band via `bun run mirror:refresh`. |
|
|
@@ -125,7 +127,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
125
127
|
|
|
126
128
|
| Module | Path | Purpose |
|
|
127
129
|
|:-------|:-----|:--------|
|
|
128
|
-
| `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` |
|
|
130
|
+
| `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` | Paced HTTP client with the rate-limit block gate, CIK resolution, all SEC API calls |
|
|
129
131
|
| `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list |
|
|
130
132
|
| `concept-series` | `src/services/edgar/concept-series.ts` | Shared frame dedup + tag selection (priority or per-filer coverage); one value per standard calendar period; series-staleness caveats |
|
|
131
133
|
| `fiscal-periods` | `src/services/edgar/fiscal-periods.ts` | Fiscal-Q4 frame caveat and per-filer missing-quarter detection |
|
|
@@ -162,7 +164,7 @@ Handlers receive a unified `ctx` object. Key properties:
|
|
|
162
164
|
|
|
163
165
|
Handlers throw — the framework catches, classifies, and formats.
|
|
164
166
|
|
|
165
|
-
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire; pass an explicit `{ recovery: { hint } }` when runtime context matters. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
167
|
+
**Recommended: typed error contract.** Declare `errors: [{ reason, code, when, recovery, retryable?, severity?, thrownBy? }]` on `tool()`. The handler then receives `ctx.fail(reason, msg?, data?)` typed against the reason union, and `data.reason` is auto-populated for observability. The `recovery` field is required (≥5 words, lint-validated). Use `ctx.recoveryFor('reason')` to spread the contract recovery onto the wire (mirrored into `content[]` unless the message already contains it verbatim); pass an explicit `{ recovery: { hint } }` when runtime context matters. Forwarding it is lint-enforced per throw site (`error-contract-recovery-unforwarded`). Mark an entry raised below the handler — a module-level helper, a service, the canvas bridge — with `thrownBy: 'service'` so `error-contract-unthrown` skips it; lint-only metadata, nothing at runtime reads it. Baseline codes (`InternalError`, `ServiceUnavailable`, `Timeout`, `ValidationError`, `SerializationError`, `RequestCancelled`) bubble freely without declaration. **All EDGAR tools that have known failure modes use this pattern.**
|
|
166
168
|
|
|
167
169
|
```ts
|
|
168
170
|
errors: [
|
|
@@ -177,7 +179,7 @@ async handler(input, ctx) {
|
|
|
177
179
|
}
|
|
178
180
|
```
|
|
179
181
|
|
|
180
|
-
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`.
|
|
182
|
+
**Service-layer pattern (no `ctx`).** Throw an `McpError` with `data: { reason, recovery: { hint } }`, and mark the tool's matching `errors[]` entry `thrownBy: 'service'`. The auto-classifier preserves `data` on the wire so clients see the same `error.data.reason` they'd see from `ctx.fail`. The tool error text closes with `(reason <reason> · not retryable)` whenever `data.reason` or `data.retryable` is set, so tests assert that `content[0].text` contains the diagnostic rather than pinning it exactly.
|
|
181
183
|
|
|
182
184
|
**Fallback for ad-hoc throws** (no contract entry fits, prototype code): use error factories or plain `Error`.
|
|
183
185
|
|
|
@@ -197,12 +199,12 @@ See framework CLAUDE.md and the `api-errors` skill for the full reference.
|
|
|
197
199
|
|
|
198
200
|
```text
|
|
199
201
|
src/
|
|
200
|
-
index.ts # createApp() entry point — stateless sessionMode, teardown closes the mirror
|
|
202
|
+
index.ts # createApp() entry point — stateless sessionMode, teardown stops the SEC pacer and closes the mirror
|
|
201
203
|
config/
|
|
202
204
|
server-config.ts # EDGAR env vars (Zod schema)
|
|
203
205
|
services/
|
|
204
206
|
edgar/
|
|
205
|
-
edgar-api-service.ts #
|
|
207
|
+
edgar-api-service.ts # Paced HTTP client + rate-limit block gate, CIK resolution
|
|
206
208
|
concept-map.ts # Friendly name → XBRL tag mapping + taxonomy/tag resolution
|
|
207
209
|
concept-series.ts # Shared frame dedup, tag selection, staleness caveats
|
|
208
210
|
fiscal-periods.ts # Fiscal Q4 / missing-quarter caveats
|
|
@@ -274,7 +276,7 @@ Available skills:
|
|
|
274
276
|
| `setup` | Post-init project orientation |
|
|
275
277
|
| `design-mcp-server` | Design tool surface, resources, and services for a new server |
|
|
276
278
|
| `add-tool` | Scaffold a new tool definition |
|
|
277
|
-
| `add-app-tool` | Scaffold an MCP App tool + UI resource
|
|
279
|
+
| `add-app-tool` | Scaffold an MCP App tool + paired UI resource |
|
|
278
280
|
| `add-resource` | Scaffold a new resource definition |
|
|
279
281
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
280
282
|
| `add-service` | Scaffold a new service integration |
|
|
@@ -285,7 +287,7 @@ Available skills:
|
|
|
285
287
|
| `code-simplifier` | Post-session cleanup against `git diff` — modernize syntax, consolidate duplication, align with the codebase |
|
|
286
288
|
| `polish-docs-meta` | Finalize docs, README, metadata, and agent protocol for shipping |
|
|
287
289
|
| `git-wrapup` | Land working-tree changes as a commit stack — version bump, changelog, verify, commit by concern, release commit on top. No tag, no push to main; opens the release PR when the project declares release PR mode |
|
|
288
|
-
| `release-pr-review` | Review pass on an open release PR — simplifier + correctness review,
|
|
290
|
+
| `release-pr-review` | Review pass on an open release PR — simplifier + correctness review, fixes as ordinary commits on top of the stack, PR body kept in sync. Release PR mode only |
|
|
289
291
|
| `release-and-publish` | Fast-forward merge (release PR mode) + tag + push + npm + MCP Registry + GH Release + Docker. Picks up from `git-wrapup` |
|
|
290
292
|
| `report-issue-local` | File a bug or feature request against this server's own repo via `gh` CLI |
|
|
291
293
|
| `report-issue-framework` | File a bug or feature request against `@cyanheads/mcp-ts-core` via `gh` CLI |
|
|
@@ -295,10 +297,10 @@ Available skills:
|
|
|
295
297
|
| `api-auth` | Auth modes, scopes, JWT/OAuth |
|
|
296
298
|
| `api-config` | AppConfig, parseConfig, env vars |
|
|
297
299
|
| `api-canvas` | DataCanvas: register tabular data, run SQL, export, plus the `spillover()` helper for big result sets — Tier 3 opt-in |
|
|
298
|
-
| `api-context` | Context interface, logger, state,
|
|
300
|
+
| `api-context` | Context interface, RequestContext, logger, state, multi-round-trip input |
|
|
299
301
|
| `api-errors` | McpError, JsonRpcErrorCode, error patterns |
|
|
300
302
|
| `api-linter` | Definition linter rule catalog — invoked by `bun run lint:mcp` and `devcheck` |
|
|
301
|
-
| `api-mirror` | MirrorService: persistent local mirror of a bulk upstream dataset
|
|
303
|
+
| `api-mirror` | MirrorService: persistent self-refreshing local mirror (embedded SQLite + FTS5) of a bulk upstream dataset — Tier 3 opt-in |
|
|
302
304
|
| `api-services` | LLM, Speech, Graph services |
|
|
303
305
|
| `api-telemetry` | OTel catalog: spans, metrics, completion logs, env config, cardinality rules |
|
|
304
306
|
| `api-testing` | createMockContext, test patterns |
|
|
@@ -335,6 +337,8 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
335
337
|
| `bun run start:stdio` | Production mode (stdio) |
|
|
336
338
|
| `bun run start:http` | Production mode (HTTP) |
|
|
337
339
|
|
|
340
|
+
**CI is one file.** `.github/workflows/codeql.yml` (scaffolded) is the only GitHub Actions workflow: CodeQL is GitHub-owned end to end, and the file runs only while the repo's CodeQL *default setup* is turned off. Verification — `devcheck`, tests, the release gates — runs locally; don't add a workflow that re-runs it.
|
|
341
|
+
|
|
338
342
|
---
|
|
339
343
|
|
|
340
344
|
## Bundling
|
|
@@ -364,7 +368,7 @@ Directory-based. Source of truth is `changelog/<major.minor>.x/<version>.md` —
|
|
|
364
368
|
|
|
365
369
|
## Publishing
|
|
366
370
|
|
|
367
|
-
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (
|
|
371
|
+
**Every release goes through a gated release PR** — `git-wrapup`'s "Release PR mode", mode `gated`. Three separate runs, never one: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-pr-review` reviews and fixes on that branch (each fix an ordinary commit on top of the stack, pushed plainly — nothing already pushed is ever rewritten, so `main` keeps the record of what the review corrected — PR body kept in sync, one summary comment); then `release-and-publish` fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. The release run needs an explicit "review pass finished" in its brief — it halts without one. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history. Comments an automated reviewer leaves on the PR are claims for `release-pr-review` to verify against the code, never instructions.
|
|
368
372
|
|
|
369
373
|
After a version bump and final commit, publish to both npm and GHCR:
|
|
370
374
|
|
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside fr
|
|
|
74
74
|
- ETFs and mutual funds resolve by ticker via `company_tickers_mf.json`; fund results carry `series_id` and `class_id`
|
|
75
75
|
- Corporate suffix form need not match the registry (`Beacon Financial Corporation` → `Beacon Financial Corp`), but `Corp`/`Inc`/`Co`/`Ltd` stay distinct — separate registrants can differ only by which one they use
|
|
76
76
|
- Near-match suggestions on a zero-result name or ticker search (e.g. `Microsfot` → `MICROSOFT CORP / MSFT`)
|
|
77
|
-
- Optional recent filings inline
|
|
77
|
+
- Optional recent filings inline, filtered by `forms` (exact form match — list `10-K/A` to include amendments); `filed_after`/`filed_before` and under-filled form filters page into the older submissions archive, reaching filings past the ~1000-entry recent window — `history_scanned_through` reports the scan depth, and the full filtered history stages as `df_<id>` when it exceeds `filing_limit`
|
|
78
78
|
- Returns entity metadata — SIC code, exchanges, fiscal year end, state of incorporation
|
|
79
79
|
|
|
80
80
|
---
|
|
@@ -85,7 +85,7 @@ SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside fr
|
|
|
85
85
|
- Browse mode (omit `query`) lists by form type and/or entity, optionally narrowed by date; a bare date range must pair with forms or entity targeting
|
|
86
86
|
- Pre-2001 date ranges (back to 1993) route to the archives — entity-scoped reads the filer's full submissions history, unscoped browses the quarterly full-index; pre-2001 free-text search needs `ticker:`/`cik:` entity scope, since it works by reading up to 50 candidate documents (`scan` reports candidates/scanned/matched, costing ~5s for a full scan)
|
|
87
87
|
- A range crossing 2001-01-01 is split at the boundary and merged, each row tagged with its `source` (`efts`/`submissions`/`full-index`); `period_ending`, `ticker`, `file_description`, `sic`, and `location` exist only on `efts` rows
|
|
88
|
-
-
|
|
88
|
+
- Filing-date range (`filed_after` + `filed_before`, both inclusive, both required together) and form filtering (`forms`, amendments included), pagination up to 10,000 results; response includes form distribution for narrowing follow-up searches
|
|
89
89
|
- The full result set stages as `df_<id>` when it exceeds the inline limit
|
|
90
90
|
|
|
91
91
|
---
|
|
@@ -136,6 +136,7 @@ SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside fr
|
|
|
136
136
|
### `secedgar_get_insider_transactions` <sub>tool</sub>
|
|
137
137
|
|
|
138
138
|
- Parses Form 4 / 4-A insider transactions from 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`
|
|
139
|
+
- `company` is the issuer — a ticker, CIK, or company name; a name matching several companies resolves to the top-ranked match, so pass a ticker or CIK when the issuer must be exact
|
|
139
140
|
- Reporting person, relationship to issuer (director, officer + title, 10% owner), and transaction date
|
|
140
141
|
- 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
|
|
141
142
|
- Filter by `transaction_type` (`purchase`, `sale`, `all`); scans newest filings first
|
|
@@ -145,7 +146,7 @@ SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside fr
|
|
|
145
146
|
|
|
146
147
|
### `secedgar_get_institutional_holdings` <sub>tool</sub>
|
|
147
148
|
|
|
148
|
-
- 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
|
|
149
|
+
- Pass the institutional filer as `company` (CIK, ticker, or full legal name, e.g. `0000102909` for Vanguard) to see what it holds; for the reverse direction — which managers hold a given company — use `secedgar_find_holders`, whose `filer_cik` results feed straight back into this tool
|
|
149
150
|
- Each holding: issuer name, CUSIP, market value (whole USD), shares/principal, and put/call; raw rows also carry investment discretion
|
|
150
151
|
- Sub-lines for the same security are consolidated into distinct positions sorted by value by default — pass `consolidate: false` for raw filing rows
|
|
151
152
|
- Resolves the filing-manager name and reporting quarter from the cover page; target a specific quarter with `quarter` (e.g. `"2025-Q4"`)
|
|
@@ -257,7 +258,7 @@ SEC EDGAR filings, XBRL financials, and company ownership data, keyless aside fr
|
|
|
257
258
|
|
|
258
259
|
- Common SEC filing types with descriptions, cadence, and typical use cases, returned as `text/markdown`
|
|
259
260
|
- Includes the full 8-K item-code decode tables for both numbering regimes (the current dotted scheme and the pre-2004-08-23 legacy integers)
|
|
260
|
-
- Helps choose the `forms`
|
|
261
|
+
- Helps choose the `forms` filter for `secedgar_search_filings` and `secedgar_company_search`, or the `items` filter for `secedgar_get_material_events`
|
|
261
262
|
|
|
262
263
|
---
|
|
263
264
|
|
|
@@ -274,7 +275,7 @@ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): s
|
|
|
274
275
|
|
|
275
276
|
EDGAR-specific:
|
|
276
277
|
|
|
277
|
-
-
|
|
278
|
+
- One request queue per process paces SEC calls under the 10 req/s limit; a 429 is never retried — it stops all outbound SEC traffic for the ten-minute block (`EDGAR_RATE_LIMIT_COOLDOWN_SECONDS`), refusing calls locally with a `retryAfter` countdown so the block can clear, then sends a single probe before resuming. Reads served from the opt-in local mirror keep answering throughout
|
|
278
279
|
- CIK resolution from tickers (including ETFs and mutual funds via `company_tickers_mf.json`), current and former company names, or raw CIK numbers, with local caching, corporate-suffix normalization, and near-match trigram suggestions on zero-result queries
|
|
279
280
|
- Friendly XBRL concept name mapping with historical tag-change handling and a searchable, reverse-lookupable concept catalog
|
|
280
281
|
- HTML-to-text conversion for filing documents, with heading detection and offset-based paging for oversized filings
|
|
@@ -285,6 +286,7 @@ Agent-friendly output:
|
|
|
285
286
|
- In-conversation SQL analytics — data-returning tools materialize their full result as a DuckDB-backed canvas dataframe (`df_<id>`); inspect its columns with `secedgar_dataframe_describe`, then query with `secedgar_dataframe_query`
|
|
286
287
|
- Discriminated outputs — `source` fields on merged filing-search rows (`efts`/`submissions`/`full-index`), typed `caveats` entries for series staleness and fiscal-Q4 gaps, and `gaps`/`failed_companies` rows instead of silent omission
|
|
287
288
|
- Graceful partial failure — `secedgar_compare_companies` returns every resolved company alongside `failed_companies` and per-concept `gaps` rather than failing the whole request
|
|
289
|
+
- One parameter name per concept — `company`, `filed_after`/`filed_before`, and `forms` mean the same thing on every tool that takes them, and each tool also accepts the other common spellings (`ticker`, `cik`, `ticker_or_cik`, `start_date`/`end_date`, `date_from`/`date_to`, `form_types`), so a name carried over from another tool is not rejected
|
|
288
290
|
- Provenance on scan and staleness — `history_scanned_through`, `publication_lag_days`, and `dataset.truncated` let agents reason about scan depth, report lag, and completeness
|
|
289
291
|
|
|
290
292
|
## Getting started
|
|
@@ -407,6 +409,7 @@ All configuration is validated at startup via Zod schemas in `src/config/server-
|
|
|
407
409
|
|:---|:---|:---|
|
|
408
410
|
| `EDGAR_USER_AGENT` | **Required.** User-Agent header for SEC compliance. Format: `"AppName contact@email.com"`. SEC blocks IPs without a valid User-Agent. | — |
|
|
409
411
|
| `EDGAR_RATE_LIMIT_RPS` | Max requests/second to SEC APIs. Do not exceed 10. | `10` |
|
|
412
|
+
| `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` | Seconds to stop sending to SEC after a 429. Calls are refused locally with a `retryAfter` countdown, then one probe request goes out. SEC lifts its block only after ten quiet minutes, so a shorter value just probes into it. | `600` |
|
|
410
413
|
| `EDGAR_TICKER_CACHE_TTL` | Seconds to cache the company tickers lookup file. | `3600` |
|
|
411
414
|
| `EDGAR_DATASET_TTL_SECONDS` | Per-table TTL for canvas-registered dataframes. Sliding window touched on every dataframe op. | `86400` |
|
|
412
415
|
| `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` |
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "After a 429, calls are refused locally for SEC's cool-down instead of each reaching SEC and restarting its block. company/filed_after/filed_before/forms replace ticker_or_cik/start_date/end_date/form_types, old spellings accepted as aliases. secedgar_dataframe_query fixes a backslash-escaping bug and gains a fuller SQL error contract."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.15.7 — 2026-09-21
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **SEC rate-limit block gate** ([#116](https://github.com/cyanheads/secedgar-mcp-server/issues/116)) — after SEC answers a 429, nothing more is sent to SEC for `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS` (default `600`): calls fail locally with reason `rate_limited` and `retryAfter` set to the seconds remaining, the first call after the cool-down goes out alone as a probe, and mirror-served reads keep answering throughout. A 429 is still never retried. `rate_limited` is declared, `thrownBy: 'service'`, on every tool that reaches SEC.
|
|
12
|
+
- **`EDGAR_RATE_LIMIT_COOLDOWN_SECONDS`** env var (default `600`) — documented in `.env.example`, `manifest.json`, and `server.json`.
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **One parameter name per concept, with the other spellings accepted as aliases** ([#115](https://github.com/cyanheads/secedgar-mcp-server/issues/115)) — `company` replaces `ticker_or_cik` on `secedgar_get_insider_transactions` and `secedgar_get_institutional_holdings`; `filed_after`/`filed_before` replace `start_date`/`end_date` on `secedgar_search_filings`; `forms` replaces `form_types` on `secedgar_company_search`. Every tool carrying one of those keys declares `inputAliases` for the retired spelling, each sibling tool's spelling, and `ticker`/`cik`/`date_from`/`date_to`, so a name carried over from another tool is never rejected. Dataframe provenance (`query_params` in `secedgar_dataframe_describe`) now records the canonical names.
|
|
17
|
+
- **Server `instructions` rewritten**, shorter and less repetitive across tool mentions.
|
|
18
|
+
- **mcp-ts-core 0.13.3–0.13.6 adoption**: an argument rejection carries reason `invalid_arguments` and a `Recovery:` line derived from the schema, naming the accepted keys when a key is unknown (0.13.3, [cyanheads/mcp-ts-core#445](https://github.com/cyanheads/mcp-ts-core/issues/445)); `_meta`, `tool_call_description`, and `toolCallId` are dropped from arguments rather than rejected, a case-style key variant (e.g. `Company`) resolves to its declared key, and a JSON-stringified array is repaired against the schema (0.13.4, [cyanheads/mcp-ts-core#453](https://github.com/cyanheads/mcp-ts-core/issues/453), [cyanheads/mcp-ts-core#452](https://github.com/cyanheads/mcp-ts-core/issues/452), [cyanheads/mcp-ts-core#234](https://github.com/cyanheads/mcp-ts-core/issues/234)); tool error text now closes with `(reason <reason>)`, adding `· retryable` or `· not retryable` when the error carries a `retryable` flag (0.13.5, [cyanheads/mcp-ts-core#458](https://github.com/cyanheads/mcp-ts-core/issues/458)).
|
|
19
|
+
- `.env.example`'s framework block synced with the template — comments reworded, newly documented variables (`MCP_HTTP_RESUMABILITY*`, `MCP_GC_PRESSURE_INTERVAL_MS`, `MCP_LOG_RATE_LIMIT_*`) added; `MCP_SESSION_MODE` stays `stateless`.
|
|
20
|
+
- `devcheck.config.json` gains a `lint` block; `scripts/lint-mcp.ts` reads `lint.truncationAllowlist` from it (a declared value wins over `MCP_LINT_TRUNCATION_ALLOWLIST`), and now guards its `main()` behind an entrypoint check so it's importable without executing.
|
|
21
|
+
- Added `.github/workflows/codeql.yml` — the one GitHub Actions workflow this project carries; CodeQL is GitHub-owned end to end and the file only runs while the repo's CodeQL default setup is off.
|
|
22
|
+
- Framework skills (`framework-skills/`), `changelog/template.md`, and the issue-report forms synced to the current framework template.
|
|
23
|
+
|
|
24
|
+
## Fixed
|
|
25
|
+
|
|
26
|
+
- **`secedgar_dataframe_query` table cells now escape backslashes before pipes** ([#114](https://github.com/cyanheads/secedgar-mcp-server/issues/114)) — a backslash already in a cell value was left unescaped, so a Markdown renderer read it as an escape and `content[]` rendered `x\|y` as `x|y` and `a\*b` as `a*b`; the header row gets the same escaping as data cells, which it previously skipped entirely.
|
|
27
|
+
- **A SELECT that prepares and then fails on the data it reads** (a bad cast, an out-of-range value) now returns `ValidationError` with reason `sql_execution_error` instead of an unreasoned `DatabaseError` (mcp-ts-core 0.13.4, [cyanheads/mcp-ts-core#451](https://github.com/cyanheads/mcp-ts-core/issues/451)). `multi_statement`, `denied_function`, and `plan_operator_not_allowed` are now declared on the tool's error contract alongside the framework's own recovery hints for each.
|
|
28
|
+
|
|
29
|
+
## Dependencies
|
|
30
|
+
|
|
31
|
+
- `@cyanheads/mcp-ts-core` ^0.13.2 → ^0.13.6
|
|
32
|
+
- `@duckdb/node-api` ^1.5.5-r.4 → ^1.5.5-r.5
|
|
33
|
+
- `zod` ^4.6.4 → ^4.6.5
|
|
34
|
+
- `@biomejs/biome` ^2.5.13 → ^2.5.14 (dev)
|
|
35
|
+
- `@types/node` ^26.5.1 → ^26.6.2 (dev)
|
|
36
|
+
- `vitest` ^5.0.0 → ^5.0.1 (dev)
|
package/changelog/template.md
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
# Required. One-line GitHub Release-style headline. 350 character cap — a
|
|
8
8
|
# ceiling, not a target. Default short and scannable. Don't pad, don't stitch
|
|
9
|
-
# unrelated changes with commas/semicolons into an inventory — pick the
|
|
10
|
-
# headline
|
|
9
|
+
# unrelated changes with commas/semicolons into an inventory — pick the one
|
|
10
|
+
# headline the release is about. Quotes required: unquoted YAML treats
|
|
11
11
|
# `: ` inside the value as a key separator and fails GitHub's strict parser.
|
|
12
12
|
summary: ""
|
|
13
13
|
|
|
@@ -119,11 +119,11 @@ security: false
|
|
|
119
119
|
TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
|
|
120
120
|
via `gh release create --notes-from-tag`. It is a condensed digest of this
|
|
121
121
|
entry, never a copy, and its format is owned by the `release-and-publish`
|
|
122
|
-
skill (step 4, "Create the annotated tag"):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
skill (step 4, "Create the annotated tag"): a short subject line without the
|
|
123
|
+
version, flat headline bullets — no Keep-a-Changelog section headers, no
|
|
124
|
+
gates line — at most one deps line, issue backlinks, and the changelog link
|
|
125
|
+
last. In release-PR mode the `git-wrapup` skill authors those bullets as the
|
|
126
|
+
PR body's `## Changes` and the tag copies them.
|
|
127
127
|
-->
|
|
128
128
|
|
|
129
129
|
## Added
|
|
@@ -6,6 +6,7 @@ import { z } from '@cyanheads/mcp-ts-core';
|
|
|
6
6
|
declare const ServerConfigSchema: z.ZodObject<{
|
|
7
7
|
userAgent: z.ZodString;
|
|
8
8
|
rateLimitRps: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
9
|
+
rateLimitCooldownSeconds: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
9
10
|
tickerCacheTtl: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
10
11
|
datasetTtlSeconds: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
11
12
|
dataframeDropEnabled: z.ZodDefault<z.ZodCodec<z.ZodString, z.ZodBoolean>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"server-config.d.ts","sourceRoot":"","sources":["../../src/config/server-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAG3C,QAAA,MAAM,kBAAkB;;;;;;;;;;;iBA+DtB,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D,wBAAgB,eAAe,IAAI,YAAY,CAc9C"}
|