@pipeworx/mcp-emarket-storage 0.1.6 → 0.1.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
@@ -5,7 +5,7 @@ Italian regulated company disclosures direct from eMarket Storage (Teleborsa)
5
5
  built to fix the months-long lag in the community ESEF index for current-year
6
6
  Italian filings.
7
7
 
8
- Part of [Pipeworx](https://pipeworx.io) — an MCP gateway connecting AI agents to 1878+ live data sources. This is an independent, unofficial integration — not affiliated with, endorsed by, or published by the upstream provider.
8
+ 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.
9
9
 
10
10
  ## Tools
11
11
 
@@ -200,7 +200,7 @@ directly, instead of just this one's:
200
200
  }
201
201
  ```
202
202
 
203
- Both URLs reach the same gateway and the same 1878+ data sources. The
203
+ Both URLs reach the same gateway and the same 1881+ data sources. The
204
204
  only difference is which pack's tools are listed **directly**; `ask_pipeworx`
205
205
  reaches all of them from either one.
206
206
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipeworx/mcp-emarket-storage",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Italian regulated disclosures direct from eMarket Storage (Teleborsa) — keyless, same-day-fresh.",
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-5c645dc46698ad0a98a7fe7c1b219859f4a852bd450e964af83f9c24afd95130",
30
- "sourceCommit": "29fb434ca65fe0f61191f19c3502f583f5fb99d7"
29
+ "sourceHash": "v1-7a1300e601cda197b5f68de83d9ff1dc4320a675b1ee1eed7eff8fadf52856fa",
30
+ "sourceCommit": "24e32b525f9c0b2fd7d94b519b08abb108cc11cd"
31
31
  }
32
32
  }
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.pipeworx-io/emarket-storage",
4
4
  "title": "Emarket Storage",
5
5
  "description": "Italian regulated disclosures direct from eMarket Storage (Teleborsa) — keyless, same-day-fresh.",
6
- "version": "0.1.6",
6
+ "version": "0.1.8",
7
7
  "websiteUrl": "https://pipeworx.io/packs/emarket-storage",
8
8
  "repository": {
9
9
  "url": "https://github.com/pipeworx-io/mcp-emarket-storage",
package/src/index.ts CHANGED
@@ -978,25 +978,64 @@ function rankMatches<T>(
978
978
  query: string,
979
979
  items: T[],
980
980
  getName: (item: T) => string,
981
+ getAliases?: (item: T) => string[],
981
982
  ): RankedMatch<T>[] {
982
983
  const q = normalize(query);
983
984
  if (!q) return [];
984
985
  const scored: { item: T; kind: MatchKind; score: number; len: number }[] = [];
985
986
  for (const item of items) {
986
987
  const name = getName(item);
987
- const n = normalize(name);
988
+ // `getAliases` (optional, fleet #3607) are OTHER spellings of the same name,
989
+ // each scored on its own; the best-scoring spelling wins. Additive on
990
+ // purpose: the raw name is always tried, so a query that already matched
991
+ // keeps matching exactly as before, and an alias can only ADD a match.
988
992
  let kind: MatchKind | null = null;
989
993
  let score = 0;
990
- if (n === q) { kind = 'exact'; score = 4; }
991
- else if (n.startsWith(`${q} `)) { kind = 'prefix'; score = 3; }
992
- else if (n.includes(` ${q} `) || n.endsWith(` ${q}`)) { kind = 'word'; score = 2; }
993
- else if (n.includes(q)) { kind = 'substring'; score = 1; }
994
+ for (const spelling of [name, ...(getAliases?.(item) ?? [])]) {
995
+ const n = normalize(spelling);
996
+ let k: MatchKind | null = null;
997
+ let sc = 0;
998
+ if (n === q) { k = 'exact'; sc = 4; }
999
+ else if (n.startsWith(`${q} `)) { k = 'prefix'; sc = 3; }
1000
+ else if (n.includes(` ${q} `) || n.endsWith(` ${q}`)) { k = 'word'; sc = 2; }
1001
+ else if (n.includes(q)) { k = 'substring'; sc = 1; }
1002
+ if (k && sc > score) { kind = k; score = sc; }
1003
+ }
994
1004
  if (kind) scored.push({ item, kind, score, len: name.length });
995
1005
  }
996
1006
  scored.sort((a, b) => b.score - a.score || a.len - b.len);
997
1007
  return scored.map(({ item, kind, score }) => ({ item, kind, score }));
998
1008
  }
999
1009
 
1010
+ /**
1011
+ * The trailing designators EDGAR appends to a registrant name that are NOT part
1012
+ * of the company's name: state or country of incorporation ("/DE/", "/MD",
1013
+ * "/CAN/", "/Cayman"), "/NEW", and the ADR marker. Matched case-insensitively
1014
+ * against the WHOLE segment after the last slash, so a name with a slash INSIDE
1015
+ * it ("CULLEN/FROST BANKERS", "M/I HOMES", "Quad/Graphics", "DATA I/O CORP",
1016
+ * "GENMAB A/S") is never cut. A genuine legal form that happens to look like a
1017
+ * 2-letter designator ("Anheuser-Busch InBev SA/NV") gains an extra alias
1018
+ * ("...SA"); the raw title is always tried too, so that costs nothing. Measured
1019
+ * against SEC's company_tickers.json 2026-10-26: 951 of 10,429 titles carry a
1020
+ * slash; the trailing segments are ADR (545), CAN, state codes, NEW, CAYMAN,
1021
+ * DEL(AWARE), CANADA, JAPAN, or empty ("TOYOTA MOTOR CORP/").
1022
+ */
1023
+ const EDGAR_TRAILING_DESIGNATOR =
1024
+ /\s*\/\s*(?:[A-Za-z]{2}|ADR|CAN|NEW|CAYMAN|DEL|DELAWARE|CANADA|JAPAN|UK)?\s*\/?\s*$/i;
1025
+
1026
+ /**
1027
+ * Other spellings of an EDGAR registrant title that name the same company:
1028
+ * the title with its jurisdiction/ADR designator removed ("TOYOTA MOTOR CORP/"
1029
+ * -> "TOYOTA MOTOR CORP"). Empty when there is nothing to remove. Used as
1030
+ * `rankMatches`'s aliases, never as a replacement for the title (fleet #3607).
1031
+ * Deliberately NOT a change to `normalize`: that is shared by every pack that
1032
+ * ranks its own list, and "/" is not a registrant-name artefact in those.
1033
+ */
1034
+ function edgarNameAliases(title: string): string[] {
1035
+ const stripped = title.replace(EDGAR_TRAILING_DESIGNATOR, '').trim();
1036
+ return stripped && stripped !== title.trim() && /\S/.test(stripped) ? [stripped] : [];
1037
+ }
1038
+
1000
1039
  interface SecTickerRow { cik_str: number; ticker: string; title: string }
1001
1040
 
1002
1041
  interface SecEntity {
@@ -1013,6 +1052,13 @@ interface SecEntity {
1013
1052
  * `matched_by: 'company_name'` alone cannot (fleet #3100).
1014
1053
  */
1015
1054
  matched_kind?: MatchKind;
1055
+ /**
1056
+ * Other companies whose names also matched — distinct companies, deduped
1057
+ * by CIK, excluding this issuer's own share classes (never the winner's
1058
+ * own ticker/CIK under a different symbol). Consumers read this to tell a
1059
+ * genuinely CONTESTED name match from a clean one. See
1060
+ * `distinctCompanyAlternatives` below for how it's built (fleet #3621).
1061
+ */
1016
1062
  alternatives?: { ticker: string; company_name: string; cik: string; kind: MatchKind }[];
1017
1063
  }
1018
1064
 
@@ -1046,16 +1092,11 @@ async function resolveSecEntity(
1046
1092
  }
1047
1093
 
1048
1094
  // 2) Company-name fallback.
1049
- const ranked = rankMatches(query, rows, (r) => r.title);
1095
+ const ranked = rankMatches(query, rows, (r) => r.title, (r) => edgarNameAliases(r.title));
1050
1096
  if (ranked.length) {
1051
1097
  const best = toEntity(ranked[0].item, 'company_name');
1052
1098
  best.matched_kind = ranked[0].kind;
1053
- const alts = ranked.slice(1, 4).map((m) => ({
1054
- ticker: m.item.ticker,
1055
- company_name: m.item.title,
1056
- cik: String(m.item.cik_str),
1057
- kind: m.kind,
1058
- }));
1099
+ const alts = distinctCompanyAlternatives(ranked);
1059
1100
  if (alts.length) best.alternatives = alts;
1060
1101
  return best;
1061
1102
  }
@@ -1063,6 +1104,33 @@ async function resolveSecEntity(
1063
1104
  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.`);
1064
1105
  }
1065
1106
 
1107
+ /** `alternatives` lists at most this many OTHER companies; company-spine.test.ts pins it to the real function. */
1108
+ const ALTERNATIVES_CAP = 3;
1109
+
1110
+ /**
1111
+ * The runners-up for a name match, as DISTINCT COMPANIES (fleet #3621).
1112
+ *
1113
+ * company_tickers.json lists one row per ticker, so an issuer with preferred
1114
+ * shares appears several times under one CIK (Ford: F, F-PB, F-PC, F-PD; AT&T:
1115
+ * T, TBB, T-PA, T-PC). Slicing the ranked rows left those rows to fill every
1116
+ * alternatives slot, hiding any real rival ranked behind them, and made a company
1117
+ * its own "alternative". So: drop every row that is the winner's own CIK, keep
1118
+ * only the best-ranked row of each other CIK, THEN cap. The winner's other
1119
+ * tickers are not lost information: they are the same company, and `ticker` /
1120
+ * `cik` already identify it. `ranked` is already best-first, so first-seen wins.
1121
+ */
1122
+ function distinctCompanyAlternatives(ranked: RankedMatch<SecTickerRow>[]): NonNullable<SecEntity['alternatives']> {
1123
+ const seen = new Set<number>([ranked[0].item.cik_str]);
1124
+ const out: NonNullable<SecEntity['alternatives']> = [];
1125
+ for (const m of ranked.slice(1)) {
1126
+ if (seen.has(m.item.cik_str)) continue;
1127
+ seen.add(m.item.cik_str);
1128
+ out.push({ ticker: m.item.ticker, company_name: m.item.title, cik: String(m.item.cik_str), kind: m.kind });
1129
+ if (out.length >= ALTERNATIVES_CAP) break;
1130
+ }
1131
+ return out;
1132
+ }
1133
+
1066
1134
  function toEntity(r: SecTickerRow, matched_by: 'ticker' | 'company_name'): SecEntity {
1067
1135
  return {
1068
1136
  ticker: r.ticker,
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-emarket-storage', version: '0.1.6' },
13
+ { name: '@pipeworx/mcp-emarket-storage', version: '0.1.8' },
14
14
  { capabilities: { tools: {} } },
15
15
  );
16
16