@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 +2 -2
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +180 -0
- package/src/server.ts +1 -1
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
|
|
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
|
|
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.
|
|
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-
|
|
30
|
-
"sourceCommit": "
|
|
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.
|
|
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.
|
|
12
|
+
{ name: '@pipeworx/mcp-edgar', version: '0.3.8' },
|
|
13
13
|
{ capabilities: { tools: {} } },
|
|
14
14
|
);
|
|
15
15
|
|