@cyanheads/secedgar-mcp-server 0.15.8 → 0.15.9

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.
Files changed (56) hide show
  1. package/AGENTS.md +10 -7
  2. package/CLAUDE.md +10 -7
  3. package/README.md +11 -6
  4. package/changelog/0.15.x/0.15.9.md +32 -0
  5. package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js +2 -2
  6. package/dist/mcp-server/prompts/definitions/company-analysis.prompt.js.map +1 -1
  7. package/dist/mcp-server/resources/definitions/concepts.resource.d.ts +2 -1
  8. package/dist/mcp-server/resources/definitions/concepts.resource.d.ts.map +1 -1
  9. package/dist/mcp-server/resources/definitions/concepts.resource.js +5 -4
  10. package/dist/mcp-server/resources/definitions/concepts.resource.js.map +1 -1
  11. package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts +10 -0
  12. package/dist/mcp-server/tools/definitions/compare-companies.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/compare-companies.tool.js +143 -14
  14. package/dist/mcp-server/tools/definitions/compare-companies.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts +6 -1
  16. package/dist/mcp-server/tools/definitions/fetch-frames.tool.d.ts.map +1 -1
  17. package/dist/mcp-server/tools/definitions/fetch-frames.tool.js +93 -32
  18. package/dist/mcp-server/tools/definitions/fetch-frames.tool.js.map +1 -1
  19. package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts +6 -0
  20. package/dist/mcp-server/tools/definitions/get-financials.tool.d.ts.map +1 -1
  21. package/dist/mcp-server/tools/definitions/get-financials.tool.js +130 -60
  22. package/dist/mcp-server/tools/definitions/get-financials.tool.js.map +1 -1
  23. package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts +3 -0
  24. package/dist/mcp-server/tools/definitions/get-snapshot.tool.d.ts.map +1 -1
  25. package/dist/mcp-server/tools/definitions/get-snapshot.tool.js +16 -7
  26. package/dist/mcp-server/tools/definitions/get-snapshot.tool.js.map +1 -1
  27. package/dist/mcp-server/tools/definitions/index.d.ts +81 -57
  28. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/search-concepts.tool.d.ts +2 -1
  30. package/dist/mcp-server/tools/definitions/search-concepts.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/definitions/search-concepts.tool.js +5 -4
  32. package/dist/mcp-server/tools/definitions/search-concepts.tool.js.map +1 -1
  33. package/dist/services/edgar/concept-map.d.ts +38 -4
  34. package/dist/services/edgar/concept-map.d.ts.map +1 -1
  35. package/dist/services/edgar/concept-map.js +196 -9
  36. package/dist/services/edgar/concept-map.js.map +1 -1
  37. package/dist/services/edgar/concept-series.d.ts +79 -18
  38. package/dist/services/edgar/concept-series.d.ts.map +1 -1
  39. package/dist/services/edgar/concept-series.js +175 -25
  40. package/dist/services/edgar/concept-series.js.map +1 -1
  41. package/dist/services/edgar/edgar-api-service.d.ts +4 -6
  42. package/dist/services/edgar/edgar-api-service.d.ts.map +1 -1
  43. package/dist/services/edgar/edgar-api-service.js +34 -38
  44. package/dist/services/edgar/edgar-api-service.js.map +1 -1
  45. package/dist/services/edgar/mirror/edgar-mirror.d.ts +10 -1
  46. package/dist/services/edgar/mirror/edgar-mirror.d.ts.map +1 -1
  47. package/dist/services/edgar/mirror/edgar-mirror.js +52 -13
  48. package/dist/services/edgar/mirror/edgar-mirror.js.map +1 -1
  49. package/dist/services/edgar/trigram-similarity.d.ts +13 -0
  50. package/dist/services/edgar/trigram-similarity.d.ts.map +1 -0
  51. package/dist/services/edgar/trigram-similarity.js +40 -0
  52. package/dist/services/edgar/trigram-similarity.js.map +1 -0
  53. package/dist/services/edgar/types.d.ts +12 -2
  54. package/dist/services/edgar/types.d.ts.map +1 -1
  55. package/package.json +2 -2
  56. package/server.json +3 -3
package/AGENTS.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** secedgar-mcp-server
4
- **Version:** 0.15.8
4
+ **Version:** 0.15.9
5
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
 
@@ -55,7 +55,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
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?` |
58
- | `secedgar_fetch_frames` | Fetch SEC XBRL frames for one concept × one period across all reporting companies | `concept`, `period`, `unit?`, `limit?`, `offset?`, `sort?` |
58
+ | `secedgar_fetch_frames` | Fetch SEC XBRL frames for one concept × one period across all reporting companies | `concept`, `period`, `taxonomy?`, `unit?`, `limit?`, `offset?`, `sort?` |
59
59
  | `secedgar_compare_companies` | Period-aligned comparison of 2-10 named companies across 1-8 concepts | `companies`, `concepts`, `taxonomy?`, `period_type?`, `periods?` |
60
60
  | `secedgar_search_concepts` | Discover supported XBRL concept names or reverse-lookup a raw tag | `search?`, `group?`, `taxonomy?` |
61
61
  | `secedgar_dataframe_describe` | List canvas dataframes with provenance, TTL, and schema | `name?` |
@@ -87,9 +87,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
87
87
  - **Name and ticker resolution:** `resolveCik` walks numeric CIK → the `/^[A-Z]{1,5}$/` ticker fast path → the name passes (exact → prefix → substring over `allEntries`) → a catch-all `byTicker` lookup for symbols whose shape skipped the fast path. Two normalizations hang off that chain. The name passes' **exact** tier also compares suffix-normalized forms (`normalizeCompanySuffix`): the terminal whitespace-delimited token only, a trailing `.`/`,` stripped as part of recognizing it, expanded to one of four canonical long forms — `corp`→`corporation`, `inc`→`incorporated`, `co`→`company`, `ltd`→`limited`. **The four buckets are never merged.** The registry carries distinct registrants differing only in which suffix they use (`TORO CO` 0000737758 vs `TORO CORP.` 0001941131, and the same shape on Blue Owl and Fluent), so a single "has a corporate suffix" marker would compare two companies equal. Only the terminal token is rewritten — the same words sit mid-name in titles ending in a different suffix (`American Water Works Company, Inc.`) — and a terminal token that merely contains one (`Ltd./ADR`) is not a suffix. Mid-name punctuation is out of scope and still blocks a match (`Fluent Inc` does not reach `Fluent, Inc.`); so are `llc`/`plc` and the `lp`/limited-partnership family, which must never fold into the `ltd` bucket (#107). The **catch-all ticker lookup** retries a dotted symbol with `.`→`-` on a miss — brokers and market data write `BRK.B` where `company_tickers.json` lists `BRK-B`, and that file carries exactly one dotted ticker (a placeholder), so the substitution cannot land on the wrong registrant. The early gate stays untouched: it is a fast path, not the only ticker lookup, and `search_filings`' `ticker:` token inherits the fix through `resolveCik` with no change of its own (#110)
88
88
  - **Former-name resolution:** `src/services/edgar/data/former-names.json` is a committed asset (generated offline via `bun run gen:former-names`) containing `[lowercasedFormerName, zeroPaddedCIK]` tuples. `buildTickerCache` folds them into `allEntries` for name search and the trigram pass. Regenerate on demand after major M&A waves — former names are immutable, so the file rarely needs updating.
89
89
  - **Near-match suggestions:** `suggestCompanies(query, allEntries)` runs a Dice-coefficient trigram pass on the zero-hit search path, returning up to 3 candidates above a threshold. Each entry is scored on its **name and its ticker**, keeping the higher of the two — one ranked, deduped, top-N list rather than two disjoint ones, so a strong ticker match can outrank a weak name match and vice versa, and a ticker-shaped miss gets help instead of a bare `no_match` (#111). An entry missing either field is simply not scored on it: former-name entries carry a name and no ticker, and MF rows carry neither a name nor a place in `allEntries`, so fund tickers stay structurally unreachable here regardless of what the scorer does. Suggestions appear in the `no_match` error `data.suggestions` and in the error message. Never auto-resolves.
90
- - **XBRL friendly names:** `concept-map.ts` maps `"revenue"` → real XBRL tags; handles historical tag changes (ASC 606), including both assessed-tax variants — a filer presenting revenue gross of excise/sales tax reports only `RevenueFromContractWithCustomerIncludingAssessedTax`, and without it the walk fell through to tags SEC retired in 2018 (#98). The Including variant sits BEHIND `Revenues`, not ahead of it — the two disagree for filers that tag gross sales and net-of-excise sales under separate elements, so promoting it over `Revenues` swaps the definition partway back through the history and prints a step change that is a tag switch, not a business fact; behind `Revenues` it only fills frames no current tag covers. `ifrsTags` covers every concept with an IFRS element confirmed present in a live 20-F filer's companyfacts; `notes_payable` is deliberately unmapped (IFRS presents borrowings as one caption, with no notes/debt split) and `shares_outstanding` is `dei`. Never add an `ifrsTags` entry from the taxonomy alone — an element existing is not evidence filers tag it (#99). `ifrsTags` order is semantic the same way `tags` is: index 0 is the counterpart of the us-gaap index-0 tag, so the same concept answers the same question under either taxonomy (`accounts_receivable` leads with the trade-only caption, not the combined trade-and-other one; `share_repurchases` with the financing outflow, not the treasury-only line a share-cancelling filer tags zero). Verify a candidate order against filers that report BOTH tags — presence alone does not settle which one leads. One concept has no right order: IFRS filers split between the employee-scheme share-based-payment element and the IFRS 2.51(a) expense total, and the two invert between filers (SAP's total is its real line with the employee element a two-period fringe; Sanofi is the exact opposite), so `stock_based_compensation` carries `ifrsTagSelection: 'coverage'` and picks per filer by which tag covers more standard periods (#101). It is the only concept that does, and the flag is scoped to `ifrsTags` on purpose — its us-gaap pair IS a ladder, since `AllocatedShareBasedCompensationExpense` is the disaggregated line and out-covers the total for Alphabet, IBM, Tesla, and P&G.
90
+ - **XBRL friendly names:** `concept-map.ts` maps `"revenue"` → real XBRL tags; handles historical tag changes (ASC 606), including both assessed-tax variants — a filer presenting revenue gross of excise/sales tax reports only `RevenueFromContractWithCustomerIncludingAssessedTax`, and without it the walk fell through to tags SEC retired in 2018 (#98). The Including variant sits BEHIND `Revenues`, not ahead of it — the two disagree for filers that tag gross sales and net-of-excise sales under separate elements, so promoting it over `Revenues` swaps the definition partway back through the history and prints a step change that is a tag switch, not a business fact; behind `Revenues` it only fills frames no current tag covers. The same precedent places successor tags: `capex` walks `PaymentsToAcquireProductiveAssets` behind the PP&E-only tag (it also counts software and intangibles) and `interest_expense` walks `InterestExpenseNonoperating` last, so NVIDIA, Amazon, Visa, and Microsoft reach current values and no frame the older tags cover changes (#125). A different-definition tag still goes in `relatedTags`, never `tags` — `ppe_net`'s finance-lease-inclusive total is the primary line for Alphabet, Meta, and Tesla (#36, #130). `ifrsTags` covers every concept with an IFRS element confirmed present in a live 20-F filer's companyfacts; `notes_payable` is deliberately unmapped (IFRS presents borrowings as one caption, with no notes/debt split) and `shares_outstanding` is `dei`. Never add an `ifrsTags` entry from the taxonomy alone — an element existing is not evidence filers tag it (#99). `ifrsTags` order is semantic the same way `tags` is: index 0 is the counterpart of the us-gaap index-0 tag, so the same concept answers the same question under either taxonomy (`accounts_receivable` leads with the trade-only caption, not the combined trade-and-other one; `share_repurchases` with the financing outflow, not the treasury-only line a share-cancelling filer tags zero). Verify a candidate order against filers that report BOTH tags — presence alone does not settle which one leads. One concept has no right order: IFRS filers split between the employee-scheme share-based-payment element and the IFRS 2.51(a) expense total, and the two invert between filers (SAP's total is its real line with the employee element a two-period fringe; Sanofi is the exact opposite), so `stock_based_compensation` carries `ifrsTagSelection: 'coverage'` and picks per filer by which tag covers more standard periods (#101). It is the only concept that does, and the flag is scoped to `ifrsTags` on purpose — its us-gaap pair IS a ladder, since `AllocatedShareBasedCompensationExpense` is the disaggregated line and out-covers the total for Alphabet, IBM, Tesla, and P&G.
91
+ - **Unknown concept names:** `findUnknownConcept` in `concept-map.ts` runs in `get_financials`, `compare_companies`, and `fetch_frames` before company resolution or any SEC request. After trimming, a catalog name in any case or separator form passes, and so does anything matching `/^[A-Z][A-Za-z0-9]*$/` — every item element in us-gaap, ifrs-full, dei, and srt has that shape and SEC matches tags case-sensitively, so no input that could return data is rejected, and a well-formed tag that 404s stays `no_data` / `no_concept_data` (#45). Everything else fails as `unknown_concept` (`NotFound`) and never reaches a request URL, where the tag is interpolated as a path segment. The hint is a formula from the `DERIVATIONS` table (`free_cash_flow`, `ebitda`, `working_capital`; read before the tag-shape test, since no taxonomy element is named `EBITDA`) or up to three catalog names by Dice trigram score ≥ 0.3 against name and label, ties by name — the label is the only route from `capital_expenditures` to `capex`. `total_debt` deliberately has no formula: `debt` is long-term only. `compare_companies` fails only when every concept is unknown; otherwise it answers the rest and lists each unknown once in `unknown_concepts`, never as a per-company gap (#128). Known concepts are deduped the same way, by what they resolve to — the catalog entry, or the trimmed raw tag — so `revenue`, `Revenue`, and ` revenue` are compared once under the first spelling and a caveat names the merged inputs; a catalog name and a raw tag it walks (`revenue` and `Revenues`) are different concepts and stay separate (#145). `trigramSimilarity` lives in the `trigram-similarity.ts` leaf so the catalog scores names without importing the HTTP service.
91
92
  - **Series staleness:** two causes, one symptom, one caveat — `seriesStalenessCaveats(tag, label, newest, reference)` in `concept-series.ts` owns both, so a series is never flagged twice in different words. A friendly name's tag array is walked in order, so a tag SEC retired can win when every current tag reports nothing for the filer; SEC ships that tell in the taxonomy `label` (`"… (Deprecated 2018-01-31)"`), already in hand — no extra upstream call. Generalized rather than pinned to one pair: `revenue` and `cogs` both carry retired fallbacks (#98). A *current* tag whose series simply ends carries no tell at all, so the fallback signal is the gap itself: **730 days**, two full fiscal years behind the reference. The floor has to clear one year plus a filing window — an annual-only concept sits a fiscal year back until the next report lands, and a 20-F is due four months after year end — which puts a healthy filer around 500 days. Each tool passes the freshest reference it has without an extra call: `get_snapshot` and `compare_companies` pass `newestReportedPeriod(facts, taxonomy)`, the newest standard period the filer reports across the whole concept catalog — it isolates a line lagging its siblings and collapses to silence for a filer that stopped filing, whose every line is equally old, and it deliberately reads the whole catalog rather than the requested concepts, or a single-concept comparison would measure a series against itself. The reference skips the `dei` namespace — cover-page facts track the filing, not the statements, so they stay current for a registrant that stopped reporting under this taxonomy; counting them put Toyota's us-gaap profile (last reported fiscal 2020) six years behind a cover-page date and flagged all 28 of its lines at once. `get_financials` passes today, because one companyconcept payload holds no filer-wide period. All three measure the *full deduped set*, not the period-filtered slice — an annual view trailing a live quarterly series is a property of the filter. The retirement stamp wins when both apply; it names the concrete cause. `get_financials` therefore reads the clock, so its tests pin one (`vi.useFakeTimers({ toFake: ['Date'] })`) (#102).
92
- - **XBRL deduplication:** Filter to entries with `frame` field to get one value per standard calendar period. When a friendly name maps to multiple tags, same-frame collisions resolve by **tag priority** — the lower-index tag wins (index 0 = preferred total, e.g. IFRS `Revenue` over the `RevenueFromContractsWithCustomers` sub-line), ties within one tag by latest `filed` (restatement). Tag-array order in `concept-map.ts` is therefore semantic (#44). The rules live once in `concept-series.ts` (`resolveFrameSeries` over `CompanyConceptUnit`s carrying a `tagIndex`; `seriesFromCompanyFacts` for the whole-company reads) — every tool reading XBRL facts goes through it so their numbers agree. `resolveConceptTarget` in `concept-map.ts` is the matching taxonomy+tag resolution (mapping taxonomy wins over the `us-gaap` default; `ifrs-full` uses `ifrsTags` when present) and carries the `tagSelection` for the array it picked. **`priority` is the default and coverage ranking is never the general rule** — it answers "which element does this filer maintain", which is the question only when the elements mean the same thing. Where they mean different things the array order is the answer and period count is noise: Molson Coors keeps eleven calendar years under a 2018-retired revenue element and ten under the assessed-tax-inclusive one against five under `Revenues`, so ranking `revenue` by coverage hands it either the dead tag or its gross-of-excise line in place of the continuous net series; Smucker and Spotify survive it only on a coincidental tie in period count (#101). `preferredTagIndex` picks the winning tag under either rule, and the resolved `tag`/`label`/`unit` follow the winner rather than the first tag to answer. The two rules also differ in what the losers contribute: under `priority` the array is a ladder, so a lower tag fills frames the leader does not report, while under `coverage` the tags are alternates and only the winner contributes — gap-filling from an alternate splices two definitions into one series, and the filers reporting both tags disagree where they overlap (Ferrari tags CY2022 at EUR 16.2M under the employee element and EUR 20.9M under the IFRS 2.51(a) total), so the filled series prints a step that is a tag switch rather than a business fact. Seven percent of the 224 IFRS filers reporting either `stock_based_compensation` tag have that shape; a winner-only series that then ends years back is what the staleness caveat is for.
93
+ - **XBRL deduplication:** Filter to entries with `frame` field to get one value per standard calendar period. SEC frames the latest-filed fact for a period, and since the pay-versus-performance rule a DEF 14A re-tags five years of annual `NetIncomeLoss`, so the proxy's figure — often rounded, sometimes mis-scaled or sign-flipped (TheRealReal's CY2024 at +134.2B against the 10-K's −134.2M) — holds the frame. A frame held by any Schedule 14A/14C form (`isProxyForm`) is answered with the latest-filed fact from another form with the same tag, unit key, `start`, and `end` (`reportingFormTwin`), keeping the frame; with no such fact the proxy value stands. Only proxy holders are replaced — a periodic-form allow-list would revert Bank of America's CY2013, framed on a 2016 8-K recast (10,539M) that no later 10-K repeats. The swap runs per tag and per unit key BEFORE cross-tag priority, so tag 0's proxy-held frame takes its twin from tag 0 and is never handed to a lower tag, and it costs two regex tests per framed value when no holder needs correcting. SEC also frames any year-long duration as `CY####`, including the trailing-twelve-month figure a 10-Q discloses, so a later 10-Q can hold an annual frame for a year the filer has not closed (Amazon's CY2026 `NetIncomeLoss` is 135.3B for July 2025–June 2026). An annual frame held by a 10-Q/10-QT takes the latest same-period fact from a form that is neither a quarterly report nor a proxy (the 10-K, in practice), else stays only when it covers a closed fiscal year a later 10-Q happens to disclose — ended before filing, and ending within a week of a year-long fact's end elsewhere in the series (or the series shows none to test against) — else drops out of the series (#142). Leaving every twinless 10-Q holder out would delete real years: across seven filers' companyfacts, 9 of 74 such holders are closed fiscal years reported only in a later 10-Q (Merck's `AccountsReceivableSale` CY2020). `frameHolderFact` owns both holder rules. The mirror's `getFrames` applies them and marks its response `holderFormsResolved`; the live frames API carries no form, so `fetch_frames` caveats annual `NetIncomeLoss` frames (#123) and annual frames for a calendar year still inside its 10-K window, through April of the next year (#142). When a friendly name maps to multiple tags, same-frame collisions resolve by **tag priority** — the lower-index tag wins (index 0 = preferred total, e.g. IFRS `Revenue` over the `RevenueFromContractsWithCustomers` sub-line), ties within one tag by latest `filed` (restatement). Tag-array order in `concept-map.ts` is therefore semantic (#44). The rules live once in `concept-series.ts` (`resolveFrameSeries` over `CompanyConceptUnit`s carrying a `tagIndex`, their source `tag`, and their unit key; `seriesFromCompanyFacts` for the whole-company reads) — every tool reading XBRL facts goes through it so their numbers agree. `resolveConceptTarget` in `concept-map.ts` is the matching taxonomy+tag resolution (mapping taxonomy wins over the `us-gaap` default; `ifrs-full` uses `ifrsTags` when present) and carries the `tagSelection` for the array it picked. `fetch_frames` resolves through it too, so its `taxonomy` input — `us-gaap` or `dei`, the only namespaces SEC publishes frames for — picks the namespace a raw tag is read from, a catalog name keeps its own mapped taxonomy under the default, and an explicit `dei` reads a catalog name's tags from `dei` exactly as `get_financials` does (#143). **`priority` is the default and coverage ranking is never the general rule** — it answers "which element does this filer maintain", which is the question only when the elements mean the same thing. Where they mean different things the array order is the answer and period count is noise: Molson Coors keeps eleven calendar years under a 2018-retired revenue element and ten under the assessed-tax-inclusive one against five under `Revenues`, so ranking `revenue` by coverage hands it either the dead tag or its gross-of-excise line in place of the continuous net series; Smucker and Spotify survive it only on a coincidental tie in period count (#101). `preferredTagIndex` picks the winning tag under either rule. Every resolved value names its own source tag — `get_financials` rows, `get_snapshot` points, and `compare_companies` cells each carry one — and the line-level `tag`/`label`/`unit` follow the tag behind the newest value (`describingTag`): under `coverage` that is the winner, and under `priority` a successor appended behind an older leader answers the recent frames, so describing the line by the leader would name a tag that produced none of its current values (#125). A companyconcept or companyfacts unit served as anything but an array is dropped at the service edge (SEC serves Visa's and Coca-Cola's `NetIncomeLoss` companyconcept as `"units":{"USD":{}}`); a companyconcept payload left with no units reads as served-empty, and `get_financials` answers every such tag from one companyfacts read, which it reuses for the no-data probe (#141). The two rules also differ in what the losers contribute: under `priority` the array is a ladder, so a lower tag fills frames the leader does not report, while under `coverage` the tags are alternates and only the winner contributes — gap-filling from an alternate splices two definitions into one series, and the filers reporting both tags disagree where they overlap (Ferrari tags CY2022 at EUR 16.2M under the employee element and EUR 20.9M under the IFRS 2.51(a) total), so the filled series prints a step that is a tag switch rather than a business fact. Seven percent of the 224 IFRS filers reporting either `stock_based_compensation` tag have that shape; a winner-only series that then ends years back is what the staleness caveat is for.
93
94
  - **Fiscal-period caveats:** SEC reports a filer's fiscal Q4 as the 10-K residual, never as a discrete quarterly fact, so the calendar quarter that a filer's fiscal Q4 *spans* carries no frame-tagged quarterly value. That is not always the quarter holding the fiscal year-end date — a January year-end closes a fiscal Q4 running Nov–Jan, which SEC frames as calendar Q4 — and calendar-year filers are affected too (no discrete Q4). `fiscal-periods.ts` owns both directions: `fiscalQ4Caveats(period)` for the cross-company frame view (`fetch_frames`), `missingQuarterCaveats(frames)` for the per-filer series view (`get_financials`, `get_snapshot`, `compare_companies`) (#95). The per-filer detection is data-driven — no maintained filer list — and reads only the newest 4 years carrying quarterly frames, applying the per-year quorum inside that window rather than before it, because SEC frame-tagged all four calendar quarters before ~CY2021 and drops the fiscal-Q4 quarter after; a whole-history read would let the older years mask the live gap, and filtering first would let a filer whose recent years are too sparse silently describe the older regime. **Keep that slice-then-filter order.** The detector reports a *set* of absent quarters, not one: a filer whose remaining fiscal quarters also span non-calendar durations loses two (Costco tags only calendar Q1 and Q4), and the per-year quorum is two of four — low enough to admit that shape, high enough to bound the absent set at two and keep a one-quarter stub year from dragging a third in (#100). Message wording and the derivation advice differ between the one- and two-quarter cases; the three consuming tools all iterate `caveats`, so nothing downstream assumes at most one entry.
94
95
  - **`get_financials` period default:** with `period_type` unset the series defaults to `annual` (clean FY series); if the annual filter empties a non-empty series whose frames are all instant (`CY####Q#I` — balance-sheet, shares-outstanding, raw instant tags), it falls back to the full series so the first call returns data (#48). An explicit `period_type` is honored as-is and still errors when it excludes everything.
95
96
  - **8-K item codes:** SEC replaced the original single-integer item numbering with the current `x.xx` scheme in Release 33-8400, effective **2004-08-23**, so EDGAR's `items` field carries two disjoint vocabularies. `eight-k-items.ts` decodes by the code's **shape**, never the filing date — dotted is current, a bare integer is legacy — because filers reported pre-changeover events under the old numbers for months after the effective date, and a date-keyed table would mislabel exactly those. The two vocabularies reuse integers for unrelated events (legacy `5` is Other Events, current `5.02` is officer departures; legacy `12` is the ancestor of `2.02`), so a dotted `items` filter never matches a pre-2004 filing — `get_material_events`' zero-hit notice says so and echoes the codes actually present. The tables back three surfaces: the tool's `items` enum, the decoded per-filing rows, and the `secedgar://filing-types` resource. A code matching a shape but absent from that regime's table comes back with no label rather than a guess.
@@ -100,10 +101,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
100
101
  - **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.
101
102
  - **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.
102
103
  - **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.
103
- - **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); with a `filed_after` / `filed_before` window it reads the in-window Form 4 / 4-A rows with an XML primary document — `recent`, then the archive pages overlapping the window, newest first, stopping once the scan cap plus a sentinel row are in hand — and with a canvas parses every one up to 100 instead of the 40-filing floor, since the window already bounds the set; `dataset.truncated` is then the sentinel or unread overlapping pages, and `history_scanned_through` the oldest filing parsed (#127); 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`.
104
+ - **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); with a `filed_after` / `filed_before` window it reads the in-window Form 4 / 4-A rows with an XML primary document — `recent`, then the archive pages overlapping the window, newest first, stopping once the scan cap plus a sentinel row are in hand — and with a canvas parses every one up to 100 instead of the 40-filing floor, since the window already bounds the set; `dataset.truncated` is then the sentinel or unread overlapping pages, and `history_scanned_through` the oldest filing parsed (#127); 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); the inline window is the newest periods of the union across companies, so a company-concept pair whose values all predate it is named in `caveats` — one line per concept, each company with its newest period and the dataframe pointer — and never in `gaps`, which keeps meaning no value in any period (#144). 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`.
104
105
  - **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.
105
106
  - **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.
106
- - **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 (superseding the mirror's bare fund rows with series/class, and covering a mirror synced before MF ingestion), so funds resolve there too (#43, #135).
107
+ - **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. The ingester stores a tag's own name when SEC serves it with no label (`InterestExpenseNonoperating`), so every read path treats a stored label equal to a camel-case compound tag as no label (`storedLabel`) — callers fall back to the concept's label, and `getFrames` answers an empty `label` as the live frames API does; a one-word element's label is the word itself (`Revenues`) and is kept. Read-side, so stored rows need no migration. 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 (superseding the mirror's bare fund rows with series/class, and covering a mirror synced before MF ingestion), so funds resolve there too (#43, #135).
107
108
 
108
109
  ---
109
110
 
@@ -132,7 +133,8 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
132
133
  | `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` | Paced HTTP client with the rate-limit block gate, CIK resolution, all SEC API calls |
133
134
  | `submissions-archive` | `src/services/edgar/submissions-archive.ts` | `SubmissionsArchiveWalk` — the one walk over `filings.files[]` archive pages: shared page cap, caller-chosen order, early stop, scan depth and truncation |
134
135
  | `filing-headers` | `src/services/edgar/filing-headers.ts` | SEC submission-header parsers: per-document types and the submission's form / filing date / period |
135
- | `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list |
136
+ | `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list; `findUnknownConcept` rejects names that are neither |
137
+ | `trigram-similarity` | `src/services/edgar/trigram-similarity.ts` | Dice trigram scorer behind company and concept near-match suggestions |
136
138
  | `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 |
137
139
  | `fiscal-periods` | `src/services/edgar/fiscal-periods.ts` | Fiscal-Q4 frame caveat and per-filer missing-quarter detection |
138
140
  | `eight-k-items` | `src/services/edgar/eight-k-items.ts` | 8-K item-code decode tables for both numbering regimes; shape-driven `decodeEightKItem` |
@@ -214,6 +216,7 @@ src/
214
216
  filing-headers.ts # SEC submission-header parsers
215
217
  concept-map.ts # Friendly name → XBRL tag mapping + taxonomy/tag resolution
216
218
  concept-series.ts # Shared frame dedup, tag selection, staleness caveats
219
+ trigram-similarity.ts # Dice trigram scorer for near-match suggestions
217
220
  fiscal-periods.ts # Fiscal Q4 / missing-quarter caveats
218
221
  eight-k-items.ts # 8-K item-code decode tables (current + legacy regimes)
219
222
  xml-nodes.ts # Shared XML tag lookup + value coercion helpers
package/CLAUDE.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Agent Protocol
2
2
 
3
3
  **Server:** secedgar-mcp-server
4
- **Version:** 0.15.8
4
+ **Version:** 0.15.9
5
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
 
@@ -55,7 +55,7 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
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?` |
58
- | `secedgar_fetch_frames` | Fetch SEC XBRL frames for one concept × one period across all reporting companies | `concept`, `period`, `unit?`, `limit?`, `offset?`, `sort?` |
58
+ | `secedgar_fetch_frames` | Fetch SEC XBRL frames for one concept × one period across all reporting companies | `concept`, `period`, `taxonomy?`, `unit?`, `limit?`, `offset?`, `sort?` |
59
59
  | `secedgar_compare_companies` | Period-aligned comparison of 2-10 named companies across 1-8 concepts | `companies`, `concepts`, `taxonomy?`, `period_type?`, `periods?` |
60
60
  | `secedgar_search_concepts` | Discover supported XBRL concept names or reverse-lookup a raw tag | `search?`, `group?`, `taxonomy?` |
61
61
  | `secedgar_dataframe_describe` | List canvas dataframes with provenance, TTL, and schema | `name?` |
@@ -87,9 +87,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
87
87
  - **Name and ticker resolution:** `resolveCik` walks numeric CIK → the `/^[A-Z]{1,5}$/` ticker fast path → the name passes (exact → prefix → substring over `allEntries`) → a catch-all `byTicker` lookup for symbols whose shape skipped the fast path. Two normalizations hang off that chain. The name passes' **exact** tier also compares suffix-normalized forms (`normalizeCompanySuffix`): the terminal whitespace-delimited token only, a trailing `.`/`,` stripped as part of recognizing it, expanded to one of four canonical long forms — `corp`→`corporation`, `inc`→`incorporated`, `co`→`company`, `ltd`→`limited`. **The four buckets are never merged.** The registry carries distinct registrants differing only in which suffix they use (`TORO CO` 0000737758 vs `TORO CORP.` 0001941131, and the same shape on Blue Owl and Fluent), so a single "has a corporate suffix" marker would compare two companies equal. Only the terminal token is rewritten — the same words sit mid-name in titles ending in a different suffix (`American Water Works Company, Inc.`) — and a terminal token that merely contains one (`Ltd./ADR`) is not a suffix. Mid-name punctuation is out of scope and still blocks a match (`Fluent Inc` does not reach `Fluent, Inc.`); so are `llc`/`plc` and the `lp`/limited-partnership family, which must never fold into the `ltd` bucket (#107). The **catch-all ticker lookup** retries a dotted symbol with `.`→`-` on a miss — brokers and market data write `BRK.B` where `company_tickers.json` lists `BRK-B`, and that file carries exactly one dotted ticker (a placeholder), so the substitution cannot land on the wrong registrant. The early gate stays untouched: it is a fast path, not the only ticker lookup, and `search_filings`' `ticker:` token inherits the fix through `resolveCik` with no change of its own (#110)
88
88
  - **Former-name resolution:** `src/services/edgar/data/former-names.json` is a committed asset (generated offline via `bun run gen:former-names`) containing `[lowercasedFormerName, zeroPaddedCIK]` tuples. `buildTickerCache` folds them into `allEntries` for name search and the trigram pass. Regenerate on demand after major M&A waves — former names are immutable, so the file rarely needs updating.
89
89
  - **Near-match suggestions:** `suggestCompanies(query, allEntries)` runs a Dice-coefficient trigram pass on the zero-hit search path, returning up to 3 candidates above a threshold. Each entry is scored on its **name and its ticker**, keeping the higher of the two — one ranked, deduped, top-N list rather than two disjoint ones, so a strong ticker match can outrank a weak name match and vice versa, and a ticker-shaped miss gets help instead of a bare `no_match` (#111). An entry missing either field is simply not scored on it: former-name entries carry a name and no ticker, and MF rows carry neither a name nor a place in `allEntries`, so fund tickers stay structurally unreachable here regardless of what the scorer does. Suggestions appear in the `no_match` error `data.suggestions` and in the error message. Never auto-resolves.
90
- - **XBRL friendly names:** `concept-map.ts` maps `"revenue"` → real XBRL tags; handles historical tag changes (ASC 606), including both assessed-tax variants — a filer presenting revenue gross of excise/sales tax reports only `RevenueFromContractWithCustomerIncludingAssessedTax`, and without it the walk fell through to tags SEC retired in 2018 (#98). The Including variant sits BEHIND `Revenues`, not ahead of it — the two disagree for filers that tag gross sales and net-of-excise sales under separate elements, so promoting it over `Revenues` swaps the definition partway back through the history and prints a step change that is a tag switch, not a business fact; behind `Revenues` it only fills frames no current tag covers. `ifrsTags` covers every concept with an IFRS element confirmed present in a live 20-F filer's companyfacts; `notes_payable` is deliberately unmapped (IFRS presents borrowings as one caption, with no notes/debt split) and `shares_outstanding` is `dei`. Never add an `ifrsTags` entry from the taxonomy alone — an element existing is not evidence filers tag it (#99). `ifrsTags` order is semantic the same way `tags` is: index 0 is the counterpart of the us-gaap index-0 tag, so the same concept answers the same question under either taxonomy (`accounts_receivable` leads with the trade-only caption, not the combined trade-and-other one; `share_repurchases` with the financing outflow, not the treasury-only line a share-cancelling filer tags zero). Verify a candidate order against filers that report BOTH tags — presence alone does not settle which one leads. One concept has no right order: IFRS filers split between the employee-scheme share-based-payment element and the IFRS 2.51(a) expense total, and the two invert between filers (SAP's total is its real line with the employee element a two-period fringe; Sanofi is the exact opposite), so `stock_based_compensation` carries `ifrsTagSelection: 'coverage'` and picks per filer by which tag covers more standard periods (#101). It is the only concept that does, and the flag is scoped to `ifrsTags` on purpose — its us-gaap pair IS a ladder, since `AllocatedShareBasedCompensationExpense` is the disaggregated line and out-covers the total for Alphabet, IBM, Tesla, and P&G.
90
+ - **XBRL friendly names:** `concept-map.ts` maps `"revenue"` → real XBRL tags; handles historical tag changes (ASC 606), including both assessed-tax variants — a filer presenting revenue gross of excise/sales tax reports only `RevenueFromContractWithCustomerIncludingAssessedTax`, and without it the walk fell through to tags SEC retired in 2018 (#98). The Including variant sits BEHIND `Revenues`, not ahead of it — the two disagree for filers that tag gross sales and net-of-excise sales under separate elements, so promoting it over `Revenues` swaps the definition partway back through the history and prints a step change that is a tag switch, not a business fact; behind `Revenues` it only fills frames no current tag covers. The same precedent places successor tags: `capex` walks `PaymentsToAcquireProductiveAssets` behind the PP&E-only tag (it also counts software and intangibles) and `interest_expense` walks `InterestExpenseNonoperating` last, so NVIDIA, Amazon, Visa, and Microsoft reach current values and no frame the older tags cover changes (#125). A different-definition tag still goes in `relatedTags`, never `tags` — `ppe_net`'s finance-lease-inclusive total is the primary line for Alphabet, Meta, and Tesla (#36, #130). `ifrsTags` covers every concept with an IFRS element confirmed present in a live 20-F filer's companyfacts; `notes_payable` is deliberately unmapped (IFRS presents borrowings as one caption, with no notes/debt split) and `shares_outstanding` is `dei`. Never add an `ifrsTags` entry from the taxonomy alone — an element existing is not evidence filers tag it (#99). `ifrsTags` order is semantic the same way `tags` is: index 0 is the counterpart of the us-gaap index-0 tag, so the same concept answers the same question under either taxonomy (`accounts_receivable` leads with the trade-only caption, not the combined trade-and-other one; `share_repurchases` with the financing outflow, not the treasury-only line a share-cancelling filer tags zero). Verify a candidate order against filers that report BOTH tags — presence alone does not settle which one leads. One concept has no right order: IFRS filers split between the employee-scheme share-based-payment element and the IFRS 2.51(a) expense total, and the two invert between filers (SAP's total is its real line with the employee element a two-period fringe; Sanofi is the exact opposite), so `stock_based_compensation` carries `ifrsTagSelection: 'coverage'` and picks per filer by which tag covers more standard periods (#101). It is the only concept that does, and the flag is scoped to `ifrsTags` on purpose — its us-gaap pair IS a ladder, since `AllocatedShareBasedCompensationExpense` is the disaggregated line and out-covers the total for Alphabet, IBM, Tesla, and P&G.
91
+ - **Unknown concept names:** `findUnknownConcept` in `concept-map.ts` runs in `get_financials`, `compare_companies`, and `fetch_frames` before company resolution or any SEC request. After trimming, a catalog name in any case or separator form passes, and so does anything matching `/^[A-Z][A-Za-z0-9]*$/` — every item element in us-gaap, ifrs-full, dei, and srt has that shape and SEC matches tags case-sensitively, so no input that could return data is rejected, and a well-formed tag that 404s stays `no_data` / `no_concept_data` (#45). Everything else fails as `unknown_concept` (`NotFound`) and never reaches a request URL, where the tag is interpolated as a path segment. The hint is a formula from the `DERIVATIONS` table (`free_cash_flow`, `ebitda`, `working_capital`; read before the tag-shape test, since no taxonomy element is named `EBITDA`) or up to three catalog names by Dice trigram score ≥ 0.3 against name and label, ties by name — the label is the only route from `capital_expenditures` to `capex`. `total_debt` deliberately has no formula: `debt` is long-term only. `compare_companies` fails only when every concept is unknown; otherwise it answers the rest and lists each unknown once in `unknown_concepts`, never as a per-company gap (#128). Known concepts are deduped the same way, by what they resolve to — the catalog entry, or the trimmed raw tag — so `revenue`, `Revenue`, and ` revenue` are compared once under the first spelling and a caveat names the merged inputs; a catalog name and a raw tag it walks (`revenue` and `Revenues`) are different concepts and stay separate (#145). `trigramSimilarity` lives in the `trigram-similarity.ts` leaf so the catalog scores names without importing the HTTP service.
91
92
  - **Series staleness:** two causes, one symptom, one caveat — `seriesStalenessCaveats(tag, label, newest, reference)` in `concept-series.ts` owns both, so a series is never flagged twice in different words. A friendly name's tag array is walked in order, so a tag SEC retired can win when every current tag reports nothing for the filer; SEC ships that tell in the taxonomy `label` (`"… (Deprecated 2018-01-31)"`), already in hand — no extra upstream call. Generalized rather than pinned to one pair: `revenue` and `cogs` both carry retired fallbacks (#98). A *current* tag whose series simply ends carries no tell at all, so the fallback signal is the gap itself: **730 days**, two full fiscal years behind the reference. The floor has to clear one year plus a filing window — an annual-only concept sits a fiscal year back until the next report lands, and a 20-F is due four months after year end — which puts a healthy filer around 500 days. Each tool passes the freshest reference it has without an extra call: `get_snapshot` and `compare_companies` pass `newestReportedPeriod(facts, taxonomy)`, the newest standard period the filer reports across the whole concept catalog — it isolates a line lagging its siblings and collapses to silence for a filer that stopped filing, whose every line is equally old, and it deliberately reads the whole catalog rather than the requested concepts, or a single-concept comparison would measure a series against itself. The reference skips the `dei` namespace — cover-page facts track the filing, not the statements, so they stay current for a registrant that stopped reporting under this taxonomy; counting them put Toyota's us-gaap profile (last reported fiscal 2020) six years behind a cover-page date and flagged all 28 of its lines at once. `get_financials` passes today, because one companyconcept payload holds no filer-wide period. All three measure the *full deduped set*, not the period-filtered slice — an annual view trailing a live quarterly series is a property of the filter. The retirement stamp wins when both apply; it names the concrete cause. `get_financials` therefore reads the clock, so its tests pin one (`vi.useFakeTimers({ toFake: ['Date'] })`) (#102).
92
- - **XBRL deduplication:** Filter to entries with `frame` field to get one value per standard calendar period. When a friendly name maps to multiple tags, same-frame collisions resolve by **tag priority** — the lower-index tag wins (index 0 = preferred total, e.g. IFRS `Revenue` over the `RevenueFromContractsWithCustomers` sub-line), ties within one tag by latest `filed` (restatement). Tag-array order in `concept-map.ts` is therefore semantic (#44). The rules live once in `concept-series.ts` (`resolveFrameSeries` over `CompanyConceptUnit`s carrying a `tagIndex`; `seriesFromCompanyFacts` for the whole-company reads) — every tool reading XBRL facts goes through it so their numbers agree. `resolveConceptTarget` in `concept-map.ts` is the matching taxonomy+tag resolution (mapping taxonomy wins over the `us-gaap` default; `ifrs-full` uses `ifrsTags` when present) and carries the `tagSelection` for the array it picked. **`priority` is the default and coverage ranking is never the general rule** — it answers "which element does this filer maintain", which is the question only when the elements mean the same thing. Where they mean different things the array order is the answer and period count is noise: Molson Coors keeps eleven calendar years under a 2018-retired revenue element and ten under the assessed-tax-inclusive one against five under `Revenues`, so ranking `revenue` by coverage hands it either the dead tag or its gross-of-excise line in place of the continuous net series; Smucker and Spotify survive it only on a coincidental tie in period count (#101). `preferredTagIndex` picks the winning tag under either rule, and the resolved `tag`/`label`/`unit` follow the winner rather than the first tag to answer. The two rules also differ in what the losers contribute: under `priority` the array is a ladder, so a lower tag fills frames the leader does not report, while under `coverage` the tags are alternates and only the winner contributes — gap-filling from an alternate splices two definitions into one series, and the filers reporting both tags disagree where they overlap (Ferrari tags CY2022 at EUR 16.2M under the employee element and EUR 20.9M under the IFRS 2.51(a) total), so the filled series prints a step that is a tag switch rather than a business fact. Seven percent of the 224 IFRS filers reporting either `stock_based_compensation` tag have that shape; a winner-only series that then ends years back is what the staleness caveat is for.
93
+ - **XBRL deduplication:** Filter to entries with `frame` field to get one value per standard calendar period. SEC frames the latest-filed fact for a period, and since the pay-versus-performance rule a DEF 14A re-tags five years of annual `NetIncomeLoss`, so the proxy's figure — often rounded, sometimes mis-scaled or sign-flipped (TheRealReal's CY2024 at +134.2B against the 10-K's −134.2M) — holds the frame. A frame held by any Schedule 14A/14C form (`isProxyForm`) is answered with the latest-filed fact from another form with the same tag, unit key, `start`, and `end` (`reportingFormTwin`), keeping the frame; with no such fact the proxy value stands. Only proxy holders are replaced — a periodic-form allow-list would revert Bank of America's CY2013, framed on a 2016 8-K recast (10,539M) that no later 10-K repeats. The swap runs per tag and per unit key BEFORE cross-tag priority, so tag 0's proxy-held frame takes its twin from tag 0 and is never handed to a lower tag, and it costs two regex tests per framed value when no holder needs correcting. SEC also frames any year-long duration as `CY####`, including the trailing-twelve-month figure a 10-Q discloses, so a later 10-Q can hold an annual frame for a year the filer has not closed (Amazon's CY2026 `NetIncomeLoss` is 135.3B for July 2025–June 2026). An annual frame held by a 10-Q/10-QT takes the latest same-period fact from a form that is neither a quarterly report nor a proxy (the 10-K, in practice), else stays only when it covers a closed fiscal year a later 10-Q happens to disclose — ended before filing, and ending within a week of a year-long fact's end elsewhere in the series (or the series shows none to test against) — else drops out of the series (#142). Leaving every twinless 10-Q holder out would delete real years: across seven filers' companyfacts, 9 of 74 such holders are closed fiscal years reported only in a later 10-Q (Merck's `AccountsReceivableSale` CY2020). `frameHolderFact` owns both holder rules. The mirror's `getFrames` applies them and marks its response `holderFormsResolved`; the live frames API carries no form, so `fetch_frames` caveats annual `NetIncomeLoss` frames (#123) and annual frames for a calendar year still inside its 10-K window, through April of the next year (#142). When a friendly name maps to multiple tags, same-frame collisions resolve by **tag priority** — the lower-index tag wins (index 0 = preferred total, e.g. IFRS `Revenue` over the `RevenueFromContractsWithCustomers` sub-line), ties within one tag by latest `filed` (restatement). Tag-array order in `concept-map.ts` is therefore semantic (#44). The rules live once in `concept-series.ts` (`resolveFrameSeries` over `CompanyConceptUnit`s carrying a `tagIndex`, their source `tag`, and their unit key; `seriesFromCompanyFacts` for the whole-company reads) — every tool reading XBRL facts goes through it so their numbers agree. `resolveConceptTarget` in `concept-map.ts` is the matching taxonomy+tag resolution (mapping taxonomy wins over the `us-gaap` default; `ifrs-full` uses `ifrsTags` when present) and carries the `tagSelection` for the array it picked. `fetch_frames` resolves through it too, so its `taxonomy` input — `us-gaap` or `dei`, the only namespaces SEC publishes frames for — picks the namespace a raw tag is read from, a catalog name keeps its own mapped taxonomy under the default, and an explicit `dei` reads a catalog name's tags from `dei` exactly as `get_financials` does (#143). **`priority` is the default and coverage ranking is never the general rule** — it answers "which element does this filer maintain", which is the question only when the elements mean the same thing. Where they mean different things the array order is the answer and period count is noise: Molson Coors keeps eleven calendar years under a 2018-retired revenue element and ten under the assessed-tax-inclusive one against five under `Revenues`, so ranking `revenue` by coverage hands it either the dead tag or its gross-of-excise line in place of the continuous net series; Smucker and Spotify survive it only on a coincidental tie in period count (#101). `preferredTagIndex` picks the winning tag under either rule. Every resolved value names its own source tag — `get_financials` rows, `get_snapshot` points, and `compare_companies` cells each carry one — and the line-level `tag`/`label`/`unit` follow the tag behind the newest value (`describingTag`): under `coverage` that is the winner, and under `priority` a successor appended behind an older leader answers the recent frames, so describing the line by the leader would name a tag that produced none of its current values (#125). A companyconcept or companyfacts unit served as anything but an array is dropped at the service edge (SEC serves Visa's and Coca-Cola's `NetIncomeLoss` companyconcept as `"units":{"USD":{}}`); a companyconcept payload left with no units reads as served-empty, and `get_financials` answers every such tag from one companyfacts read, which it reuses for the no-data probe (#141). The two rules also differ in what the losers contribute: under `priority` the array is a ladder, so a lower tag fills frames the leader does not report, while under `coverage` the tags are alternates and only the winner contributes — gap-filling from an alternate splices two definitions into one series, and the filers reporting both tags disagree where they overlap (Ferrari tags CY2022 at EUR 16.2M under the employee element and EUR 20.9M under the IFRS 2.51(a) total), so the filled series prints a step that is a tag switch rather than a business fact. Seven percent of the 224 IFRS filers reporting either `stock_based_compensation` tag have that shape; a winner-only series that then ends years back is what the staleness caveat is for.
93
94
  - **Fiscal-period caveats:** SEC reports a filer's fiscal Q4 as the 10-K residual, never as a discrete quarterly fact, so the calendar quarter that a filer's fiscal Q4 *spans* carries no frame-tagged quarterly value. That is not always the quarter holding the fiscal year-end date — a January year-end closes a fiscal Q4 running Nov–Jan, which SEC frames as calendar Q4 — and calendar-year filers are affected too (no discrete Q4). `fiscal-periods.ts` owns both directions: `fiscalQ4Caveats(period)` for the cross-company frame view (`fetch_frames`), `missingQuarterCaveats(frames)` for the per-filer series view (`get_financials`, `get_snapshot`, `compare_companies`) (#95). The per-filer detection is data-driven — no maintained filer list — and reads only the newest 4 years carrying quarterly frames, applying the per-year quorum inside that window rather than before it, because SEC frame-tagged all four calendar quarters before ~CY2021 and drops the fiscal-Q4 quarter after; a whole-history read would let the older years mask the live gap, and filtering first would let a filer whose recent years are too sparse silently describe the older regime. **Keep that slice-then-filter order.** The detector reports a *set* of absent quarters, not one: a filer whose remaining fiscal quarters also span non-calendar durations loses two (Costco tags only calendar Q1 and Q4), and the per-year quorum is two of four — low enough to admit that shape, high enough to bound the absent set at two and keep a one-quarter stub year from dragging a third in (#100). Message wording and the derivation advice differ between the one- and two-quarter cases; the three consuming tools all iterate `caveats`, so nothing downstream assumes at most one entry.
94
95
  - **`get_financials` period default:** with `period_type` unset the series defaults to `annual` (clean FY series); if the annual filter empties a non-empty series whose frames are all instant (`CY####Q#I` — balance-sheet, shares-outstanding, raw instant tags), it falls back to the full series so the first call returns data (#48). An explicit `period_type` is honored as-is and still errors when it excludes everything.
95
96
  - **8-K item codes:** SEC replaced the original single-integer item numbering with the current `x.xx` scheme in Release 33-8400, effective **2004-08-23**, so EDGAR's `items` field carries two disjoint vocabularies. `eight-k-items.ts` decodes by the code's **shape**, never the filing date — dotted is current, a bare integer is legacy — because filers reported pre-changeover events under the old numbers for months after the effective date, and a date-keyed table would mislabel exactly those. The two vocabularies reuse integers for unrelated events (legacy `5` is Other Events, current `5.02` is officer departures; legacy `12` is the ancestor of `2.02`), so a dotted `items` filter never matches a pre-2004 filing — `get_material_events`' zero-hit notice says so and echoes the codes actually present. The tables back three surfaces: the tool's `items` enum, the decoded per-filing rows, and the `secedgar://filing-types` resource. A code matching a shape but absent from that regime's table comes back with no label rather than a guess.
@@ -100,10 +101,10 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
100
101
  - **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.
101
102
  - **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.
102
103
  - **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.
103
- - **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); with a `filed_after` / `filed_before` window it reads the in-window Form 4 / 4-A rows with an XML primary document — `recent`, then the archive pages overlapping the window, newest first, stopping once the scan cap plus a sentinel row are in hand — and with a canvas parses every one up to 100 instead of the 40-filing floor, since the window already bounds the set; `dataset.truncated` is then the sentinel or unread overlapping pages, and `history_scanned_through` the oldest filing parsed (#127); 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`.
104
+ - **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); with a `filed_after` / `filed_before` window it reads the in-window Form 4 / 4-A rows with an XML primary document — `recent`, then the archive pages overlapping the window, newest first, stopping once the scan cap plus a sentinel row are in hand — and with a canvas parses every one up to 100 instead of the 40-filing floor, since the window already bounds the set; `dataset.truncated` is then the sentinel or unread overlapping pages, and `history_scanned_through` the oldest filing parsed (#127); 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); the inline window is the newest periods of the union across companies, so a company-concept pair whose values all predate it is named in `caveats` — one line per concept, each company with its newest period and the dataframe pointer — and never in `gaps`, which keeps meaning no value in any period (#144). 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`.
104
105
  - **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.
105
106
  - **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.
106
- - **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 (superseding the mirror's bare fund rows with series/class, and covering a mirror synced before MF ingestion), so funds resolve there too (#43, #135).
107
+ - **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. The ingester stores a tag's own name when SEC serves it with no label (`InterestExpenseNonoperating`), so every read path treats a stored label equal to a camel-case compound tag as no label (`storedLabel`) — callers fall back to the concept's label, and `getFrames` answers an empty `label` as the live frames API does; a one-word element's label is the word itself (`Revenues`) and is kept. Read-side, so stored rows need no migration. 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 (superseding the mirror's bare fund rows with series/class, and covering a mirror synced before MF ingestion), so funds resolve there too (#43, #135).
107
108
 
108
109
  ---
109
110
 
@@ -132,7 +133,8 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
132
133
  | `EdgarApiService` | `src/services/edgar/edgar-api-service.ts` | Paced HTTP client with the rate-limit block gate, CIK resolution, all SEC API calls |
133
134
  | `submissions-archive` | `src/services/edgar/submissions-archive.ts` | `SubmissionsArchiveWalk` — the one walk over `filings.files[]` archive pages: shared page cap, caller-chosen order, early stop, scan depth and truncation |
134
135
  | `filing-headers` | `src/services/edgar/filing-headers.ts` | SEC submission-header parsers: per-document types and the submission's form / filing date / period |
135
- | `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list |
136
+ | `concept-map` | `src/services/edgar/concept-map.ts` | Static friendly name → XBRL tag mapping; `resolveConceptTarget` picks the taxonomy + tag list; `findUnknownConcept` rejects names that are neither |
137
+ | `trigram-similarity` | `src/services/edgar/trigram-similarity.ts` | Dice trigram scorer behind company and concept near-match suggestions |
136
138
  | `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 |
137
139
  | `fiscal-periods` | `src/services/edgar/fiscal-periods.ts` | Fiscal-Q4 frame caveat and per-filer missing-quarter detection |
138
140
  | `eight-k-items` | `src/services/edgar/eight-k-items.ts` | 8-K item-code decode tables for both numbering regimes; shape-driven `decodeEightKItem` |
@@ -214,6 +216,7 @@ src/
214
216
  filing-headers.ts # SEC submission-header parsers
215
217
  concept-map.ts # Friendly name → XBRL tag mapping + taxonomy/tag resolution
216
218
  concept-series.ts # Shared frame dedup, tag selection, staleness caveats
219
+ trigram-similarity.ts # Dice trigram scorer for near-match suggestions
217
220
  fiscal-periods.ts # Fiscal Q4 / missing-quarter caveats
218
221
  eight-k-items.ts # 8-K item-code decode tables (current + legacy regimes)
219
222
  xml-nodes.ts # Shared XML tag lookup + value coercion helpers
package/README.md CHANGED
@@ -98,14 +98,15 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
98
98
  ### `secedgar_get_financials` <sub>tool</sub>
99
99
 
100
100
  - `company` (ticker or CIK) plus `concept` as a friendly name or raw XBRL tag; `taxonomy` `us-gaap` (default), `ifrs-full`, or `dei`; `period_type` `annual`, `quarterly`, or `all`, defaulting to annual with a fallback to the full series for instant concepts; `limit` 1–100 trims the inline series
101
- - A deduplicated series, newest first, one value per calendar period with its source `form`, `filed` date, and `accession_number`; `tags_tried` names the tags walked, and an empty result fails as `no_concept_data`, `no_frame_data`, or `no_period_data`
101
+ - A deduplicated series, newest first, one value per calendar period with its source `form`, `filed` date, `accession_number`, and `tag`; `tags_tried` names the tags walked, and an empty result fails as `no_concept_data`, `no_frame_data`, or `no_period_data`
102
+ - A `concept` that is neither a friendly name nor an UpperCamelCase tag fails as `unknown_concept` before any SEC request, with a formula for standard combinations (`free_cash_flow`, `ebitda`, `working_capital`) or up to three closest friendly names
102
103
 
103
104
  ---
104
105
 
105
106
  ### `secedgar_get_snapshot` <sub>tool</sub>
106
107
 
107
108
  - `company`, `taxonomy` `us-gaap` (default) or `ifrs-full`, and `period_type` `annual`, `quarterly`, or `both` (default); one companyfacts read covers every supported concept, and nothing is staged as a dataframe
108
- - Each `lines` entry reports the latest `annual` and `quarterly` value for a duration concept, or the latest `instant` value for balance-sheet and entity-info concepts, with the `tag` and `taxonomy` it came from; concepts the filer doesn't report land in `gaps` with `tags_tried`
109
+ - Each `lines` entry reports the latest `annual` and `quarterly` value for a duration concept, or the latest `instant` value for balance-sheet and entity-info concepts, each with the `tag` it came from, under the line's `taxonomy`; concepts the filer doesn't report land in `gaps` with `tags_tried`
109
110
 
110
111
  ---
111
112
 
@@ -160,14 +161,18 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
160
161
 
161
162
  - `concept` as a friendly name or raw tag, `period` as `CY2023`, `CY2024Q2`, or `CY2023Q4I`, `unit` (default `USD`), `sort` `desc` / `asc`; `limit` 1–100 (default 25) with `offset` / `next_offset` down the ranking
162
163
  - One call queries one tag: `unqueried_tags` lists same-meaning variants to fetch separately, and `related_tags` lists alternate-definition tags some filers report instead
163
- - `value_distribution.max_to_p95_ratio` flags scale-factor outliers, `period_end_range` shows fiscal-year mixing, and `caveats` names the fiscal-Q4 gap in quarterly frames
164
+ - `value_distribution.max_to_p95_ratio` flags scale-factor outliers, `period_end_range` shows fiscal-year mixing, and `caveats` names the fiscal-Q4 gap in quarterly frames, the proxy-statement rows in annual `NetIncomeLoss` frames, and the 10-Q trailing-twelve-month rows an annual frame can hold while its year is still open
165
+ - SEC publishes frames for us-gaap and dei tags only: `taxonomy` `us-gaap` (default) or `dei` picks the namespace for a raw tag (`EntityCommonStockSharesOutstanding` is dei), a friendly name keeps its own mapped taxonomy (`shares_outstanding` reads dei), and an explicit `dei` reads a friendly name's tags from dei, as in `secedgar_get_financials`; IFRS filers are read per company with `taxonomy` `ifrs-full`
166
+ - A `concept` that is neither a friendly name nor an UpperCamelCase tag fails as `unknown_concept` before the frames request, with the same formula or closest-name hint as `secedgar_get_financials`; a well-formed tag with no frame is `no_data`
164
167
 
165
168
  ---
166
169
 
167
170
  ### `secedgar_compare_companies` <sub>tool</sub>
168
171
 
169
172
  - 2–10 `companies` × 1–8 `concepts`; `taxonomy` `us-gaap` (default) or `ifrs-full`; `period_type` `annual` (default) or `quarterly`; `periods` 1–12 (default 4), trimmed further when the inline matrix gets too large
170
- - `cells` align each value on a calendar `period` and keep its `frame` and `period_end`; `failed_companies` (reason `not_found`, `ambiguous`, or `no_company_facts`) and `gaps` report what's missing, and `caveats` flag differing period ends and unit mismatches
173
+ - `cells` align each value on a calendar `period` and keep its `frame`, `period_end`, and source `tag`; `failed_companies` (reason `not_found`, `ambiguous`, or `no_company_facts`) and `gaps` (no value in any period) report what's missing, and `caveats` flag differing period ends, unit mismatches, and, once per concept, the companies whose values all predate the inline window, each with its newest period
174
+ - A concept that is neither a friendly name nor an UpperCamelCase tag is listed once in `unknown_concepts` with its hint, never as a gap per company; the call fails as `unknown_concept` only when every concept is one
175
+ - Inputs naming the same concept (`revenue` and `Revenue`, or one raw tag spelled twice) are compared once under the first spelling, with a caveat naming the merged inputs; a friendly name and a raw tag it maps to (`revenue` and `Revenues`) stay separate
171
176
 
172
177
  ---
173
178
 
@@ -217,7 +222,7 @@ SEC EDGAR filings, XBRL financials, and ownership data. No API key needed, only
217
222
  ### `secedgar_company_analysis` <sub>prompt</sub>
218
223
 
219
224
  - Arguments: `company` required; `focus_areas` optional free text
220
- - Returns one user message with a numbered workflow (company search, financial trends, filing review, material events, peer comparison via `secedgar_fetch_frames`) and a findings template; insider, institutional, or blockholder terms in `focus_areas` add those ownership steps, and "ownership" adds all three
225
+ - Returns one user message with a numbered workflow (company search, a financial profile via `secedgar_get_snapshot` with trends via `secedgar_get_financials`, filing review, material events, and a peer comparison via `secedgar_compare_companies` with `secedgar_fetch_frames` for a market-wide ranking) and a findings template; insider, institutional, or blockholder terms in `focus_areas` add those ownership steps, and "ownership" adds all three
221
226
 
222
227
  ## Features
223
228
 
@@ -227,7 +232,7 @@ EDGAR-specific:
227
232
 
228
233
  - One process-wide queue paces SEC requests under the 10 req/s limit. A 429 is never retried: every SEC call is refused locally as `rate_limited` with a `retryAfter` countdown for `EDGAR_RATE_LIMIT_COOLDOWN_SECONDS`, then a single probe goes out. Reads served from the local mirror keep answering
229
234
  - CIK resolution from tickers (fund tickers included), current and former company names, or raw CIKs, with corporate-suffix normalization and near-match suggestions on a miss
230
- - Friendly XBRL concept names that handle historical tag changes. `secedgar_get_financials`, `secedgar_get_snapshot`, and `secedgar_compare_companies` share one frame dedup and tag priority, so their numbers agree, and each reports `caveats` for calendar quarters missing from the frame-tagged series (SEC files fiscal Q4 only as the 10-K residual) and for series that stop years short
235
+ - Friendly XBRL concept names that handle historical tag changes. `secedgar_get_financials`, `secedgar_get_snapshot`, and `secedgar_compare_companies` share one frame dedup and tag priority, so their numbers agree; a period whose frame SEC assigned to a proxy statement's pay-versus-performance figure is answered from the filer's own report instead, an annual frame holding a 10-Q's trailing-twelve-month figure is left out of the annual series, and each reports `caveats` for calendar quarters missing from the frame-tagged series (SEC files fiscal Q4 only as the 10-K residual) and for series that stop years short
231
236
  - Filing documents converted from HTML to text, with heading detection and offset paging for oversized filings
232
237
  - Opt-in local SQLite mirror of company tickers and XBRL company-facts (`EDGAR_MIRROR_ENABLED`) that serves CIK resolution and financials from disk
233
238
 
@@ -0,0 +1,32 @@
1
+ ---
2
+ summary: "Annual XBRL values no longer come from a proxy statement's pay-versus-performance figure or a 10-Q's trailing-twelve-month total. Adds ppe_net, pretax_income, and shares_diluted, successor tags for capex and interest_expense, early unknown_concept errors with suggestions, and a taxonomy input on secedgar_fetch_frames."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.15.9 — 2026-09-25
8
+
9
+ ## Added
10
+
11
+ - **Catalog concepts `ppe_net`, `pretax_income`, and `shares_diluted`** ([#130](https://github.com/cyanheads/secedgar-mcp-server/issues/130)) — each with a confirmed IFRS tag; `ppe_net` lists the finance-lease-inclusive total as a related tag, not a fallback.
12
+ - **`unknown_concept` before any SEC request** ([#128](https://github.com/cyanheads/secedgar-mcp-server/issues/128)) — `secedgar_get_financials`, `secedgar_compare_companies`, and `secedgar_fetch_frames` reject a `concept` that is neither a friendly name nor an UpperCamelCase tag, with a formula (`free_cash_flow`, `ebitda`, `working_capital`) or up to three closest names. `secedgar_compare_companies` fails only when every concept is unknown and otherwise lists them in `unknown_concepts`.
13
+ - **`secedgar_fetch_frames` `taxonomy` input, `us-gaap` or `dei`** ([#143](https://github.com/cyanheads/secedgar-mcp-server/issues/143)) — a raw dei tag such as `EntityCommonStockSharesOutstanding` now reads the dei frames; the response echoes `taxonomy`.
14
+ - **Per-value `tag`** on `secedgar_get_financials` rows, `secedgar_get_snapshot` points, and `secedgar_compare_companies` cells ([#125](https://github.com/cyanheads/secedgar-mcp-server/issues/125)).
15
+
16
+ ## Changed
17
+
18
+ - **`capex` walks `PaymentsToAcquireProductiveAssets` and `interest_expense` walks `InterestExpenseNonoperating`** ([#125](https://github.com/cyanheads/secedgar-mcp-server/issues/125)) — both behind the existing tags, so they fill only frames those leave empty. A line's `concept` / `tag`, `label`, and `unit` now describe the tag behind its newest value.
19
+ - **`secedgar_company_analysis` routes the financial profile through `secedgar_get_snapshot` and the peer step through `secedgar_compare_companies`** ([#129](https://github.com/cyanheads/secedgar-mcp-server/issues/129)); `secedgar://concepts` and `secedgar_search_concepts` name all three tools that take a friendly name.
20
+ - **`secedgar_compare_companies` compares inputs naming the same concept once** ([#145](https://github.com/cyanheads/secedgar-mcp-server/issues/145)) — `revenue` and `Revenue` merge under the first spelling with a caveat; a friendly name and a raw tag it maps to stay separate.
21
+
22
+ ## Fixed
23
+
24
+ - **A frame held by a Schedule 14A/14C proxy statement resolves to the filer's own report of the period** ([#123](https://github.com/cyanheads/secedgar-mcp-server/issues/123)) — the pay-versus-performance `NetIncomeLoss` no longer replaces the 10-K value. Live `secedgar_fetch_frames` responses carry a caveat on annual `NetIncomeLoss`; mirror-served frames apply the rule.
25
+ - **An annual frame held by a 10-Q's trailing-twelve-month figure leaves the annual series** ([#142](https://github.com/cyanheads/secedgar-mcp-server/issues/142)) — replaced by the 10-K fact when one exists, else kept only when the series marks it a closed fiscal year. Live `secedgar_fetch_frames` caveats annual frames through April of the following year.
26
+ - **A companyconcept unit served as an empty object no longer fails `secedgar_get_financials`** ([#141](https://github.com/cyanheads/secedgar-mcp-server/issues/141)) — non-array units are dropped at the service edge and the tag is answered from companyfacts.
27
+ - **`secedgar_compare_companies` names a company whose values all predate the inline window** ([#144](https://github.com/cyanheads/secedgar-mcp-server/issues/144)) — one caveat per concept with each company's newest period; `gaps` still means no value in any period.
28
+ - **Mirror reads no longer print a raw tag as its label** when SEC served none (`InterestExpenseNonoperating`), and `secedgar_fetch_frames` treats SEC's bare `-` `loc` as no location.
29
+
30
+ ## Dependencies
31
+
32
+ - `@socketsecurity/bun-security-scanner` ^1.1.2 → ^1.1.3 (dev)
@@ -23,7 +23,7 @@ export const companyAnalysisPrompt = prompt('secedgar_company_analysis', {
23
23
  const wantsBlockholders = /blockholder|activist|beneficial|13[\s-]?[dg]|stake|ownership/.test(focus);
24
24
  const steps = [
25
25
  '**Company Identification** — Use `secedgar_company_search` to resolve the company and review recent filings.',
26
- '**Financial Trends** — Use `secedgar_get_financials` to pull key metrics (revenue, net_income, eps_diluted, assets, debt, operating_cash_flow) and identify trends over the last 3-5 years.',
26
+ '**Financial Trends** — Use `secedgar_get_snapshot` for the latest value of every supported concept in one call, then `secedgar_get_financials` for the 3-5 year trend of the lines that matter most (for example revenue, net_income, eps_diluted, debt, operating_cash_flow).',
27
27
  '**Recent Filings Review** — Use `secedgar_get_filing` to read the most recent 10-K or 10-Q for qualitative insights (risk factors, MD&A, business overview).',
28
28
  '**Material Events** — Use `secedgar_get_material_events` to surface recent 8-K filings with their item codes decoded, narrowing to the events that matter (`items: ["1.01"]` for material agreements, `["2.02"]` for results, `["5.02"]` for officer changes, `["4.02"]` for non-reliance).',
29
29
  ];
@@ -36,7 +36,7 @@ export const companyAnalysisPrompt = prompt('secedgar_company_analysis', {
36
36
  if (wantsBlockholders) {
37
37
  steps.push("**Blockholders** — Use `secedgar_get_beneficial_owners` for the 5%-and-over stakes, reading each reporting person separately (percent of class is per person, so joint filers do not sum). A 13D carries the holder's stated purpose of the transaction; a 13G is the passive form and has none. Structured coverage starts 2024-12-18 — the response counts the earlier text filings, which `secedgar_search_filings` finds and `secedgar_get_filing` reads.");
38
38
  }
39
- steps.push('**Industry Context** — Use `secedgar_fetch_frames` to compare key metrics against peers.');
39
+ steps.push('**Industry Context** — Choose a few comparable companies and use `secedgar_compare_companies` with this company and those peers as `companies` (2-10 in all) and the key metrics as `concepts` (up to 8), aligned on calendar periods. To see where one metric ranks across every reporting company in a single period, use `secedgar_fetch_frames`.');
40
40
  const findings = [
41
41
  '- **Company Overview** — entity details, industry, fiscal year',
42
42
  '- **Financial Summary** — key metrics table with trends',
@@ -1 +1 @@
1
- {"version":3,"file":"company-analysis.prompt.js","sourceRoot":"","sources":["../../../../src/mcp-server/prompts/definitions/company-analysis.prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,2BAA2B,EAAE;IACvE,WAAW,EAAE,mKAAmK;IAEhL,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;QACtF,WAAW,EAAE,CAAC;aACX,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,8HAA8H,CAC/H;KACJ,CAAC;IAEF,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;QACjB,oFAAoF;QACpF,qFAAqF;QACrF,sFAAsF;QACtF,kEAAkE;QAClE,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,YAAY,GAAG,0CAA0C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5E,MAAM,kBAAkB,GAAG,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3E,MAAM,iBAAiB,GAAG,8DAA8D,CAAC,IAAI,CAC3F,KAAK,CACN,CAAC;QAEF,MAAM,KAAK,GAAa;YACtB,8GAA8G;YAC9G,6LAA6L;YAC7L,8JAA8J;YAC9J,6RAA6R;SAC9R,CAAC;QACF,IAAI,YAAY,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CACR,oVAAoV,CACrV,CAAC;QACJ,CAAC;QACD,IAAI,kBAAkB,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CACR,kTAAkT,CACnT,CAAC;QACJ,CAAC;QACD,IAAI,iBAAiB,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CACR,+bAA+b,CAChc,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CACR,0FAA0F,CAC3F,CAAC;QAEF,MAAM,QAAQ,GAAa;YACzB,gEAAgE;YAChE,yDAAyD;YACzD,yDAAyD;YACzD,kEAAkE;SACnE,CAAC;QACF,IAAI,YAAY,IAAI,kBAAkB,IAAI,iBAAiB,EAAE,CAAC;YAC5D,QAAQ,CAAC,IAAI,CACX,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QAClF,QAAQ,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QAElF,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE;wBACJ,iCAAiC,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,qBAAqB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;wBACpH,EAAE;wBACF,kCAAkC;wBAClC,EAAE;wBACF,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;wBAC9C,EAAE;wBACF,sBAAsB;wBACtB,GAAG,QAAQ;qBACZ,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
1
+ {"version":3,"file":"company-analysis.prompt.js","sourceRoot":"","sources":["../../../../src/mcp-server/prompts/definitions/company-analysis.prompt.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,2BAA2B,EAAE;IACvE,WAAW,EAAE,mKAAmK;IAEhL,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wDAAwD,CAAC;QACtF,WAAW,EAAE,CAAC;aACX,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,8HAA8H,CAC/H;KACJ,CAAC;IAEF,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE;QACjB,oFAAoF;QACpF,qFAAqF;QACrF,sFAAsF;QACtF,kEAAkE;QAClE,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACrD,MAAM,YAAY,GAAG,0CAA0C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5E,MAAM,kBAAkB,GAAG,mCAAmC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3E,MAAM,iBAAiB,GAAG,8DAA8D,CAAC,IAAI,CAC3F,KAAK,CACN,CAAC;QAEF,MAAM,KAAK,GAAa;YACtB,8GAA8G;YAC9G,gRAAgR;YAChR,8JAA8J;YAC9J,6RAA6R;SAC9R,CAAC;QACF,IAAI,YAAY,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CACR,oVAAoV,CACrV,CAAC;QACJ,CAAC;QACD,IAAI,kBAAkB,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CACR,kTAAkT,CACnT,CAAC;QACJ,CAAC;QACD,IAAI,iBAAiB,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CACR,+bAA+b,CAChc,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CACR,sVAAsV,CACvV,CAAC;QAEF,MAAM,QAAQ,GAAa;YACzB,gEAAgE;YAChE,yDAAyD;YACzD,yDAAyD;YACzD,kEAAkE;SACnE,CAAC;QACF,IAAI,YAAY,IAAI,kBAAkB,IAAI,iBAAiB,EAAE,CAAC;YAC5D,QAAQ,CAAC,IAAI,CACX,gHAAgH,CACjH,CAAC;QACJ,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QAClF,QAAQ,CAAC,IAAI,CAAC,kEAAkE,CAAC,CAAC;QAElF,OAAO;YACL;gBACE,IAAI,EAAE,MAAM;gBACZ,OAAO,EAAE;oBACP,IAAI,EAAE,MAAM;oBACZ,IAAI,EAAE;wBACJ,iCAAiC,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,qBAAqB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;wBACpH,EAAE;wBACF,kCAAkC;wBAClC,EAAE;wBACF,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;wBAC9C,EAAE;wBACF,sBAAsB;wBACtB,GAAG,QAAQ;qBACZ,CAAC,IAAI,CAAC,IAAI,CAAC;iBACb;aACF;SACF,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Reference list of common XBRL financial concepts grouped by financial statement.
3
- * The "menu" an agent reads before calling secedgar_get_financials.
3
+ * The "menu" an agent reads before calling secedgar_get_financials, secedgar_compare_companies,
4
+ * or secedgar_fetch_frames — the three tools that take a friendly name as concept input.
4
5
  * @module mcp-server/resources/definitions/concepts
5
6
  */
6
7
  export declare const conceptsResource: import("@cyanheads/mcp-ts-core").ResourceDefinition<import("zod").ZodObject<Readonly<{
@@ -1 +1 @@
1
- {"version":3,"file":"concepts.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/concepts.resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAeH,eAAO,MAAM,gBAAgB;;wDA6D3B,CAAC"}
1
+ {"version":3,"file":"concepts.resource.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/concepts.resource.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH,eAAO,MAAM,gBAAgB;;wDA6D3B,CAAC"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Reference list of common XBRL financial concepts grouped by financial statement.
3
- * The "menu" an agent reads before calling secedgar_get_financials.
3
+ * The "menu" an agent reads before calling secedgar_get_financials, secedgar_compare_companies,
4
+ * or secedgar_fetch_frames — the three tools that take a friendly name as concept input.
4
5
  * @module mcp-server/resources/definitions/concepts
5
6
  */
6
7
  import { resource } from '@cyanheads/mcp-ts-core';
@@ -14,7 +15,7 @@ const GROUP_LABELS = {
14
15
  };
15
16
  export const conceptsResource = resource('secedgar://concepts', {
16
17
  name: 'XBRL Financial Concepts',
17
- description: 'Reference list of common XBRL financial concepts grouped by financial statement, mapping friendly names accepted by secedgar_get_financials and secedgar_fetch_frames to their XBRL tags.',
18
+ description: 'Reference list of common XBRL financial concepts grouped by financial statement, mapping friendly names accepted by secedgar_get_financials, secedgar_compare_companies, and secedgar_fetch_frames to their XBRL tags.',
18
19
  mimeType: 'text/markdown',
19
20
  handler(_params, _ctx) {
20
21
  const groups = new Map();
@@ -24,8 +25,8 @@ export const conceptsResource = resource('secedgar://concepts', {
24
25
  groups.set(entry.group, list);
25
26
  }
26
27
  const lines = ['# XBRL Financial Concepts', ''];
27
- lines.push('Use these friendly names with `secedgar_get_financials` and `secedgar_fetch_frames`.', '');
28
- lines.push('Raw XBRL tags are also accepted as an escape hatch for concepts not listed here.', '');
28
+ lines.push('Use these friendly names with `secedgar_get_financials`, `secedgar_compare_companies`, and `secedgar_fetch_frames`.', '');
29
+ lines.push('Raw XBRL tags (UpperCamelCase element names such as `NetIncomeLoss`) are also accepted as an escape hatch for concepts not listed here.', '');
29
30
  for (const [group, items] of groups) {
30
31
  if (items.length === 0)
31
32
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"concepts.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/concepts.resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAG/D,MAAM,YAAY,GAAiC;IACjD,gBAAgB,EAAE,kBAAkB;IACpC,aAAa,EAAE,eAAe;IAC9B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,aAAa;CAC3B,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,qBAAqB,EAAE;IAC9D,IAAI,EAAE,yBAAyB;IAC/B,WAAW,EACT,2LAA2L;IAC7L,QAAQ,EAAE,eAAe;IAEzB,OAAO,CAAC,OAAO,EAAE,IAAI;QACnB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAgC,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,KAAK,GAAa,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC;QAC1D,KAAK,CAAC,IAAI,CACR,sFAAsF,EACtF,EAAE,CACH,CAAC;QACF,KAAK,CAAC,IAAI,CACR,kFAAkF,EAClF,EAAE,CACH,CAAC;QAEF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACjC,KAAK,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;YACtE,KAAK,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;YAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5D,sEAAsE;gBACtE,+DAA+D;gBAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM;oBACpC,CAAC,CAAC,oBAAoB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACvE,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM;oBACtC,CAAC,CAAC,uDAAuD,IAAI,CAAC,WAAW;yBACpE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC;yBACtC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACjB,CAAC,CAAC,EAAE,CAAC;gBACP,KAAK,CAAC,IAAI,CACR,OAAO,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,KAAK,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,IAAI,IAAI,CAC3G,CAAC;YACJ,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QACjB,SAAS,EAAE;YACT;gBACE,GAAG,EAAE,qBAAqB;gBAC1B,IAAI,EAAE,yBAAyB;gBAC/B,WAAW,EAAE,4DAA4D;gBACzE,QAAQ,EAAE,eAAe;aAC1B;SACF;KACF,CAAC;CACH,CAAC,CAAC"}
1
+ {"version":3,"file":"concepts.resource.js","sourceRoot":"","sources":["../../../../src/mcp-server/resources/definitions/concepts.resource.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAG/D,MAAM,YAAY,GAAiC;IACjD,gBAAgB,EAAE,kBAAkB;IACpC,aAAa,EAAE,eAAe;IAC9B,SAAS,EAAE,WAAW;IACtB,SAAS,EAAE,WAAW;IACtB,WAAW,EAAE,aAAa;CAC3B,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,qBAAqB,EAAE;IAC9D,IAAI,EAAE,yBAAyB;IAC/B,WAAW,EACT,wNAAwN;IAC1N,QAAQ,EAAE,eAAe;IAEzB,OAAO,CAAC,OAAO,EAAE,IAAI;QACnB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAgC,CAAC;QACvD,KAAK,MAAM,KAAK,IAAI,YAAY,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACjB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,KAAK,GAAa,CAAC,2BAA2B,EAAE,EAAE,CAAC,CAAC;QAC1D,KAAK,CAAC,IAAI,CACR,qHAAqH,EACrH,EAAE,CACH,CAAC;QACF,KAAK,CAAC,IAAI,CACR,yIAAyI,EACzI,EAAE,CACH,CAAC;QAEF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC;YACpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YACjC,KAAK,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,yDAAyD,CAAC,CAAC;YACtE,KAAK,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;YAC9D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC5D,sEAAsE;gBACtE,+DAA+D;gBAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM;oBACpC,CAAC,CAAC,oBAAoB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACvE,CAAC,CAAC,EAAE,CAAC;gBACP,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM;oBACtC,CAAC,CAAC,uDAAuD,IAAI,CAAC,WAAW;yBACpE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC;yBACtC,IAAI,CAAC,IAAI,CAAC,EAAE;oBACjB,CAAC,CAAC,EAAE,CAAC;gBACP,KAAK,CAAC,IAAI,CACR,OAAO,IAAI,CAAC,IAAI,QAAQ,IAAI,CAAC,KAAK,MAAM,OAAO,GAAG,QAAQ,GAAG,OAAO,MAAM,IAAI,CAAC,QAAQ,MAAM,IAAI,CAAC,IAAI,IAAI,CAC3G,CAAC;YACJ,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;QAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QACjB,SAAS,EAAE;YACT;gBACE,GAAG,EAAE,qBAAqB;gBAC1B,IAAI,EAAE,yBAAyB;gBAC/B,WAAW,EAAE,4DAA4D;gBACzE,QAAQ,EAAE,eAAe;aAC1B;SACF;KACF,CAAC;CACH,CAAC,CAAC"}
@@ -65,6 +65,11 @@ export declare const compareCompaniesTool: import("@cyanheads/mcp-ts-core").Tool
65
65
  concept: z.ZodString;
66
66
  tags_tried: z.ZodArray<z.ZodString>;
67
67
  }, z.core.$strip>>;
68
+ unknown_concepts: z.ZodArray<z.ZodObject<{
69
+ concept: z.ZodString;
70
+ derivation: z.ZodOptional<z.ZodString>;
71
+ suggestions: z.ZodArray<z.ZodString>;
72
+ }, z.core.$strip>>;
68
73
  caveats: z.ZodArray<z.ZodString>;
69
74
  dataset: z.ZodOptional<z.ZodObject<{
70
75
  name: z.ZodString;
@@ -81,6 +86,11 @@ export declare const compareCompaniesTool: import("@cyanheads/mcp-ts-core").Tool
81
86
  readonly code: JsonRpcErrorCode.NotFound;
82
87
  readonly when: "Companies resolved but not one of them reports any of the requested concepts for the requested period type";
83
88
  readonly recovery: "Switch period_type, or discover reported concept names with secedgar_search_concepts.";
89
+ }, {
90
+ readonly reason: "unknown_concept";
91
+ readonly code: JsonRpcErrorCode.NotFound;
92
+ readonly when: "Every requested concept is neither a supported friendly name nor shaped like an XBRL tag, so no request is sent";
93
+ readonly recovery: "Use a friendly name from secedgar_search_concepts or a valid raw XBRL tag.";
84
94
  }, {
85
95
  readonly reason: "rate_limited";
86
96
  readonly code: JsonRpcErrorCode.RateLimited;
@@ -1 +1 @@
1
- {"version":3,"file":"compare-companies.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/compare-companies.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAwEjE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+hB/B,CAAC"}
1
+ {"version":3,"file":"compare-companies.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/compare-companies.tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAkFjE,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsrB/B,CAAC"}