fdic-mcp-server 1.1.0 → 1.1.2

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.
Files changed (4) hide show
  1. package/README.md +129 -294
  2. package/dist/index.js +207 -107
  3. package/dist/server.js +211 -109
  4. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamable
30
30
  var import_express = __toESM(require("express"));
31
31
 
32
32
  // src/constants.ts
33
- var VERSION = true ? "1.1.0" : process.env.npm_package_version ?? "0.0.0-dev";
33
+ var VERSION = true ? "1.1.2" : process.env.npm_package_version ?? "0.0.0-dev";
34
34
  var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
35
35
  var CHARACTER_LIMIT = 5e4;
36
36
  var ENDPOINTS = {
@@ -55,12 +55,23 @@ var apiClient = import_axios.default.create({
55
55
  }
56
56
  });
57
57
  var QUERY_CACHE_TTL_MS = 6e4;
58
+ var QUERY_CACHE_MAX_ENTRIES = 500;
58
59
  var queryCache = /* @__PURE__ */ new Map();
59
60
  function pruneExpiredQueryCache(now) {
60
61
  for (const [key, entry] of queryCache.entries()) {
61
- if (entry.expiresAt <= now) {
62
- queryCache.delete(key);
62
+ if (entry.expiresAt > now) {
63
+ break;
63
64
  }
65
+ queryCache.delete(key);
66
+ }
67
+ }
68
+ function evictOverflowQueryCache() {
69
+ while (queryCache.size > QUERY_CACHE_MAX_ENTRIES) {
70
+ const oldestKey = queryCache.keys().next().value;
71
+ if (oldestKey === void 0) {
72
+ break;
73
+ }
74
+ queryCache.delete(oldestKey);
64
75
  }
65
76
  }
66
77
  function getCacheKey(endpoint, params) {
@@ -74,6 +85,38 @@ function getCacheKey(endpoint, params) {
74
85
  params.sort_order ?? null
75
86
  ]);
76
87
  }
88
+ function isRecord(value) {
89
+ return typeof value === "object" && value !== null;
90
+ }
91
+ function validateFdicResponseShape(endpoint, payload) {
92
+ if (!isRecord(payload)) {
93
+ throw new Error(
94
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected an object payload.`
95
+ );
96
+ }
97
+ const { data, meta } = payload;
98
+ if (!Array.isArray(data)) {
99
+ throw new Error(
100
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected 'data' to be an array.`
101
+ );
102
+ }
103
+ if (!isRecord(meta) || typeof meta.total !== "number") {
104
+ throw new Error(
105
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected 'meta.total' to be a number.`
106
+ );
107
+ }
108
+ return {
109
+ data: data.map((item, index) => {
110
+ if (!isRecord(item) || !isRecord(item.data)) {
111
+ throw new Error(
112
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected data[${index}] to contain an object 'data' property.`
113
+ );
114
+ }
115
+ return { data: item.data };
116
+ }),
117
+ meta: { total: meta.total }
118
+ };
119
+ }
77
120
  async function queryEndpoint(endpoint, params, options = {}) {
78
121
  if (options.signal?.aborted) {
79
122
  throw new Error("FDIC API request was canceled before it started.");
@@ -101,7 +144,7 @@ async function queryEndpoint(endpoint, params, options = {}) {
101
144
  params: queryParams,
102
145
  signal: options.signal
103
146
  });
104
- return response.data;
147
+ return validateFdicResponseShape(endpoint, response.data);
105
148
  } catch (err) {
106
149
  if (options.signal?.aborted || typeof err === "object" && err !== null && "code" in err && err.code === "ERR_CANCELED" || err instanceof DOMException && err.name === "AbortError") {
107
150
  throw new Error("FDIC API request was canceled.");
@@ -133,6 +176,7 @@ async function queryEndpoint(endpoint, params, options = {}) {
133
176
  expiresAt: now + QUERY_CACHE_TTL_MS,
134
177
  value: requestPromise
135
178
  });
179
+ evictOverflowQueryCache();
136
180
  }
137
181
  try {
138
182
  return await requestPromise;
@@ -144,7 +188,14 @@ async function queryEndpoint(endpoint, params, options = {}) {
144
188
  }
145
189
  }
146
190
  function extractRecords(response) {
147
- return response.data.map((item) => item.data);
191
+ return response.data.map((item, index) => {
192
+ if (!isRecord(item) || !isRecord(item.data)) {
193
+ throw new Error(
194
+ `Unexpected FDIC API response shape: expected data[${index}] to contain an object 'data' property.`
195
+ );
196
+ }
197
+ return item.data;
198
+ });
148
199
  }
149
200
  function buildPaginationInfo(total, offset, count) {
150
201
  const has_more = total > offset + count;
@@ -156,6 +207,10 @@ function buildPaginationInfo(total, offset, count) {
156
207
  ...has_more ? { next_offset: offset + count } : {}
157
208
  };
158
209
  }
210
+ function buildTruncationWarning(label, total, count, guidance) {
211
+ if (total <= count) return void 0;
212
+ return `${label} truncated to ${count.toLocaleString()} records out of ${total.toLocaleString()} matched rows. ${guidance}`;
213
+ }
159
214
  function truncateIfNeeded(text, charLimit) {
160
215
  if (text.length <= charLimit) return text;
161
216
  return text.slice(0, charLimit) + `
@@ -1073,9 +1128,40 @@ Prefer concise human-readable summaries or tables when answering users. Structur
1073
1128
 
1074
1129
  // src/tools/analysis.ts
1075
1130
  var import_zod7 = require("zod");
1131
+
1132
+ // src/tools/shared/queryUtils.ts
1076
1133
  var CHUNK_SIZE = 25;
1077
1134
  var MAX_CONCURRENCY = 4;
1078
1135
  var ANALYSIS_TIMEOUT_MS = 9e4;
1136
+ function asNumber(value) {
1137
+ return typeof value === "number" ? value : null;
1138
+ }
1139
+ function buildCertFilters(certs) {
1140
+ const filters = [];
1141
+ for (let i = 0; i < certs.length; i += CHUNK_SIZE) {
1142
+ const chunk = certs.slice(i, i + CHUNK_SIZE);
1143
+ filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1144
+ }
1145
+ return filters;
1146
+ }
1147
+ async function mapWithConcurrency(values, limit, mapper) {
1148
+ const results = new Array(values.length);
1149
+ let nextIndex = 0;
1150
+ async function worker() {
1151
+ while (true) {
1152
+ const currentIndex = nextIndex;
1153
+ nextIndex += 1;
1154
+ if (currentIndex >= values.length) return;
1155
+ results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1156
+ }
1157
+ }
1158
+ await Promise.all(
1159
+ Array.from({ length: Math.min(limit, values.length) }, () => worker())
1160
+ );
1161
+ return results;
1162
+ }
1163
+
1164
+ // src/tools/analysis.ts
1079
1165
  var SortFieldSchema = import_zod7.z.enum([
1080
1166
  "asset_growth",
1081
1167
  "asset_growth_pct",
@@ -1124,32 +1210,9 @@ var SnapshotAnalysisSchema = import_zod7.z.object({
1124
1210
  });
1125
1211
  }
1126
1212
  });
1127
- function asNumber(value) {
1128
- return typeof value === "number" ? value : null;
1129
- }
1130
- function buildCertFilters(certs) {
1131
- const filters = [];
1132
- for (let i = 0; i < certs.length; i += CHUNK_SIZE) {
1133
- const chunk = certs.slice(i, i + CHUNK_SIZE);
1134
- filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1135
- }
1136
- return filters;
1137
- }
1138
- async function mapWithConcurrency(values, limit, mapper) {
1139
- const results = new Array(values.length);
1140
- let nextIndex = 0;
1141
- async function worker() {
1142
- while (true) {
1143
- const currentIndex = nextIndex;
1144
- nextIndex += 1;
1145
- if (currentIndex >= values.length) return;
1146
- results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1147
- }
1148
- }
1149
- await Promise.all(
1150
- Array.from({ length: Math.min(limit, values.length) }, () => worker())
1151
- );
1152
- return results;
1213
+ function maxOrNull(values) {
1214
+ const nonNullValues = values.filter((value) => value !== null);
1215
+ return nonNullValues.length > 0 ? Math.max(...nonNullValues) : null;
1153
1216
  }
1154
1217
  function ratio(numerator, denominator) {
1155
1218
  if (numerator === null || denominator === null || denominator === 0) {
@@ -1333,7 +1396,7 @@ function summarizeTimeSeries(records, demographicsByDate, institution) {
1333
1396
  const depositsPerOfficeEnd = ratio(depEnd, officesEnd);
1334
1397
  const depositsToAssetsStart = ratio(depStart, assetStart);
1335
1398
  const depositsToAssetsEnd = ratio(depEnd, assetEnd);
1336
- const peakAsset = Math.max(...assetSeries.filter((value) => value !== null));
1399
+ const peakAsset = maxOrNull(assetSeries);
1337
1400
  const troughRoaValues = roaSeries.filter((value) => value !== null);
1338
1401
  const troughRoa = troughRoaValues.length > 0 ? Math.min(...troughRoaValues) : null;
1339
1402
  const comparison = {
@@ -1456,29 +1519,46 @@ async function fetchBatchedRecordsForDates(endpoint, certs, repdteFilters, field
1456
1519
  certFilter
1457
1520
  }))
1458
1521
  );
1459
- const responses = await mapWithConcurrency(tasks, MAX_CONCURRENCY, async (task) => {
1460
- const response = await queryEndpoint(endpoint, {
1461
- filters: `(${task.certFilter}) AND ${task.repdteFilter}`,
1462
- fields,
1463
- limit: 1e4,
1464
- offset: 0,
1465
- sort_by: "CERT",
1466
- sort_order: "ASC"
1467
- }, { signal });
1468
- return { repdteFilter: task.repdteFilter, response };
1469
- });
1522
+ const responses = await mapWithConcurrency(
1523
+ tasks,
1524
+ MAX_CONCURRENCY,
1525
+ async (task) => {
1526
+ const response = await queryEndpoint(
1527
+ endpoint,
1528
+ {
1529
+ filters: `(${task.certFilter}) AND ${task.repdteFilter}`,
1530
+ fields,
1531
+ limit: 1e4,
1532
+ offset: 0,
1533
+ sort_by: "CERT",
1534
+ sort_order: "ASC"
1535
+ },
1536
+ { signal }
1537
+ );
1538
+ return { repdteFilter: task.repdteFilter, response };
1539
+ }
1540
+ );
1470
1541
  const byDate = /* @__PURE__ */ new Map();
1542
+ const warnings = /* @__PURE__ */ new Set();
1471
1543
  for (const { repdteFilter, response } of responses) {
1472
1544
  if (!byDate.has(repdteFilter)) {
1473
1545
  byDate.set(repdteFilter, /* @__PURE__ */ new Map());
1474
1546
  }
1547
+ const records = extractRecords(response);
1548
+ const warning = buildTruncationWarning(
1549
+ `${endpoint} batch for ${repdteFilter}`,
1550
+ response.meta.total,
1551
+ records.length,
1552
+ "Narrow the comparison set with institution_filters or certs for complete analysis."
1553
+ );
1554
+ if (warning) warnings.add(warning);
1475
1555
  const target = byDate.get(repdteFilter);
1476
- for (const record of extractRecords(response)) {
1556
+ for (const record of records) {
1477
1557
  const cert = asNumber(record.CERT);
1478
1558
  if (cert !== null) target.set(cert, record);
1479
1559
  }
1480
1560
  }
1481
- return byDate;
1561
+ return { byDate, warnings: [...warnings] };
1482
1562
  }
1483
1563
  async function fetchSeriesRecords(endpoint, certs, startRepdte, endRepdte, fields, signal) {
1484
1564
  const certFilters = buildCertFilters(certs);
@@ -1495,15 +1575,24 @@ async function fetchSeriesRecords(endpoint, certs, startRepdte, endRepdte, field
1495
1575
  }, { signal })
1496
1576
  );
1497
1577
  const grouped = /* @__PURE__ */ new Map();
1578
+ const warnings = /* @__PURE__ */ new Set();
1498
1579
  for (const response of responses) {
1499
- for (const record of extractRecords(response)) {
1580
+ const records = extractRecords(response);
1581
+ const warning = buildTruncationWarning(
1582
+ `${endpoint} batch for REPDTE:[${startRepdte} TO ${endRepdte}]`,
1583
+ response.meta.total,
1584
+ records.length,
1585
+ "Narrow the comparison set with certs or a shorter date range for complete analysis."
1586
+ );
1587
+ if (warning) warnings.add(warning);
1588
+ for (const record of records) {
1500
1589
  const cert = asNumber(record.CERT);
1501
1590
  if (cert === null) continue;
1502
1591
  if (!grouped.has(cert)) grouped.set(cert, []);
1503
1592
  grouped.get(cert).push(record);
1504
1593
  }
1505
1594
  }
1506
- return grouped;
1595
+ return { grouped, warnings: [...warnings] };
1507
1596
  }
1508
1597
  function buildSnapshotComparison(cert, institution, startFinancial, endFinancial, startDemo, endDemo, startRepdte, endRepdte) {
1509
1598
  const assetStart = asNumber(startFinancial.ASSET);
@@ -1660,8 +1749,9 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1660
1749
  )
1661
1750
  );
1662
1751
  let comparisons = [];
1752
+ const warnings = rosterResult.warning ? [rosterResult.warning] : [];
1663
1753
  if (analysis_mode === "timeseries") {
1664
- const [financialSeries, demographicsSeries] = await Promise.all([
1754
+ const [financialSeriesResult, demographicsSeriesResult] = await Promise.all([
1665
1755
  fetchSeriesRecords(
1666
1756
  ENDPOINTS.FINANCIALS,
1667
1757
  candidateCerts,
@@ -1677,8 +1767,17 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1677
1767
  end_repdte,
1678
1768
  "CERT,REPDTE,OFFTOT,OFFSTATE,CBSANAME",
1679
1769
  controller.signal
1680
- ) : Promise.resolve(/* @__PURE__ */ new Map())
1770
+ ) : Promise.resolve({
1771
+ grouped: /* @__PURE__ */ new Map(),
1772
+ warnings: []
1773
+ })
1681
1774
  ]);
1775
+ warnings.push(
1776
+ ...financialSeriesResult.warnings,
1777
+ ...demographicsSeriesResult.warnings
1778
+ );
1779
+ const financialSeries = financialSeriesResult.grouped;
1780
+ const demographicsSeries = demographicsSeriesResult.grouped;
1682
1781
  comparisons = candidateCerts.map(
1683
1782
  (cert) => summarizeTimeSeries(
1684
1783
  financialSeries.get(cert) ?? [],
@@ -1692,7 +1791,7 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1692
1791
  )
1693
1792
  ).filter((comparison) => comparison !== null);
1694
1793
  } else {
1695
- const [financialSnapshots, demographicSnapshots] = await Promise.all([
1794
+ const [financialSnapshotsResult, demographicSnapshotsResult] = await Promise.all([
1696
1795
  fetchBatchedRecordsForDates(
1697
1796
  ENDPOINTS.FINANCIALS,
1698
1797
  candidateCerts,
@@ -1706,8 +1805,17 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1706
1805
  [`REPDTE:${start_repdte}`, `REPDTE:${end_repdte}`],
1707
1806
  "CERT,REPDTE,OFFTOT,OFFSTATE,CBSANAME",
1708
1807
  controller.signal
1709
- ) : Promise.resolve(/* @__PURE__ */ new Map())
1808
+ ) : Promise.resolve({
1809
+ byDate: /* @__PURE__ */ new Map(),
1810
+ warnings: []
1811
+ })
1710
1812
  ]);
1813
+ warnings.push(
1814
+ ...financialSnapshotsResult.warnings,
1815
+ ...demographicSnapshotsResult.warnings
1816
+ );
1817
+ const financialSnapshots = financialSnapshotsResult.byDate;
1818
+ const demographicSnapshots = demographicSnapshotsResult.byDate;
1711
1819
  const startFinancials = financialSnapshots.get(`REPDTE:${start_repdte}`) ?? /* @__PURE__ */ new Map();
1712
1820
  const endFinancials = financialSnapshots.get(`REPDTE:${end_repdte}`) ?? /* @__PURE__ */ new Map();
1713
1821
  const startDemographics = demographicSnapshots.get(`REPDTE:${start_repdte}`) ?? /* @__PURE__ */ new Map();
@@ -1743,14 +1851,14 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1743
1851
  analysis_mode,
1744
1852
  sort_by,
1745
1853
  sort_order,
1746
- warnings: rosterResult.warning ? [rosterResult.warning] : [],
1854
+ warnings,
1747
1855
  insights: buildTopLevelInsights(sortedComparisons),
1748
1856
  ...pagination,
1749
1857
  comparisons: ranked
1750
1858
  };
1751
1859
  const text = truncateIfNeeded(
1752
1860
  [
1753
- rosterResult.warning ? `Warning: ${rosterResult.warning}` : null,
1861
+ ...warnings.map((warning) => `Warning: ${warning}`),
1754
1862
  formatComparisonText(output)
1755
1863
  ].filter((value) => value !== null).join("\n\n"),
1756
1864
  CHARACTER_LIMIT
@@ -1777,9 +1885,8 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1777
1885
 
1778
1886
  // src/tools/peerGroup.ts
1779
1887
  var import_zod8 = require("zod");
1780
- function asNumber2(value) {
1781
- return typeof value === "number" ? value : null;
1782
- }
1888
+
1889
+ // src/tools/shared/financialMetrics.ts
1783
1890
  function safeRatio(numerator, denominator) {
1784
1891
  if (numerator === null || denominator === null || denominator === 0) {
1785
1892
  return null;
@@ -1793,14 +1900,14 @@ function safeRatioPositiveDenom(numerator, denominator) {
1793
1900
  return numerator / denominator;
1794
1901
  }
1795
1902
  function deriveMetrics(raw) {
1796
- const asset = asNumber2(raw.ASSET);
1797
- const dep = asNumber2(raw.DEP);
1798
- const eqtot = asNumber2(raw.EQTOT);
1799
- const lnlsnet = asNumber2(raw.LNLSNET);
1800
- const intinc = asNumber2(raw.INTINC);
1801
- const eintexp = asNumber2(raw.EINTEXP);
1802
- const nonii = asNumber2(raw.NONII);
1803
- const nonix = asNumber2(raw.NONIX);
1903
+ const asset = asNumber(raw.ASSET);
1904
+ const dep = asNumber(raw.DEP);
1905
+ const eqtot = asNumber(raw.EQTOT);
1906
+ const lnlsnet = asNumber(raw.LNLSNET);
1907
+ const intinc = asNumber(raw.INTINC);
1908
+ const eintexp = asNumber(raw.EINTEXP);
1909
+ const nonii = asNumber(raw.NONII);
1910
+ const nonix = asNumber(raw.NONIX);
1804
1911
  const netInterestIncome = intinc !== null && eintexp !== null ? intinc - eintexp : null;
1805
1912
  const revenueDenominator = netInterestIncome !== null && nonii !== null ? netInterestIncome + nonii : null;
1806
1913
  const equityRatioRaw = safeRatio(eqtot, asset);
@@ -1808,9 +1915,9 @@ function deriveMetrics(raw) {
1808
1915
  return {
1809
1916
  asset,
1810
1917
  dep,
1811
- roa: asNumber2(raw.ROA),
1812
- roe: asNumber2(raw.ROE),
1813
- netnim: asNumber2(raw.NETNIM),
1918
+ roa: asNumber(raw.ROA),
1919
+ roe: asNumber(raw.ROE),
1920
+ netnim: asNumber(raw.NETNIM),
1814
1921
  equity_ratio: equityRatioRaw !== null ? equityRatioRaw * 100 : null,
1815
1922
  efficiency_ratio: efficiencyRatioRaw !== null ? efficiencyRatioRaw * 100 : null,
1816
1923
  loan_to_deposit: safeRatio(lnlsnet, dep),
@@ -1825,6 +1932,8 @@ function computeMedian(values) {
1825
1932
  if (sorted.length % 2 === 1) return sorted[mid];
1826
1933
  return (sorted[mid - 1] + sorted[mid]) / 2;
1827
1934
  }
1935
+
1936
+ // src/tools/peerGroup.ts
1828
1937
  function computeCompetitionRank(subjectValue, peerValues, higherIsBetter) {
1829
1938
  if (peerValues.length === 0) return null;
1830
1939
  const ascending = higherIsBetter === false;
@@ -1839,7 +1948,7 @@ function computeCompetitionRank(subjectValue, peerValues, higherIsBetter) {
1839
1948
  }
1840
1949
  }
1841
1950
  const rank = ranks.get(subjectValue);
1842
- const of = peerValues.length;
1951
+ const of = all.length;
1843
1952
  const percentile = Math.round((1 - (rank - 1) / of) * 100);
1844
1953
  return { rank, of, percentile };
1845
1954
  }
@@ -1950,34 +2059,7 @@ var PeerGroupInputSchema = import_zod8.z.object({
1950
2059
  });
1951
2060
  }
1952
2061
  });
1953
- var CHUNK_SIZE2 = 25;
1954
- var MAX_CONCURRENCY2 = 4;
1955
- var ANALYSIS_TIMEOUT_MS2 = 9e4;
1956
2062
  var FINANCIAL_FIELDS = "CERT,ASSET,DEP,NETINC,ROA,ROE,NETNIM,EQTOT,LNLSNET,INTINC,EINTEXP,NONII,NONIX";
1957
- function buildCertFilters2(certs) {
1958
- const filters = [];
1959
- for (let i = 0; i < certs.length; i += CHUNK_SIZE2) {
1960
- const chunk = certs.slice(i, i + CHUNK_SIZE2);
1961
- filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1962
- }
1963
- return filters;
1964
- }
1965
- async function mapWithConcurrency2(values, limit, mapper) {
1966
- const results = new Array(values.length);
1967
- let nextIndex = 0;
1968
- async function worker() {
1969
- while (true) {
1970
- const currentIndex = nextIndex;
1971
- nextIndex += 1;
1972
- if (currentIndex >= values.length) return;
1973
- results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1974
- }
1975
- }
1976
- await Promise.all(
1977
- Array.from({ length: Math.min(limit, values.length) }, () => worker())
1978
- );
1979
- return results;
1980
- }
1981
2063
  function formatMetricValue(key, value) {
1982
2064
  if (value === null) return "n/a";
1983
2065
  const def = METRIC_DEFINITIONS[key];
@@ -2060,7 +2142,7 @@ Metrics ranked (fixed order):
2060
2142
  - Equity Capital Ratio, Efficiency Ratio, Loan-to-Deposit Ratio
2061
2143
  - Deposits-to-Assets Ratio, Non-Interest Income Share
2062
2144
 
2063
- Rankings use competition rank (1, 2, 2, 4) with metric-specific denominators. Subject is excluded from peer set.
2145
+ Rankings use competition rank (1, 2, 2, 4). Rank, denominator, and percentile all use the same comparison set: matched peers plus the subject institution.
2064
2146
 
2065
2147
  Output includes:
2066
2148
  - Subject rankings and percentiles (when cert provided)
@@ -2079,7 +2161,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2079
2161
  },
2080
2162
  async (params) => {
2081
2163
  const controller = new AbortController();
2082
- const timeoutId = setTimeout(() => controller.abort(), ANALYSIS_TIMEOUT_MS2);
2164
+ const timeoutId = setTimeout(() => controller.abort(), ANALYSIS_TIMEOUT_MS);
2083
2165
  try {
2084
2166
  const warnings = [];
2085
2167
  let subjectProfile = null;
@@ -2164,7 +2246,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2164
2246
  }
2165
2247
  if (params.cert) {
2166
2248
  rosterRecords = rosterRecords.filter(
2167
- (r) => asNumber2(r.CERT) !== params.cert
2249
+ (r) => asNumber(r.CERT) !== params.cert
2168
2250
  );
2169
2251
  }
2170
2252
  const criteriaUsed = {
@@ -2216,12 +2298,12 @@ Override precedence: cert derives defaults, then explicit params override them.`
2216
2298
  structuredContent: output2
2217
2299
  };
2218
2300
  }
2219
- const peerCerts = rosterRecords.map((r) => asNumber2(r.CERT)).filter((c) => c !== null);
2220
- const certFilters = buildCertFilters2(peerCerts);
2301
+ const peerCerts = rosterRecords.map((r) => asNumber(r.CERT)).filter((c) => c !== null);
2302
+ const certFilters = buildCertFilters(peerCerts);
2221
2303
  const extraFieldsCsv = params.extra_fields && params.extra_fields.length > 0 ? "," + params.extra_fields.join(",") : "";
2222
- const financialResponses = await mapWithConcurrency2(
2304
+ const financialResponses = await mapWithConcurrency(
2223
2305
  certFilters,
2224
- MAX_CONCURRENCY2,
2306
+ MAX_CONCURRENCY,
2225
2307
  async (certFilter) => queryEndpoint(
2226
2308
  ENDPOINTS.FINANCIALS,
2227
2309
  {
@@ -2237,13 +2319,21 @@ Override precedence: cert derives defaults, then explicit params override them.`
2237
2319
  );
2238
2320
  const peerFinancialsByCert = /* @__PURE__ */ new Map();
2239
2321
  for (const response of financialResponses) {
2240
- for (const record of extractRecords(response)) {
2241
- const cert = asNumber2(record.CERT);
2322
+ const records = extractRecords(response);
2323
+ const warning = buildTruncationWarning(
2324
+ `financials batch for REPDTE:${params.repdte}`,
2325
+ response.meta.total,
2326
+ records.length,
2327
+ "Narrow the peer group criteria for complete analysis."
2328
+ );
2329
+ if (warning && !warnings.includes(warning)) warnings.push(warning);
2330
+ for (const record of records) {
2331
+ const cert = asNumber(record.CERT);
2242
2332
  if (cert !== null) peerFinancialsByCert.set(cert, record);
2243
2333
  }
2244
2334
  }
2245
2335
  const rosterByCert = new Map(
2246
- rosterRecords.map((r) => [asNumber2(r.CERT), r]).filter(
2336
+ rosterRecords.map((r) => [asNumber(r.CERT), r]).filter(
2247
2337
  (e) => e[0] !== null
2248
2338
  )
2249
2339
  );
@@ -2340,7 +2430,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2340
2430
  if (controller.signal.aborted) {
2341
2431
  return formatToolError(
2342
2432
  new Error(
2343
- `Peer group analysis timed out after ${Math.floor(ANALYSIS_TIMEOUT_MS2 / 1e3)} seconds. Narrow the peer group criteria and try again.`
2433
+ `Peer group analysis timed out after ${Math.floor(ANALYSIS_TIMEOUT_MS / 1e3)} seconds. Narrow the peer group criteria and try again.`
2344
2434
  )
2345
2435
  );
2346
2436
  }
@@ -2375,6 +2465,16 @@ async function runStdio() {
2375
2465
  await server.connect(transport);
2376
2466
  console.error("FDIC BankFind MCP server running on stdio");
2377
2467
  }
2468
+ function parseHttpPort(rawPort) {
2469
+ const port = Number.parseInt(rawPort ?? "3000", 10);
2470
+ if (Number.isNaN(port)) {
2471
+ throw new Error(`Invalid PORT value: ${rawPort ?? ""}`);
2472
+ }
2473
+ if (port < 0 || port > 65535) {
2474
+ throw new Error(`PORT must be between 0 and 65535. Received: ${port}`);
2475
+ }
2476
+ return port;
2477
+ }
2378
2478
  function createApp() {
2379
2479
  const app = (0, import_express.default)();
2380
2480
  app.use(import_express.default.json());
@@ -2418,7 +2518,7 @@ function createApp() {
2418
2518
  }
2419
2519
  async function runHTTP() {
2420
2520
  const app = createApp();
2421
- const port = parseInt(process.env.PORT ?? "3000");
2521
+ const port = parseHttpPort(process.env.PORT);
2422
2522
  app.listen(port, () => {
2423
2523
  console.error(
2424
2524
  `FDIC BankFind MCP server running on http://localhost:${port}/mcp`