@pipeworx/mcp-edgar 0.3.14 → 0.3.16
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 +81 -13
- 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 1881+ 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
|
|
|
@@ -111,7 +111,7 @@ directly, instead of just this one's:
|
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
Both URLs reach the same gateway and the same
|
|
114
|
+
Both URLs reach the same gateway and the same 1881+ data sources. The
|
|
115
115
|
only difference is which pack's tools are listed **directly**; `ask_pipeworx`
|
|
116
116
|
reaches all of them from either one.
|
|
117
117
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipeworx/mcp-edgar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.16",
|
|
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-7833dc290ad97eca84fde46549d82a1502b8e5f9f99ce63b465d3fe526f25542",
|
|
30
|
+
"sourceCommit": "24e32b525f9c0b2fd7d94b519b08abb108cc11cd"
|
|
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.16",
|
|
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
|
@@ -1065,25 +1065,64 @@ function rankMatches<T>(
|
|
|
1065
1065
|
query: string,
|
|
1066
1066
|
items: T[],
|
|
1067
1067
|
getName: (item: T) => string,
|
|
1068
|
+
getAliases?: (item: T) => string[],
|
|
1068
1069
|
): RankedMatch<T>[] {
|
|
1069
1070
|
const q = normalize(query);
|
|
1070
1071
|
if (!q) return [];
|
|
1071
1072
|
const scored: { item: T; kind: MatchKind; score: number; len: number }[] = [];
|
|
1072
1073
|
for (const item of items) {
|
|
1073
1074
|
const name = getName(item);
|
|
1074
|
-
|
|
1075
|
+
// `getAliases` (optional, fleet #3607) are OTHER spellings of the same name,
|
|
1076
|
+
// each scored on its own; the best-scoring spelling wins. Additive on
|
|
1077
|
+
// purpose: the raw name is always tried, so a query that already matched
|
|
1078
|
+
// keeps matching exactly as before, and an alias can only ADD a match.
|
|
1075
1079
|
let kind: MatchKind | null = null;
|
|
1076
1080
|
let score = 0;
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
+
for (const spelling of [name, ...(getAliases?.(item) ?? [])]) {
|
|
1082
|
+
const n = normalize(spelling);
|
|
1083
|
+
let k: MatchKind | null = null;
|
|
1084
|
+
let sc = 0;
|
|
1085
|
+
if (n === q) { k = 'exact'; sc = 4; }
|
|
1086
|
+
else if (n.startsWith(`${q} `)) { k = 'prefix'; sc = 3; }
|
|
1087
|
+
else if (n.includes(` ${q} `) || n.endsWith(` ${q}`)) { k = 'word'; sc = 2; }
|
|
1088
|
+
else if (n.includes(q)) { k = 'substring'; sc = 1; }
|
|
1089
|
+
if (k && sc > score) { kind = k; score = sc; }
|
|
1090
|
+
}
|
|
1081
1091
|
if (kind) scored.push({ item, kind, score, len: name.length });
|
|
1082
1092
|
}
|
|
1083
1093
|
scored.sort((a, b) => b.score - a.score || a.len - b.len);
|
|
1084
1094
|
return scored.map(({ item, kind, score }) => ({ item, kind, score }));
|
|
1085
1095
|
}
|
|
1086
1096
|
|
|
1097
|
+
/**
|
|
1098
|
+
* The trailing designators EDGAR appends to a registrant name that are NOT part
|
|
1099
|
+
* of the company's name: state or country of incorporation ("/DE/", "/MD",
|
|
1100
|
+
* "/CAN/", "/Cayman"), "/NEW", and the ADR marker. Matched case-insensitively
|
|
1101
|
+
* against the WHOLE segment after the last slash, so a name with a slash INSIDE
|
|
1102
|
+
* it ("CULLEN/FROST BANKERS", "M/I HOMES", "Quad/Graphics", "DATA I/O CORP",
|
|
1103
|
+
* "GENMAB A/S") is never cut. A genuine legal form that happens to look like a
|
|
1104
|
+
* 2-letter designator ("Anheuser-Busch InBev SA/NV") gains an extra alias
|
|
1105
|
+
* ("...SA"); the raw title is always tried too, so that costs nothing. Measured
|
|
1106
|
+
* against SEC's company_tickers.json 2026-10-26: 951 of 10,429 titles carry a
|
|
1107
|
+
* slash; the trailing segments are ADR (545), CAN, state codes, NEW, CAYMAN,
|
|
1108
|
+
* DEL(AWARE), CANADA, JAPAN, or empty ("TOYOTA MOTOR CORP/").
|
|
1109
|
+
*/
|
|
1110
|
+
const EDGAR_TRAILING_DESIGNATOR =
|
|
1111
|
+
/\s*\/\s*(?:[A-Za-z]{2}|ADR|CAN|NEW|CAYMAN|DEL|DELAWARE|CANADA|JAPAN|UK)?\s*\/?\s*$/i;
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Other spellings of an EDGAR registrant title that name the same company:
|
|
1115
|
+
* the title with its jurisdiction/ADR designator removed ("TOYOTA MOTOR CORP/"
|
|
1116
|
+
* -> "TOYOTA MOTOR CORP"). Empty when there is nothing to remove. Used as
|
|
1117
|
+
* `rankMatches`'s aliases, never as a replacement for the title (fleet #3607).
|
|
1118
|
+
* Deliberately NOT a change to `normalize`: that is shared by every pack that
|
|
1119
|
+
* ranks its own list, and "/" is not a registrant-name artefact in those.
|
|
1120
|
+
*/
|
|
1121
|
+
function edgarNameAliases(title: string): string[] {
|
|
1122
|
+
const stripped = title.replace(EDGAR_TRAILING_DESIGNATOR, '').trim();
|
|
1123
|
+
return stripped && stripped !== title.trim() && /\S/.test(stripped) ? [stripped] : [];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1087
1126
|
interface SecTickerRow$shared { cik_str: number; ticker: string; title: string }
|
|
1088
1127
|
|
|
1089
1128
|
interface SecEntity {
|
|
@@ -1100,6 +1139,13 @@ interface SecEntity {
|
|
|
1100
1139
|
* `matched_by: 'company_name'` alone cannot (fleet #3100).
|
|
1101
1140
|
*/
|
|
1102
1141
|
matched_kind?: MatchKind;
|
|
1142
|
+
/**
|
|
1143
|
+
* Other companies whose names also matched — distinct companies, deduped
|
|
1144
|
+
* by CIK, excluding this issuer's own share classes (never the winner's
|
|
1145
|
+
* own ticker/CIK under a different symbol). Consumers read this to tell a
|
|
1146
|
+
* genuinely CONTESTED name match from a clean one. See
|
|
1147
|
+
* `distinctCompanyAlternatives` below for how it's built (fleet #3621).
|
|
1148
|
+
*/
|
|
1103
1149
|
alternatives?: { ticker: string; company_name: string; cik: string; kind: MatchKind }[];
|
|
1104
1150
|
}
|
|
1105
1151
|
|
|
@@ -1133,16 +1179,11 @@ async function resolveSecEntity(
|
|
|
1133
1179
|
}
|
|
1134
1180
|
|
|
1135
1181
|
// 2) Company-name fallback.
|
|
1136
|
-
const ranked = rankMatches(query, rows, (r) => r.title);
|
|
1182
|
+
const ranked = rankMatches(query, rows, (r) => r.title, (r) => edgarNameAliases(r.title));
|
|
1137
1183
|
if (ranked.length) {
|
|
1138
1184
|
const best = toEntity(ranked[0].item, 'company_name');
|
|
1139
1185
|
best.matched_kind = ranked[0].kind;
|
|
1140
|
-
const alts = ranked
|
|
1141
|
-
ticker: m.item.ticker,
|
|
1142
|
-
company_name: m.item.title,
|
|
1143
|
-
cik: String(m.item.cik_str),
|
|
1144
|
-
kind: m.kind,
|
|
1145
|
-
}));
|
|
1186
|
+
const alts = distinctCompanyAlternatives(ranked);
|
|
1146
1187
|
if (alts.length) best.alternatives = alts;
|
|
1147
1188
|
return best;
|
|
1148
1189
|
}
|
|
@@ -1150,6 +1191,33 @@ async function resolveSecEntity(
|
|
|
1150
1191
|
throw new Error(`No SEC company matches "${query}". Pass a US-listed ticker ("AAPL") or the exact listed-company name ("Apple Inc."). If this is a clinical-trial sponsor, an operating subsidiary (e.g. "Merck Sharp & Dohme" → Merck & Co), or a foreign/private entity, call sponsor_to_filer({sponsor}) instead — it resolves subsidiaries to the listed parent and honestly reports when no US-listed filer exists.`);
|
|
1151
1192
|
}
|
|
1152
1193
|
|
|
1194
|
+
/** `alternatives` lists at most this many OTHER companies; company-spine.test.ts pins it to the real function. */
|
|
1195
|
+
const ALTERNATIVES_CAP = 3;
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* The runners-up for a name match, as DISTINCT COMPANIES (fleet #3621).
|
|
1199
|
+
*
|
|
1200
|
+
* company_tickers.json lists one row per ticker, so an issuer with preferred
|
|
1201
|
+
* shares appears several times under one CIK (Ford: F, F-PB, F-PC, F-PD; AT&T:
|
|
1202
|
+
* T, TBB, T-PA, T-PC). Slicing the ranked rows left those rows to fill every
|
|
1203
|
+
* alternatives slot, hiding any real rival ranked behind them, and made a company
|
|
1204
|
+
* its own "alternative". So: drop every row that is the winner's own CIK, keep
|
|
1205
|
+
* only the best-ranked row of each other CIK, THEN cap. The winner's other
|
|
1206
|
+
* tickers are not lost information: they are the same company, and `ticker` /
|
|
1207
|
+
* `cik` already identify it. `ranked` is already best-first, so first-seen wins.
|
|
1208
|
+
*/
|
|
1209
|
+
function distinctCompanyAlternatives(ranked: RankedMatch<SecTickerRow$shared>[]): NonNullable<SecEntity['alternatives']> {
|
|
1210
|
+
const seen = new Set<number>([ranked[0].item.cik_str]);
|
|
1211
|
+
const out: NonNullable<SecEntity['alternatives']> = [];
|
|
1212
|
+
for (const m of ranked.slice(1)) {
|
|
1213
|
+
if (seen.has(m.item.cik_str)) continue;
|
|
1214
|
+
seen.add(m.item.cik_str);
|
|
1215
|
+
out.push({ ticker: m.item.ticker, company_name: m.item.title, cik: String(m.item.cik_str), kind: m.kind });
|
|
1216
|
+
if (out.length >= ALTERNATIVES_CAP) break;
|
|
1217
|
+
}
|
|
1218
|
+
return out;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1153
1221
|
function toEntity(r: SecTickerRow$shared, matched_by: 'ticker' | 'company_name'): SecEntity {
|
|
1154
1222
|
return {
|
|
1155
1223
|
ticker: r.ticker,
|
|
@@ -1797,7 +1865,7 @@ const tools: McpToolExport['tools'] = [
|
|
|
1797
1865
|
{
|
|
1798
1866
|
name: 'edgar_ticker_to_cik',
|
|
1799
1867
|
description:
|
|
1800
|
-
'Resolve a US stock ticker (e.g. "TSLA") OR a company name (e.g. "Tesla", "Apple Inc") to the SEC\'s 10-digit CIK identifier — required by every other SEC tool. Call THIS FIRST when you have a ticker/name and need to use edgar_company_concept, edgar_company_filings, edgar_company_facts, sec_8k_recent, or any other SEC-keyed tool. Returns {cik, cik_padded, company_name, ticker, matched_by}; when matched by name it also returns `alternatives`
|
|
1868
|
+
'Resolve a US stock ticker (e.g. "TSLA") OR a company name (e.g. "Tesla", "Apple Inc") to the SEC\'s 10-digit CIK identifier — required by every other SEC tool. Call THIS FIRST when you have a ticker/name and need to use edgar_company_concept, edgar_company_filings, edgar_company_facts, sec_8k_recent, or any other SEC-keyed tool. Returns {cik, cik_padded, company_name, ticker, matched_by}; when matched by name it also returns `alternatives` — up to 3 OTHER companies whose names also matched, one per distinct CIK, with the winner\'s OWN share classes/tickers excluded (never listed as its own alternative). Use `alternatives` to tell a genuinely CONTESTED name match (a real rival company also matched) from a clean one (nothing else did). Cheap, no rate limit concerns. Most other tools also accept tickers/names directly and call this internally — only use it explicitly when you want the CIK as data. The response carries a `next` hint: the usual NEXT step after resolving is edgar_company_snapshot({ticker_or_cik}), which returns the recent filings list AND the headline XBRL financials in one call — do not chain edgar_company_filings then edgar_company_concept by hand to get that.',
|
|
1801
1869
|
summary: 'The SEC CIK number for a stock ticker or company name, from EDGAR\'s company list.',
|
|
1802
1870
|
inputSchema: {
|
|
1803
1871
|
type: 'object' as const,
|