@pipeworx/mcp-edgar 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The Securities and Exchange Commission's filings database. Every public company in the US files here, every filing is timestamped and immutable, and every disclosure (revenue, debt, risk factors, executive compensation, M&A activity, insider trading) is structured for machine retrieval. Free and authoritative.
4
4
 
5
- Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1784+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
5
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1819+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
6
6
 
7
7
  ## Why this matters for AI agents
8
8
 
@@ -106,7 +106,7 @@ directly, instead of just this one's:
106
106
  }
107
107
  ```
108
108
 
109
- Both URLs reach the same gateway and the same 1784+ data sources. The
109
+ Both URLs reach the same gateway and the same 1819+ data sources. The
110
110
  only difference is which pack's tools are listed **directly**; `ask_pipeworx`
111
111
  reaches all of them from either one.
112
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipeworx/mcp-edgar",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -26,7 +26,7 @@
26
26
  "@cloudflare/workers-types": "^4.20260405.1"
27
27
  },
28
28
  "pipeworx": {
29
- "sourceHash": "v1-d35d4a284237ace3314859c5df49ad1b77a0a9e2e7696d3f0b601f112b20e42a",
30
- "sourceCommit": "3723e46c6d115bd4a1fd2175e6bf758555e7cb50"
29
+ "sourceHash": "v1-7ce3b2ab39a6713c682f44a33fe013a7e07b55298b5e01883e16f33b0d1ab1d4",
30
+ "sourceCommit": "a03f96ed672b8c149593c64ecbf30280cabf084c"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.pipeworx-io/edgar",
4
4
  "title": "Edgar",
5
5
  "description": "EDGAR MCP — SEC EDGAR public APIs (free, no auth)",
6
- "version": "0.3.9",
6
+ "version": "0.3.11",
7
7
  "websiteUrl": "https://pipeworx.io/packs/edgar",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-edgar",
package/src/index.ts CHANGED
@@ -699,6 +699,16 @@ const normalize = (s: string): string =>
699
699
  * exact (4) > prefix (3) > whole-word (2) > substring (1). Ties break by shortest
700
700
  * name — the primary entity (e.g. "Apple Inc." over "Apple Hospitality REIT").
701
701
  * Returns only items that match at all, best first. Pure (no I/O).
702
+ *
703
+ * `prefix` requires a WORD BOUNDARY (`n.startsWith(q + ' ')`), not a bare
704
+ * `startsWith`. A bare `startsWith` let "ZOOM" claim "ZOOMCAR HOLDINGS INC" at
705
+ * the SAME tier as "Apple" claiming "APPLE INC." — "Zoomcar" is a different
706
+ * word that happens to begin with the same letters, not the brand name plus a
707
+ * corporate-form suffix. That conflation is what sent resolve_entity("Zoom")
708
+ * to Zoomcar and resolve_entity("Audi") to AudioEye Inc (fleet #2776's
709
+ * mis-resolution fixture) — both now correctly fall through to the weaker
710
+ * `substring` tier instead of tying with a genuine word-boundary prefix hit
711
+ * like "Palantir" -> "Palantir Technologies Inc." (fleet #3100).
702
712
  */
703
713
  function rankMatches<T>(
704
714
  query: string,
@@ -714,7 +724,7 @@ function rankMatches<T>(
714
724
  let kind: MatchKind | null = null;
715
725
  let score = 0;
716
726
  if (n === q) { kind = 'exact'; score = 4; }
717
- else if (n.startsWith(q)) { kind = 'prefix'; score = 3; }
727
+ else if (n.startsWith(`${q} `)) { kind = 'prefix'; score = 3; }
718
728
  else if (n.includes(` ${q} `) || n.endsWith(` ${q}`)) { kind = 'word'; score = 2; }
719
729
  else if (n.includes(q)) { kind = 'substring'; score = 1; }
720
730
  if (kind) scored.push({ item, kind, score, len: name.length });
@@ -731,7 +741,15 @@ interface SecEntity {
731
741
  cik_padded: string;
732
742
  company_name: string;
733
743
  matched_by: 'ticker' | 'company_name';
734
- alternatives?: { ticker: string; company_name: string; cik: string }[];
744
+ /**
745
+ * The `rankMatches` kind that won, when `matched_by` is `'company_name'`
746
+ * (a ticker hit has no kind — it did not go through ranking at all). Lets a
747
+ * caller tell a word-boundary PREFIX hit ("Palantir" -> "Palantir
748
+ * Technologies Inc.") from a weaker `word`/`substring` guess, which
749
+ * `matched_by: 'company_name'` alone cannot (fleet #3100).
750
+ */
751
+ matched_kind?: MatchKind;
752
+ alternatives?: { ticker: string; company_name: string; cik: string; kind: MatchKind }[];
735
753
  }
736
754
 
737
755
  const SEC_TICKERS_URL = 'https://www.sec.gov/files/company_tickers.json';
@@ -767,10 +785,12 @@ async function resolveSecEntity(
767
785
  const ranked = rankMatches(query, rows, (r) => r.title);
768
786
  if (ranked.length) {
769
787
  const best = toEntity(ranked[0].item, 'company_name');
788
+ best.matched_kind = ranked[0].kind;
770
789
  const alts = ranked.slice(1, 4).map((m) => ({
771
790
  ticker: m.item.ticker,
772
791
  company_name: m.item.title,
773
792
  cik: String(m.item.cik_str),
793
+ kind: m.kind,
774
794
  }));
775
795
  if (alts.length) best.alternatives = alts;
776
796
  return best;
@@ -1238,7 +1258,7 @@ const tools: McpToolExport['tools'] = [
1238
1258
  concept: {
1239
1259
  type: 'string',
1240
1260
  description:
1241
- 'REQUIRED (alias `metric`). Metric name. Common: "Revenue" / "Revenues", "NetIncomeLoss", "Cash", "Assets", "Liabilities", "StockholdersEquity", "EarningsPerShareDiluted", "LongTermDebt".',
1261
+ 'REQUIRED (alias `metric`). Metric name. Common: "Revenue" / "Revenues", "NetIncomeLoss", "Cash", "Assets", "Liabilities", "StockholdersEquity", "EarningsPerShareDiluted", "LongTermDebt", "SharesOutstanding" (newest cover-page count, dei), "Capex", "OperatingCashFlow", "ResearchAndDevelopment", "SGA", "CostOfRevenue", "DilutedShares", "PublicFloat". Exact us-gaap or dei tag names also work.',
1242
1262
  },
1243
1263
  period: {
1244
1264
  type: 'string',
@@ -3128,6 +3148,17 @@ async function companyFacts(cikOrTicker: string) {
3128
3148
  // Anything not in the table still falls through to [concept] (the bare
3129
3149
  // literal), so we don't regress for niche concepts not yet observed in
3130
3150
  // failures.
3151
+ // Cover-page tags live in the `dei` taxonomy, not us-gaap. Until 2026-10-16
3152
+ // every candidate was fetched under /us-gaap/, so the dei tag listed below for
3153
+ // "shares outstanding" 404'd on every filer and
3154
+ // edgar_company_concept({concept:"EntityCommonStockSharesOutstanding"}) told
3155
+ // callers "this filer reports no XBRL tag matching ..." for Tesla, which has 67
3156
+ // such facts (latest 3,949,547,394 at 2026-07-16). A report built on that
3157
+ // answer took the share count from a stale 10-Q cover instead and was off 5%
3158
+ // on every per-share number. Resolve the namespace per TAG, not per lookup.
3159
+ const DEI_TAGS = new Set(['EntityCommonStockSharesOutstanding', 'EntityPublicFloat']);
3160
+ const namespaceForTag = (defaultNs: string, tag: string): string => (DEI_TAGS.has(tag) ? 'dei' : defaultNs);
3161
+
3131
3162
  const CONCEPT_CANDIDATES: Record<string, string[]> = {
3132
3163
  // ── friendly names ────────────────────────────────────────────────
3133
3164
  revenue: ['RevenueFromContractWithCustomerExcludingAssessedTax', 'Revenues', 'SalesRevenueNet'],
@@ -3145,6 +3176,31 @@ const CONCEPT_CANDIDATES: Record<string, string[]> = {
3145
3176
  sharesoutstanding: ['CommonStockSharesOutstanding', 'EntityCommonStockSharesOutstanding', 'CommonStockSharesIssued'],
3146
3177
  commonsharesoutstanding: ['CommonStockSharesOutstanding', 'EntityCommonStockSharesOutstanding', 'CommonStockSharesIssued'],
3147
3178
  commonstocksharesoutstanding: ['CommonStockSharesOutstanding', 'EntityCommonStockSharesOutstanding'],
3179
+ // The cash-flow and expense lines an equity model needs. Measured 2026-10-16:
3180
+ // a routed "Tesla capex Q2 2026" question reached this tool as
3181
+ // concept:"CapitalExpenditures", which no filer tags, and the answer was
3182
+ // "reports no matching XBRL tag" — a miss on a figure every 10-Q carries as
3183
+ // PaymentsToAcquirePropertyPlantAndEquipment. The report that asked went to a
3184
+ // third-party site for it instead.
3185
+ capex: ['PaymentsToAcquirePropertyPlantAndEquipment', 'PaymentsToAcquireProductiveAssets', 'PaymentsForCapitalImprovements'],
3186
+ capitalexpenditure: ['PaymentsToAcquirePropertyPlantAndEquipment', 'PaymentsToAcquireProductiveAssets', 'PaymentsForCapitalImprovements'],
3187
+ capitalexpenditures: ['PaymentsToAcquirePropertyPlantAndEquipment', 'PaymentsToAcquireProductiveAssets', 'PaymentsForCapitalImprovements'],
3188
+ operatingcashflow: ['NetCashProvidedByUsedInOperatingActivities', 'NetCashProvidedByUsedInOperatingActivitiesContinuingOperations'],
3189
+ cashfromoperations: ['NetCashProvidedByUsedInOperatingActivities', 'NetCashProvidedByUsedInOperatingActivitiesContinuingOperations'],
3190
+ netcashfromoperatingactivities: ['NetCashProvidedByUsedInOperatingActivities', 'NetCashProvidedByUsedInOperatingActivitiesContinuingOperations'],
3191
+ netcashprovidedbyoperatingactivities: ['NetCashProvidedByUsedInOperatingActivities', 'NetCashProvidedByUsedInOperatingActivitiesContinuingOperations'],
3192
+ researchanddevelopment: ['ResearchAndDevelopmentExpense', 'ResearchAndDevelopmentExpenseExcludingAcquiredInProcessCost'],
3193
+ researchanddevelopmentexpense: ['ResearchAndDevelopmentExpense', 'ResearchAndDevelopmentExpenseExcludingAcquiredInProcessCost'],
3194
+ sga: ['SellingGeneralAndAdministrativeExpense'],
3195
+ sellinggeneralandadministrative: ['SellingGeneralAndAdministrativeExpense'],
3196
+ costofrevenue: ['CostOfRevenue', 'CostOfGoodsAndServicesSold', 'CostOfGoodsSold'],
3197
+ costofgoodssold: ['CostOfGoodsAndServicesSold', 'CostOfGoodsSold', 'CostOfRevenue'],
3198
+ cogs: ['CostOfGoodsAndServicesSold', 'CostOfGoodsSold', 'CostOfRevenue'],
3199
+ dilutedshares: ['WeightedAverageNumberOfDilutedSharesOutstanding'],
3200
+ weightedaveragedilutedshares: ['WeightedAverageNumberOfDilutedSharesOutstanding'],
3201
+ basicshares: ['WeightedAverageNumberOfSharesOutstandingBasic'],
3202
+ weightedaveragebasicshares: ['WeightedAverageNumberOfSharesOutstandingBasic'],
3203
+ publicfloat: ['EntityPublicFloat'],
3148
3204
  cash: [
3149
3205
  'CashAndCashEquivalentsAtCarryingValue',
3150
3206
  'Cash',
@@ -3399,7 +3455,7 @@ async function companyConcept(
3399
3455
  const tryNamespace = async (ns: string, tags: string[]) => {
3400
3456
  for (const candidate of tags) {
3401
3457
  const r = await pwFetch(
3402
- `${DATA_BASE}/api/xbrl/companyconcept/CIK${paddedCik}/${ns}/${encodeURIComponent(candidate)}.json`,
3458
+ `${DATA_BASE}/api/xbrl/companyconcept/CIK${paddedCik}/${namespaceForTag(ns, candidate)}/${encodeURIComponent(candidate)}.json`,
3403
3459
  { headers: SEC_HEADERS },
3404
3460
  );
3405
3461
  if (!r.ok) { lastStatus = r.status; continue; }
@@ -3438,12 +3494,18 @@ async function companyConcept(
3438
3494
  if (!factsRes.ok) {
3439
3495
  lastStatus = factsRes.status;
3440
3496
  } else if (tokens.length) {
3441
- const facts = (await factsRes.json()) as { entityName?: string; facts?: { 'us-gaap'?: Record<string, { label?: string; description?: string; units: Record<string, ConceptDoc['units'][string]> }> } };
3497
+ type FactsNs = Record<string, { label?: string; description?: string; units: Record<string, ConceptDoc['units'][string]> }>;
3498
+ const facts = (await factsRes.json()) as { entityName?: string; facts?: { 'us-gaap'?: FactsNs; dei?: FactsNs } };
3442
3499
  resolvedEntityName = facts.entityName ?? '';
3443
- const gaap = facts.facts?.['us-gaap'] ?? {};
3500
+ // Scan dei too: cover-page facts (shares outstanding, public float) are
3501
+ // reported there and nowhere in us-gaap for many filers.
3502
+ const scanNamespaces: [string, FactsNs][] = [
3503
+ ['us-gaap', facts.facts?.['us-gaap'] ?? {}],
3504
+ ['dei', facts.facts?.dei ?? {}],
3505
+ ];
3444
3506
  const FLOW = /^(ProceedsFrom|RepaymentsOf|PaymentsOf|IncreaseDecrease|AmortizationOf|Gain|Loss)/;
3445
3507
  const matches: { isFlow: boolean; latestEnd: string; doc: ConceptDoc }[] = [];
3446
- for (const [tag, entry] of Object.entries(gaap)) {
3508
+ for (const [scanNs, nsFacts] of scanNamespaces) for (const [tag, entry] of Object.entries(nsFacts)) {
3447
3509
  if (!tokens.some((t) => norm(tag).includes(t))) continue;
3448
3510
  let latestEnd = '';
3449
3511
  for (const [, values] of unitEntries(entry.units)) {
@@ -3455,7 +3517,7 @@ async function companyConcept(
3455
3517
  matches.push({
3456
3518
  isFlow: FLOW.test(tag),
3457
3519
  latestEnd,
3458
- doc: { cik: Number(cik), entityName: facts.entityName ?? '', tag, taxonomy: 'us-gaap', label: entry.label ?? tag, description: entry.description ?? '', units: entry.units },
3520
+ doc: { cik: Number(cik), entityName: facts.entityName ?? '', tag, taxonomy: scanNs, label: entry.label ?? tag, description: entry.description ?? '', units: entry.units },
3459
3521
  });
3460
3522
  }
3461
3523
  matches.sort((a, b) => Number(a.isFlow) - Number(b.isFlow) || (b.latestEnd ?? '').localeCompare(a.latestEnd ?? ''));
@@ -3536,7 +3598,7 @@ async function companyConcept(
3536
3598
  reported: false,
3537
3599
  latest: null,
3538
3600
  values: [],
3539
- note: `This filer reports no XBRL tag matching "${concept}" under any recognized name — checked us-gaap candidates ${JSON.stringify(candidates)}, IFRS, and a full company-facts name scan. This means EITHER the company genuinely has none (common for e.g. long-term debt at equity-funded filers) OR it reports the item under a tag we didn't match (e.g. convertible/other debt lines). value is null, not zero — call edgar_company_facts({cik:"${cikOrTicker}"}) to see the exact tags this filer uses before treating it as absent.`,
3601
+ note: `This filer reports no XBRL tag matching "${concept}" under any recognized name — checked us-gaap/dei candidates ${JSON.stringify(candidates)}, IFRS, and a full company-facts name scan (us-gaap and dei). This means EITHER the company genuinely has none (common for e.g. long-term debt at equity-funded filers) OR it reports the item under a tag we didn't match (e.g. convertible/other debt lines). value is null, not zero — call edgar_company_facts({cik:"${cikOrTicker}"}) to see the exact tags this filer uses before treating it as absent.`,
3540
3602
  };
3541
3603
  }
3542
3604
  found.sort((a, b) => (b.latestEnd ?? '').localeCompare(a.latestEnd ?? ''));
@@ -3591,6 +3653,17 @@ async function companyConcept(
3591
3653
  value: entry.val,
3592
3654
  form: entry.form, // 10-K / 10-Q / 20-F / 40-F (+ /A amendments)
3593
3655
  filed: entry.filed,
3656
+ // Which filing this exact number came out of (fleet #3178). SEC's
3657
+ // companyconcept payload carries `accn` on every fact and we were
3658
+ // dropping it, so a caller holding a value and a period_end had no way
3659
+ // back to the document without a second edgar_company_filings call and
3660
+ // a date match — and a consumer that FANS OUT over this tool
3661
+ // (entity_profile's fundamentals) could not cite its own figures at all.
3662
+ // Reported rows only: a derived Q4/standalone-quarter row below is
3663
+ // arithmetic over several filings, not a fact any one of them states, so
3664
+ // it carries `accession: undefined` rather than the accession of
3665
+ // whichever leg happened to be the minuend.
3666
+ accession: entry.accn as string | undefined,
3594
3667
  unit,
3595
3668
  derived: false as boolean | undefined,
3596
3669
  note: undefined as string | undefined,
@@ -3640,6 +3713,7 @@ async function companyConcept(
3640
3713
  value: fyRow.value - qSum,
3641
3714
  form: 'derived',
3642
3715
  filed: fyRow.filed,
3716
+ accession: undefined, // arithmetic over FY + Q1-Q3, not a fact one filing states
3643
3717
  unit: fyRow.unit,
3644
3718
  derived: true,
3645
3719
  note: 'Computed as FY minus Q1-Q3 — SEC filers do not report a standalone Q4 fact.',
@@ -3704,6 +3778,7 @@ async function companyConcept(
3704
3778
  value: q2ytd.value - q1.value,
3705
3779
  form: 'derived',
3706
3780
  filed: q2ytd.filed,
3781
+ accession: undefined, // YTD(Q2) minus Q1 — spans two filings
3707
3782
  unit: q2ytd.unit,
3708
3783
  derived: true,
3709
3784
  note: `Computed as the YTD figure filed for Q2 (${q2ytd.value}, covering ${q2ytd.period_start}..${q2ytd.period_end}) minus Q1 (${q1.value}) — this filer reports this concept cumulatively in its 10-Q (a cash-flow-statement concept, typically), not quarter-only.`,
@@ -3723,6 +3798,7 @@ async function companyConcept(
3723
3798
  value: q3ytd.value - q2ytd.value,
3724
3799
  form: 'derived',
3725
3800
  filed: q3ytd.filed,
3801
+ accession: undefined, // YTD(Q3) minus YTD(Q2) — spans two filings
3726
3802
  unit: q3ytd.unit,
3727
3803
  derived: true,
3728
3804
  note: `Computed as the YTD figure filed for Q3 (${q3ytd.value}) minus the YTD figure filed for Q2 (${q2ytd.value}) — this filer reports this concept cumulatively in its 10-Q, not quarter-only.`,
@@ -3823,8 +3899,18 @@ async function companyConcept(
3823
3899
  // neither the caller nor any automatic check can tell it apart". A
3824
3900
  // substituted tag is the same shape, and gets the same treatment.
3825
3901
  const requested = concept.trim();
3826
- const substituted = requested.length > 0
3827
- && data.tag.toLowerCase().replace(/[^a-z0-9]/g, '') !== requested.toLowerCase().replace(/[^a-z0-9]/g, '');
3902
+ // A friendly name the tool itself documents ("shares outstanding", "revenue")
3903
+ // resolving to one of ITS OWN listed tags is the advertised behaviour, not a
3904
+ // substitution — the warning below is for the fuzzy name-scan path, where a
3905
+ // narrower tag really can stand in for a broader concept. Until 2026-10-16
3906
+ // "shares outstanding" → CommonStockSharesOutstanding carried the full "NOT
3907
+ // interchangeable" warning on a correct answer.
3908
+ const requestedKey = requested.toLowerCase().replace(/[^a-z0-9]/g, '');
3909
+ const aliasServed = (CONCEPT_CANDIDATES[requestedKey] ?? []).some(
3910
+ (t) => t.toLowerCase() === data.tag.toLowerCase(),
3911
+ );
3912
+ const substituted = requested.length > 0 && !aliasServed
3913
+ && data.tag.toLowerCase().replace(/[^a-z0-9]/g, '') !== requestedKey;
3828
3914
 
3829
3915
  // Coverage-bound disclosure (fleet #693). A returned value can be genuinely
3830
3916
  // CURRENT for this exact XBRL tag while a newer periodic filing exists that
package/src/server.ts CHANGED
@@ -10,7 +10,7 @@ import pack from './index.js';
10
10
 
11
11
 
12
12
  const server = new Server(
13
- { name: '@pipeworx/mcp-edgar', version: '0.3.9' },
13
+ { name: '@pipeworx/mcp-edgar', version: '0.3.11' },
14
14
  { capabilities: { tools: {} } },
15
15
  );
16
16