@pipeworx/mcp-edgar 0.3.7 → 0.3.8

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 1684+ live data sources.
5
+ Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1763+ 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 1684+ data sources. The
109
+ Both URLs reach the same gateway and the same 1763+ 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.7",
3
+ "version": "0.3.8",
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-32b9918a5b5098bb4af39453590b40c58883a8cca190d5ec3630203cf8236594",
30
- "sourceCommit": "7ab72cdcaf29d0be5874bd2191074e69f97c9b0b"
29
+ "sourceHash": "v1-9871b5732265c259f8fc2a821a7721a32960cabdc9041cc1d5ea4a1e0b8d3f37",
30
+ "sourceCommit": "051ee2039fc96224cb5b7358fd69d525bced13a6"
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.7",
6
+ "version": "0.3.8",
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
@@ -3106,6 +3106,83 @@ const IFRS_FALLBACK: Record<string, string[]> = {
3106
3106
  equity: ['Equity', 'EquityAttributableToOwnersOfParent'],
3107
3107
  };
3108
3108
 
3109
+ // --- Liquidity companion tags (fleet #2879 / feedback #137) ----------------
3110
+ // "Cash" alone (CashAndCashEquivalentsAtCarryingValue, or its substitutes)
3111
+ // excludes marketable/short-term securities, which biotechs and other
3112
+ // cash-burning companies routinely park a large share of their runway in.
3113
+ // Verified live for Capricor (CIK 1133869, accession 0001104659-26-097294,
3114
+ // period 2026-06-30): Cash alone is $20,935,187; adding
3115
+ // AvailableForSaleSecuritiesDebtSecuritiesCurrent ($216,999,595) gets to
3116
+ // $237,934,782 — the $237.9M the reporter expected, an 11x difference from
3117
+ // the cash-only figure. Agents asking "Cash" are almost always asking a
3118
+ // runway/liquidity question, so when the resolved tag is cash-shaped, also
3119
+ // probe for marketable-securities tags at the SAME period_end and attach a
3120
+ // derived total_liquidity. Order matters: try the tag this filer is most
3121
+ // likely to use first (fewer wasted fetches); first one with any data wins.
3122
+ const CASH_LIKE_TAGS = new Set([
3123
+ 'CashAndCashEquivalentsAtCarryingValue', 'Cash',
3124
+ 'CashCashEquivalentsRestrictedCashAndRestrictedCashEquivalents',
3125
+ ]);
3126
+ const MARKETABLE_SECURITIES_CURRENT_TAGS = [
3127
+ 'AvailableForSaleSecuritiesDebtSecuritiesCurrent', 'MarketableSecuritiesCurrent', 'ShortTermInvestments',
3128
+ ];
3129
+ const MARKETABLE_SECURITIES_NONCURRENT_TAGS = [
3130
+ 'AvailableForSaleSecuritiesDebtSecuritiesNoncurrent', 'MarketableSecuritiesNoncurrent', 'LongTermInvestments',
3131
+ ];
3132
+
3133
+ interface LiquidityBreakdown {
3134
+ value: number;
3135
+ derived: true;
3136
+ components: Record<string, number>;
3137
+ note: string;
3138
+ }
3139
+
3140
+ /** Full time series for one us-gaap concept, keyed by period end date (the
3141
+ * freshest-filed fact per end date wins on duplicates). Null when the filer
3142
+ * has no data under this exact tag (a 404 or an empty series) — callers try
3143
+ * the next candidate tag rather than treating that as "no securities". */
3144
+ async function fetchConceptSeries(
3145
+ paddedCik: string,
3146
+ tag: string,
3147
+ ): Promise<Map<string, { value: number; form: string; filed: string }> | null> {
3148
+ const r = await pwFetch(
3149
+ `${DATA_BASE}/api/xbrl/companyconcept/CIK${paddedCik}/us-gaap/${encodeURIComponent(tag)}.json`,
3150
+ { headers: SEC_HEADERS },
3151
+ ).catch(() => null);
3152
+ if (!r || !r.ok) return null;
3153
+ const doc = (await r.json()) as {
3154
+ units?: Record<string, { end?: string; val: number; form: string; filed: string }[]>;
3155
+ };
3156
+ const map = new Map<string, { value: number; form: string; filed: string }>();
3157
+ for (const values of Object.values(doc.units ?? {})) {
3158
+ if (!Array.isArray(values)) continue;
3159
+ for (const e of values) {
3160
+ const key = e.end ?? '';
3161
+ if (!key) continue;
3162
+ const prior = map.get(key);
3163
+ if (!prior || (e.filed ?? '') > prior.filed) map.set(key, { value: e.val, form: e.form, filed: e.filed });
3164
+ }
3165
+ }
3166
+ return map.size ? map : null;
3167
+ }
3168
+
3169
+ /** Try each candidate tag in order; return the first with any data. A filer
3170
+ * uses exactly one of these at a time but SWITCHES over its history (Capricor
3171
+ * reported MarketableSecuritiesCurrent through FY2025Q3, then switched to
3172
+ * AvailableForSaleSecuritiesDebtSecuritiesCurrent from FY2025Q4 on — verified
3173
+ * live), so trying only the newest-looking tag would miss older periods and
3174
+ * trying only one fixed tag would miss filers who never used it. */
3175
+ async function firstAvailableSeries(
3176
+ paddedCik: string,
3177
+ tags: string[],
3178
+ ): Promise<{ tag: string; series: Map<string, { value: number; form: string; filed: string }> } | null> {
3179
+ for (const tag of tags) {
3180
+ const series = await fetchConceptSeries(paddedCik, tag);
3181
+ if (series) return { tag, series };
3182
+ }
3183
+ return null;
3184
+ }
3185
+
3109
3186
  async function companyConcept(
3110
3187
  cikOrTicker: string,
3111
3188
  concept: string,
@@ -3380,6 +3457,7 @@ async function companyConcept(
3380
3457
  unit,
3381
3458
  derived: false as boolean | undefined,
3382
3459
  note: undefined as string | undefined,
3460
+ total_liquidity: undefined as LiquidityBreakdown | undefined,
3383
3461
  }))
3384
3462
  .sort((a, b) => (b.period_end ?? '').localeCompare(a.period_end ?? ''));
3385
3463
 
@@ -3428,6 +3506,7 @@ async function companyConcept(
3428
3506
  unit: fyRow.unit,
3429
3507
  derived: true,
3430
3508
  note: 'Computed as FY minus Q1-Q3 — SEC filers do not report a standalone Q4 fact.',
3509
+ total_liquidity: undefined,
3431
3510
  });
3432
3511
  }
3433
3512
  if (derivedQ4.length > 0) {
@@ -3447,6 +3526,78 @@ async function companyConcept(
3447
3526
  entries.sort((a, b) => (b.period_end ?? '').localeCompare(a.period_end ?? ''));
3448
3527
  }
3449
3528
 
3529
+ // Derive STANDALONE quarters from YEAR-TO-DATE duration data (fleet #2879 /
3530
+ // feedback #137, item 3). Income-statement concepts (Revenue, NetIncomeLoss)
3531
+ // are usually filed quarter-only in a 10-Q and the dedup above already
3532
+ // prefers that discrete fact over a same-day YTD one. CASH-FLOW-STATEMENT
3533
+ // concepts (NetCashProvidedByUsedInOperatingActivities and siblings) have no
3534
+ // discrete-quarter fact at all — US GAAP presents the cash-flow statement
3535
+ // cumulatively, so a 10-Q's "Q2" figure IS six months, not two. Taking that
3536
+ // as a quarterly burn rate overstates it ~2x in Q2 and ~3x in Q3. Verified
3537
+ // live for Capricor (CIK 1133869) NetCashProvidedByUsedInOperatingActivities,
3538
+ // FY2026: Q1 (2026-01-01..03-31) = -$29,254,891; "Q2" (2026-01-01..06-30,
3539
+ // span 181 days — not the 80-100d a real quarter spans) = -$60,740,078. The
3540
+ // real Q2 burn is -$31,485,187 (Q2 YTD minus Q1), roughly double what the
3541
+ // filed Q2 figure alone would suggest if read as quarterly.
3542
+ //
3543
+ // Detected structurally (by span, not a hardcoded tag list, so it covers any
3544
+ // concept a filer reports this way): anchor on a genuinely quarter-length Q1
3545
+ // (span 80-100d), then look for a same-fiscal-year-start Q2/Q3 row whose span
3546
+ // is ~2x/~3x a quarter (170-190d / 260-290d) — i.e. YTD-shaped. The ORIGINAL
3547
+ // YTD-labeled row is kept as-filed; the derived standalone figure is added
3548
+ // alongside it with form:"derived", so a caller filtering on fiscal_period
3549
+ // sees both the as-filed YTD number and the true quarterly one, and nothing
3550
+ // is silently replaced.
3551
+ const standaloneQuarters: typeof entries = [];
3552
+ for (const q1 of entries) {
3553
+ if (q1.fiscal_period !== 'Q1' || q1.unit.includes('/')) continue;
3554
+ const q1Span = spanDays(q1.period_start, q1.period_end);
3555
+ if (!(q1Span >= 80 && q1Span <= 100)) continue; // Q1 YTD === Q1 quarter; this anchors the fiscal year's start date
3556
+ const q2ytd = entries.find((e) => {
3557
+ if (e.fiscal_period !== 'Q2' || e.unit !== q1.unit || e.period_start !== q1.period_start) return false;
3558
+ const s = spanDays(e.period_start, e.period_end);
3559
+ return s >= 170 && s <= 190;
3560
+ });
3561
+ if (!q2ytd) continue;
3562
+ standaloneQuarters.push({
3563
+ fiscal_year: q2ytd.fiscal_year,
3564
+ fiscal_period: 'Q2',
3565
+ period_start: q1.period_end,
3566
+ period_end: q2ytd.period_end,
3567
+ value: q2ytd.value - q1.value,
3568
+ form: 'derived',
3569
+ filed: q2ytd.filed,
3570
+ unit: q2ytd.unit,
3571
+ derived: true,
3572
+ 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.`,
3573
+ total_liquidity: undefined,
3574
+ });
3575
+ const q3ytd = entries.find((e) => {
3576
+ if (e.fiscal_period !== 'Q3' || e.unit !== q1.unit || e.period_start !== q1.period_start) return false;
3577
+ const s = spanDays(e.period_start, e.period_end);
3578
+ return s >= 260 && s <= 290;
3579
+ });
3580
+ if (q3ytd) {
3581
+ standaloneQuarters.push({
3582
+ fiscal_year: q3ytd.fiscal_year,
3583
+ fiscal_period: 'Q3',
3584
+ period_start: q2ytd.period_end,
3585
+ period_end: q3ytd.period_end,
3586
+ value: q3ytd.value - q2ytd.value,
3587
+ form: 'derived',
3588
+ filed: q3ytd.filed,
3589
+ unit: q3ytd.unit,
3590
+ derived: true,
3591
+ 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.`,
3592
+ total_liquidity: undefined,
3593
+ });
3594
+ }
3595
+ }
3596
+ if (standaloneQuarters.length > 0) {
3597
+ entries.push(...standaloneQuarters);
3598
+ entries.sort((a, b) => (b.period_end ?? '').localeCompare(a.period_end ?? ''));
3599
+ }
3600
+
3450
3601
  // PERIOD FILTER (fleet #2058). `fiscal_year` and `fiscal_period` are the
3451
3602
  // names of two fields on every row below, and callers asking a one-period
3452
3603
  // question ("net income fiscal 2024") send them as ARGUMENTS — measured on
@@ -3556,6 +3707,35 @@ async function companyConcept(
3556
3707
  // must be the freshest fact WITHIN the period asked for, or the one field a
3557
3708
  // synthesizer reaches for first would answer a different year than the
3558
3709
  // question (fleet #2058).
3710
+ // LIQUIDITY ENRICHMENT (fleet #2879 / feedback #137, items 1-2). Only when
3711
+ // the resolved tag is cash-shaped — these securities tags don't apply to
3712
+ // Revenue, LongTermDebt, etc. Mutates the row objects in place (not a copy),
3713
+ // so `latestFact` below and `latest`/`values`/`annual_values` in the return
3714
+ // all pick up the same enrichment without threading it through separately.
3715
+ if (CASH_LIKE_TAGS.has(data.tag)) {
3716
+ const [curRes, noncurRes] = await Promise.all([
3717
+ firstAvailableSeries(paddedCik, MARKETABLE_SECURITIES_CURRENT_TAGS),
3718
+ firstAvailableSeries(paddedCik, MARKETABLE_SECURITIES_NONCURRENT_TAGS),
3719
+ ]);
3720
+ if (curRes || noncurRes) {
3721
+ for (const row of rows) {
3722
+ if (!row.period_end) continue;
3723
+ const cur = curRes?.series.get(row.period_end);
3724
+ const noncur = noncurRes?.series.get(row.period_end);
3725
+ if (!cur && !noncur) continue; // no companion data for this exact period-end — don't imply $0 securities
3726
+ const components: Record<string, number> = { [`cash (${data.tag})`]: row.value };
3727
+ if (cur) components[`marketable_securities_current (${curRes!.tag})`] = cur.value;
3728
+ if (noncur) components[`marketable_securities_noncurrent (${noncurRes!.tag})`] = noncur.value;
3729
+ row.total_liquidity = {
3730
+ value: Object.values(components).reduce((s, v) => s + v, 0),
3731
+ derived: true,
3732
+ components,
3733
+ note: 'Derived: cash plus marketable/short-term securities reported for the same period_end. Not itself an SEC-filed figure — see components for the tags and values it was summed from.',
3734
+ };
3735
+ }
3736
+ }
3737
+ }
3738
+
3559
3739
  const latestFact = rows.find((e) => !e.derived) ?? null;
3560
3740
  let coverageBound: string | undefined;
3561
3741
  if (latestFact?.filed) {
package/src/server.ts CHANGED
@@ -9,7 +9,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
9
9
  import pack from './index.js';
10
10
 
11
11
  const server = new Server(
12
- { name: '@pipeworx/mcp-edgar', version: '0.3.7' },
12
+ { name: '@pipeworx/mcp-edgar', version: '0.3.8' },
13
13
  { capabilities: { tools: {} } },
14
14
  );
15
15