@pipeworx/mcp-edgar 0.3.8 → 0.3.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.
- package/README.md +2 -2
- package/package.json +3 -3
- package/server.json +1 -1
- package/src/index.ts +201 -25
- package/src/server.ts +5 -2
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 1784+ 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 1784+ 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.9",
|
|
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-d35d4a284237ace3314859c5df49ad1b77a0a9e2e7696d3f0b601f112b20e42a",
|
|
30
|
+
"sourceCommit": "3723e46c6d115bd4a1fd2175e6bf758555e7cb50"
|
|
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.9",
|
|
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
|
@@ -1854,6 +1854,23 @@ function significantTokens(name: string): string[] {
|
|
|
1854
1854
|
return normName(name).split(' ').filter((t) => t.length > 1 && !GENERIC_NAME_TOKENS.has(t));
|
|
1855
1855
|
}
|
|
1856
1856
|
|
|
1857
|
+
// 2026-10-09: shared/src/resolve.ts's rankMatches only strips '.' and ',', so
|
|
1858
|
+
// "BRISTOL-MYERS SQUIBB COMPANY" (hyphen kept, full "COMPANY") never matched
|
|
1859
|
+
// SEC's own listed title "BRISTOL MYERS SQUIBB CO" — resolved via full-text
|
|
1860
|
+
// fallback as no_us_registrant instead of the direct CIK 14272 hit. Do not fix
|
|
1861
|
+
// this in shared (used by other packs); derive a second, edgar-only candidate
|
|
1862
|
+
// query that also hits the hyphen (normName already strips it) and the SEC
|
|
1863
|
+
// house style for a trailing corporate suffix.
|
|
1864
|
+
const SUFFIX_TO_SEC_STYLE: Record<string, string> = {
|
|
1865
|
+
COMPANY: 'CO', CORPORATION: 'CORP', INCORPORATED: 'INC', LIMITED: 'LTD',
|
|
1866
|
+
};
|
|
1867
|
+
function secStyleQuery(name: string): string {
|
|
1868
|
+
const tokens = normName(name).split(' ').filter(Boolean);
|
|
1869
|
+
const last = tokens[tokens.length - 1];
|
|
1870
|
+
if (last && SUFFIX_TO_SEC_STYLE[last]) tokens[tokens.length - 1] = SUFFIX_TO_SEC_STYLE[last];
|
|
1871
|
+
return tokens.join(' ');
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1857
1874
|
function distinctiveToken(name: string): string {
|
|
1858
1875
|
return significantTokens(name)[0] ?? normName(name).split(' ')[0] ?? '';
|
|
1859
1876
|
}
|
|
@@ -1861,8 +1878,14 @@ function distinctiveToken(name: string): string {
|
|
|
1861
1878
|
// SEC company names use "&", not "and". Produce query variants: &-normalized
|
|
1862
1879
|
// full name first, then progressively drop trailing generic words (browse-edgar
|
|
1863
1880
|
// matches on a name prefix and a too-specific full name can return nothing).
|
|
1881
|
+
// 2026-10-13: browse-edgar's `company=` param is a literal prefix match and
|
|
1882
|
+
// does NOT fold punctuation itself -- company=Bristol-Myers%20Squibb returns
|
|
1883
|
+
// 0 candidates while company=Bristol%20Myers%20Squibb returns CIK 14272 (the
|
|
1884
|
+
// filer's own listed title has no hyphen). Fold -/() to spaces before
|
|
1885
|
+
// querying, same characters normName() already strips for the ranking path,
|
|
1886
|
+
// so every query we send is already in SEC's unpunctuated house style.
|
|
1864
1887
|
function secQueryVariants(name: string): string[] {
|
|
1865
|
-
const amp = name.replace(/\band\b/gi, '&').replace(/\s+/g, ' ').trim();
|
|
1888
|
+
const amp = name.replace(/\band\b/gi, '&').replace(/[-/()]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1866
1889
|
const out = [amp];
|
|
1867
1890
|
const words = amp.split(' ');
|
|
1868
1891
|
for (let end = words.length; end > 1; end--) {
|
|
@@ -1880,28 +1903,99 @@ async function fetchCompanyTickers(): Promise<SecTickerRow[]> {
|
|
|
1880
1903
|
return Object.values(data);
|
|
1881
1904
|
}
|
|
1882
1905
|
|
|
1906
|
+
// Legal-form designators. A filer name carrying none of these (and no
|
|
1907
|
+
// descriptor word like PHARMACEUTICA / PRODUCTS / PARTNERS) in a 2-4 word
|
|
1908
|
+
// "Last First M" shape is an individual Form 3/4/5 reporting owner, never a
|
|
1909
|
+
// company's own CIK. #2998: "JANSSEN PHARMACEUTICALS INC" fell back to the
|
|
1910
|
+
// bare "JANSSEN" query, whose browse-edgar list is alphabetical by person
|
|
1911
|
+
// name, and took "Janssen Angela K" (CIK 1664284) as J&J's subsidiary.
|
|
1912
|
+
const LEGAL_FORM_TOKENS = new Set([
|
|
1913
|
+
'INC', 'CORP', 'CO', 'COMPANY', 'CORPORATION', 'INCORPORATED', 'LTD', 'LIMITED',
|
|
1914
|
+
'LLC', 'LP', 'LLP', 'PLC', 'SA', 'AG', 'NV', 'BV', 'GMBH', 'AB', 'AS', 'OY', 'SPA',
|
|
1915
|
+
'SE', 'KK', 'PTE', 'PTY', 'SARL', 'SAS', 'TRUST', 'FUND', 'PARTNERSHIP',
|
|
1916
|
+
]);
|
|
1917
|
+
const COMPANY_WORDS = /\b(PHARMA\w*|THERAP\w*|BIO\w*|PRODUCTS?|PARTNERS|ASSOCIATES|CAPITAL|VENTURES|HOLDINGS?|GROUP|LABORATOR\w*|LABS|SCIENCES?|MEDICAL|HEALTH\w*|INTERNATIONAL|GLOBAL|SERVICES|SYSTEMS|TECHNOLOG\w*|INDUSTRIES|ENTERPRISES|MANAGEMENT|INVESTMENTS?|FOUNDATION|UNIVERSITY|BANK|FINANCIAL|ENERGY|RESEARCH|AMERICA|USA|US)\b/;
|
|
1918
|
+
|
|
1919
|
+
/** Person-shaped filer name ("Janssen Angela K", "JANSSEN MICHAEL J"). */
|
|
1920
|
+
function isPersonShapedName(name: string): boolean {
|
|
1921
|
+
const n = name.toUpperCase().replace(/\./g, '').replace(/[,&/()-]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
1922
|
+
const toks = n.split(' ').filter(Boolean);
|
|
1923
|
+
if (toks.length < 2 || toks.length > 4) return false;
|
|
1924
|
+
if (toks.some((t) => LEGAL_FORM_TOKENS.has(t) || /\d/.test(t))) return false;
|
|
1925
|
+
return !COMPANY_WORDS.test(n);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const STOP_TOKENS = new Set(['THE', 'OF', 'AND']);
|
|
1929
|
+
|
|
1930
|
+
// Two tokens agree when equal, or one is a >=6-char prefix of the other
|
|
1931
|
+
// ("PHARMACEUTICA" ~ "PHARMACEUTICALS").
|
|
1932
|
+
const tokAgree = (a: string, b: string): boolean =>
|
|
1933
|
+
a === b || (Math.min(a.length, b.length) >= 6 && (a.startsWith(b) || b.startsWith(a)));
|
|
1934
|
+
|
|
1935
|
+
/** Similarity of a candidate filer name to the FULL sponsor query, ignoring
|
|
1936
|
+
* legal-form suffixes (INC matching INC says nothing). Significant tokens
|
|
1937
|
+
* weigh 10, descriptor words (PHARMACEUTICALS, BIOTECH...) weigh 1. */
|
|
1938
|
+
function nameSimilarity(query: string, candidate: string): { sig: number; desc: number; score: number } {
|
|
1939
|
+
const keep = (t: string) => t.length > 1 && !LEGAL_FORM_TOKENS.has(t) && !STOP_TOKENS.has(t);
|
|
1940
|
+
const qAll = normName(query).split(' ').filter(keep);
|
|
1941
|
+
const cAll = normName(candidate).split(' ').filter(keep);
|
|
1942
|
+
const sigSet = new Set(significantTokens(query));
|
|
1943
|
+
let sig = 0, desc = 0;
|
|
1944
|
+
for (const q of qAll) {
|
|
1945
|
+
if (!cAll.some((c) => tokAgree(q, c))) continue;
|
|
1946
|
+
if (sigSet.has(q)) sig++; else desc++;
|
|
1947
|
+
}
|
|
1948
|
+
return { sig, desc, score: sig * 10 + desc };
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
const decodeHtml = (s: string): string =>
|
|
1952
|
+
s.replace(/&/g, '&').replace(/'|'/g, "'").replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
1953
|
+
|
|
1954
|
+
/** browse-edgar HTML company search -> [{cik, name}]. The atom output carries
|
|
1955
|
+
* no names (SEC renders them as "ARRAY(0x...)"), so a ranking by name needs
|
|
1956
|
+
* the HTML list; an exact single hit renders the company page instead. */
|
|
1957
|
+
function parseBrowseEdgarCompanies(html: string): { cik: string; name: string }[] {
|
|
1958
|
+
const out: { cik: string; name: string }[] = [];
|
|
1959
|
+
const single = html.match(/<span class="companyName">([\s\S]*?)<acronym[^>]*>CIK<\/acronym>#:\s*<a[^>]*>(\d+)/);
|
|
1960
|
+
if (single) return [{ cik: String(Number(single[2])), name: decodeHtml(single[1]).trim() }];
|
|
1961
|
+
for (const m of html.matchAll(/CIK=(\d+)&owner=include[^"]*">\d+<\/a><\/td>\s*<td scope="row">([\s\S]*?)<\/td>/g)) {
|
|
1962
|
+
out.push({ cik: String(Number(m[1])), name: decodeHtml(m[2].split(/<br/i)[0]).trim() });
|
|
1963
|
+
}
|
|
1964
|
+
return out;
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1883
1967
|
/** Does the sponsor name have its OWN SEC filer CIK (registered entity — a
|
|
1884
|
-
* subsidiary, or a private co that filed a Form D / draft S-1)?
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1968
|
+
* subsidiary, or a private co that filed a Form D / draft S-1)? Candidates
|
|
1969
|
+
* come from browse-edgar's company search (count=40 -- its order is
|
|
1970
|
+
* alphabetical, not relevance, so the first hits of a short query are often
|
|
1971
|
+
* people); individual reporting owners are dropped, the rest ranked by
|
|
1972
|
+
* similarity to the FULL sponsor name, and the best are validated against
|
|
1973
|
+
* their real name from the submissions API (which doubles as the entity
|
|
1974
|
+
* profile). When the query carries descriptor words beyond its one
|
|
1975
|
+
* distinctive token ("JANSSEN PHARMACEUTICALS"), a candidate must agree on
|
|
1976
|
+
* at least one of them. Returns {cik, profile} or null. */
|
|
1888
1977
|
async function edgarOwnCik(sponsor: string): Promise<{ cik: string; profile: EntityProfile } | null> {
|
|
1889
1978
|
const sig = significantTokens(sponsor);
|
|
1890
1979
|
const need = Math.min(2, sig.length || 1);
|
|
1980
|
+
const queryDesc = nameSimilarity(sponsor, sponsor).desc;
|
|
1981
|
+
const needDesc = sig.length <= 1 && queryDesc > 0 ? 1 : 0;
|
|
1982
|
+
const passes = (s: { sig: number; desc: number }) => s.sig >= need && s.desc >= needDesc;
|
|
1891
1983
|
for (const q of secQueryVariants(sponsor)) {
|
|
1892
|
-
const url = `https://www.sec.gov/cgi-bin/browse-edgar?company=${encodeURIComponent(q)}&action=getcompany&type=&dateb=&owner=include&count=
|
|
1984
|
+
const url = `https://www.sec.gov/cgi-bin/browse-edgar?company=${encodeURIComponent(q)}&action=getcompany&type=&dateb=&owner=include&count=40`;
|
|
1893
1985
|
const res = await pwFetch(url, { headers: { 'User-Agent': SEC_HEADERS['User-Agent'] } }).catch(() => null);
|
|
1894
1986
|
if (!res || !res.ok) continue;
|
|
1895
|
-
const
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1987
|
+
const found = parseBrowseEdgarCompanies(await res.text());
|
|
1988
|
+
const ranked = found
|
|
1989
|
+
.filter((c) => !isPersonShapedName(c.name))
|
|
1990
|
+
.map((c, i) => ({ ...c, i, sim: nameSimilarity(sponsor, c.name) }))
|
|
1991
|
+
.filter((c) => passes(c.sim))
|
|
1992
|
+
.sort((a, b) => b.sim.score - a.sim.score || a.i - b.i);
|
|
1993
|
+
for (const c of ranked.slice(0, 3)) {
|
|
1994
|
+
const profile = await edgarEntityProfile(c.cik);
|
|
1995
|
+
if (!profile?.name || isPersonShapedName(profile.name)) continue;
|
|
1996
|
+
if (passes(nameSimilarity(sponsor, profile.name))) return { cik: c.cik, profile };
|
|
1903
1997
|
}
|
|
1904
|
-
if (
|
|
1998
|
+
if (found.length) break; // first variant that returned candidates is authoritative
|
|
1905
1999
|
}
|
|
1906
2000
|
return null;
|
|
1907
2001
|
}
|
|
@@ -1972,16 +2066,24 @@ async function sponsorToFiler(sponsor: string): Promise<unknown> {
|
|
|
1972
2066
|
const tickers = await fetchCompanyTickers();
|
|
1973
2067
|
|
|
1974
2068
|
// 1) Direct listed match (exact / prefix / whole-word — not loose substring).
|
|
2069
|
+
// Also rank a hyphen-normalized, suffix-normalized form ("Bristol-Myers
|
|
2070
|
+
// Squibb Company" -> "BRISTOL MYERS SQUIBB CO") and keep whichever scores
|
|
2071
|
+
// higher, so inputs that already match keep behaving exactly as before.
|
|
1975
2072
|
const ranked = rankMatches(name, tickers, (r) => r.title);
|
|
1976
|
-
|
|
1977
|
-
|
|
2073
|
+
const secStyled = secStyleQuery(name);
|
|
2074
|
+
const rankedSecStyle = secStyled === name.toUpperCase() ? [] : rankMatches(secStyled, tickers, (r) => r.title);
|
|
2075
|
+
const best = rankedSecStyle.length && (!ranked.length || rankedSecStyle[0].score > ranked[0].score)
|
|
2076
|
+
? rankedSecStyle
|
|
2077
|
+
: ranked;
|
|
2078
|
+
if (best.length && best[0].score >= 2) {
|
|
2079
|
+
const m = best[0].item;
|
|
1978
2080
|
return {
|
|
1979
2081
|
status: 'resolved',
|
|
1980
2082
|
method: 'direct',
|
|
1981
|
-
confidence:
|
|
2083
|
+
confidence: best[0].score >= 3 ? 'high' : 'medium',
|
|
1982
2084
|
sponsor: name,
|
|
1983
2085
|
filer: filerOut(m),
|
|
1984
|
-
alternatives:
|
|
2086
|
+
alternatives: best.slice(1, 4).map((x) => filerOut(x.item)),
|
|
1985
2087
|
};
|
|
1986
2088
|
}
|
|
1987
2089
|
|
|
@@ -1994,24 +2096,59 @@ async function sponsorToFiler(sponsor: string): Promise<unknown> {
|
|
|
1994
2096
|
}
|
|
1995
2097
|
}
|
|
1996
2098
|
|
|
1997
|
-
// 3) Full-text: listed filers referencing the sponsor; confirm a parent
|
|
1998
|
-
//
|
|
1999
|
-
// "merely mentioned by an unrelated
|
|
2099
|
+
// 3) Full-text: listed filers referencing the sponsor; confirm a parent.
|
|
2100
|
+
// Rule A (shared_token_fulltext): the sponsor's distinctive token appears
|
|
2101
|
+
// in the candidate's OWN name (kills "merely mentioned by an unrelated
|
|
2102
|
+
// filer" false positives) — catches e.g. "Capricor Inc." -> "Capricor
|
|
2103
|
+
// Therapeutics". This does NOT catch a brand subsidiary whose name shares
|
|
2104
|
+
// no word with its parent ("Janssen" / "Johnson & Johnson", "Merck Sharp
|
|
2105
|
+
// and Dohme" is the one case that happens to share "Merck") — for that we
|
|
2106
|
+
// never promote on mention-count alone (a press/litigation mention is not
|
|
2107
|
+
// ownership); we ask SEC's own authoritative source instead: does the
|
|
2108
|
+
// dominant candidate's latest 10-K Exhibit 21 subsidiary list (Item
|
|
2109
|
+
// 601(b)(21)) name the sponsor? Rule B (ex21_subsidiary_match) fires only
|
|
2110
|
+
// then, and only against the single dominant candidate, so a genuine miss
|
|
2111
|
+
// never pays for an extra filing+exhibit fetch. #2971.
|
|
2000
2112
|
const ft = await fullTextListedFilers(name).catch(() => ({ candidates: [] as ListedFiler[], total: 0 }));
|
|
2001
|
-
|
|
2113
|
+
let confirmed = ft.candidates.find((c) => normName(c.name).split(' ').includes(disc));
|
|
2114
|
+
let rule: 'shared_token_fulltext' | 'ex21_subsidiary_match' | null = confirmed ? 'shared_token_fulltext' : null;
|
|
2115
|
+
let ex21Match: string | null = null;
|
|
2116
|
+
|
|
2117
|
+
if (!confirmed && ft.candidates.length) {
|
|
2118
|
+
const top = ft.candidates[0];
|
|
2119
|
+
const second = ft.candidates[1];
|
|
2120
|
+
// Dominance gate before spending the EX-21 fetch: top mention count must
|
|
2121
|
+
// be meaningful on its own AND clearly ahead of the runner-up.
|
|
2122
|
+
const dominant = top.hits >= 5 && (!second || top.hits >= 3 * second.hits);
|
|
2123
|
+
if (dominant) {
|
|
2124
|
+
ex21Match = await exhibit21NamesSponsor(top.cik, name).catch(() => null);
|
|
2125
|
+
if (ex21Match) {
|
|
2126
|
+
confirmed = top;
|
|
2127
|
+
rule = 'ex21_subsidiary_match';
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2002
2132
|
if (confirmed) {
|
|
2133
|
+
const directMatch = own ? { cik: own.cik, name: own.profile?.name ?? null, listed: false } : null;
|
|
2003
2134
|
return {
|
|
2004
2135
|
status: 'resolved_via_parent',
|
|
2005
|
-
method: 'subsidiary_fulltext',
|
|
2136
|
+
method: rule === 'ex21_subsidiary_match' ? 'subsidiary_exhibit21' : 'subsidiary_fulltext',
|
|
2137
|
+
rule,
|
|
2006
2138
|
confidence: confirmed.hits >= 10 ? 'high' : 'medium',
|
|
2007
2139
|
sponsor: name,
|
|
2008
2140
|
filer: { company_name: confirmed.name, ticker: confirmed.ticker, cik: confirmed.cik, cik_padded: confirmed.cik.padStart(10, '0') },
|
|
2141
|
+
parent_filer: { company_name: confirmed.name, ticker: confirmed.ticker, cik: confirmed.cik, cik_padded: confirmed.cik.padStart(10, '0') },
|
|
2142
|
+
direct_match: directMatch,
|
|
2009
2143
|
evidence: {
|
|
2010
2144
|
parent_mention_filings: confirmed.hits,
|
|
2011
2145
|
distinctive_token: disc,
|
|
2012
2146
|
own_subsidiary_cik: own?.cik ?? null,
|
|
2013
2147
|
own_subsidiary_name: own?.profile?.name ?? null,
|
|
2014
|
-
|
|
2148
|
+
exhibit_21_subsidiary_name: ex21Match,
|
|
2149
|
+
note: rule === 'ex21_subsidiary_match'
|
|
2150
|
+
? `"${name}" is named as a subsidiary in ${confirmed.name}'s own SEC Exhibit 21 filing (as "${ex21Match}") -- the authoritative ownership disclosure, used because the two names share no common word.`
|
|
2151
|
+
: 'Sponsor appears to be an operating subsidiary. Resolved to the listed parent whose SEC filings reference it and whose name shares the sponsor\'s distinctive token.',
|
|
2015
2152
|
},
|
|
2016
2153
|
alternatives: ft.candidates.slice(0, 3).map((c) => ({ company_name: c.name, ticker: c.ticker, cik: c.cik, mention_filings: c.hits })),
|
|
2017
2154
|
};
|
|
@@ -4466,6 +4603,45 @@ function parseSubsidiaryTable(html: string): Subsidiary[] {
|
|
|
4466
4603
|
return out;
|
|
4467
4604
|
}
|
|
4468
4605
|
|
|
4606
|
+
// Used by sponsor_to_filer (#2971) to confirm a dominant full-text-mention
|
|
4607
|
+
// candidate as the sponsor's real listed parent via SEC's own authoritative
|
|
4608
|
+
// ownership disclosure, for the case where the subsidiary's name shares no
|
|
4609
|
+
// word with its parent's (Janssen / Johnson & Johnson). Walks the SAME path
|
|
4610
|
+
// as filer_to_sponsors (latest 10-K -> -index.html -> EX-21 -> parsed table)
|
|
4611
|
+
// but stops at the first subsidiary whose name shares a significant token
|
|
4612
|
+
// with the sponsor, rather than returning the whole list. One filing fetch +
|
|
4613
|
+
// one exhibit fetch, bounded by the caller's dominance gate so it only runs
|
|
4614
|
+
// against a serious candidate. Returns the matching subsidiary's own EX-21
|
|
4615
|
+
// name, or null (no 10-K / no EX-21 / didn't parse / sponsor not listed).
|
|
4616
|
+
async function exhibit21NamesSponsor(cik: string, sponsorName: string): Promise<string | null> {
|
|
4617
|
+
const paddedCik = padCik(cik);
|
|
4618
|
+
const subRes = await pwFetch(`${DATA_BASE}/submissions/CIK${paddedCik}.json`, { headers: SEC_HEADERS });
|
|
4619
|
+
if (!subRes.ok) return null;
|
|
4620
|
+
const sub = (await subRes.json()) as {
|
|
4621
|
+
filings?: { recent?: { accessionNumber?: string[]; form?: string[] } };
|
|
4622
|
+
};
|
|
4623
|
+
const forms = sub.filings?.recent?.form ?? [];
|
|
4624
|
+
const accs = sub.filings?.recent?.accessionNumber ?? [];
|
|
4625
|
+
const idx10k = forms.findIndex((f) => f === '10-K' || f === '10-K/A');
|
|
4626
|
+
if (idx10k < 0) return null;
|
|
4627
|
+
const accession = accs[idx10k];
|
|
4628
|
+
if (!accession) return null;
|
|
4629
|
+
const accessionPath = accession.replace(/-/g, '');
|
|
4630
|
+
const cikNoZeros = String(Number(cik));
|
|
4631
|
+
const folder = `https://www.sec.gov/Archives/edgar/data/${cikNoZeros}/${accessionPath}`;
|
|
4632
|
+
const idxRes = await pwFetch(`${folder}/${accession}-index.html`, { headers: SEC_HEADERS });
|
|
4633
|
+
if (!idxRes.ok) return null;
|
|
4634
|
+
const exUrl = findExhibit21Url(await idxRes.text());
|
|
4635
|
+
if (!exUrl) return null;
|
|
4636
|
+
const exRes = await pwFetch(exUrl, { headers: { 'User-Agent': SEC_HEADERS['User-Agent'] } });
|
|
4637
|
+
if (!exRes.ok) return null;
|
|
4638
|
+
const parsed = parseSubsidiaryTable(await exRes.text());
|
|
4639
|
+
const sig = new Set(significantTokens(sponsorName));
|
|
4640
|
+
if (!sig.size) return null;
|
|
4641
|
+
const hit = parsed.find((s) => significantTokens(s.name).some((t) => sig.has(t)));
|
|
4642
|
+
return hit ? hit.name : null;
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4469
4645
|
async function filerToSponsors(
|
|
4470
4646
|
tickerOrCik: string | undefined,
|
|
4471
4647
|
nameFilterArg: string | undefined,
|
package/src/server.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import pack from './index.js';
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
const server = new Server(
|
|
12
|
-
{ name: '@pipeworx/mcp-edgar', version: '0.3.
|
|
13
|
+
{ name: '@pipeworx/mcp-edgar', version: '0.3.9' },
|
|
13
14
|
{ capabilities: { tools: {} } },
|
|
14
15
|
);
|
|
15
16
|
|
|
@@ -24,7 +25,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
24
25
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
25
26
|
const { name, arguments: args } = request.params;
|
|
26
27
|
try {
|
|
27
|
-
const
|
|
28
|
+
const callArgs = { ...((args ?? {}) as Record<string, unknown>) };
|
|
29
|
+
|
|
30
|
+
const result = await pack.callTool(name, callArgs);
|
|
28
31
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
29
32
|
} catch (err) {
|
|
30
33
|
return {
|