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/server.js CHANGED
@@ -32,7 +32,8 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  createApp: () => createApp,
34
34
  createServer: () => createServer,
35
- main: () => main
35
+ main: () => main,
36
+ parseHttpPort: () => parseHttpPort
36
37
  });
37
38
  module.exports = __toCommonJS(index_exports);
38
39
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
@@ -41,7 +42,7 @@ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamable
41
42
  var import_express = __toESM(require("express"));
42
43
 
43
44
  // src/constants.ts
44
- var VERSION = true ? "1.1.0" : process.env.npm_package_version ?? "0.0.0-dev";
45
+ var VERSION = true ? "1.1.2" : process.env.npm_package_version ?? "0.0.0-dev";
45
46
  var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
46
47
  var CHARACTER_LIMIT = 5e4;
47
48
  var ENDPOINTS = {
@@ -66,12 +67,23 @@ var apiClient = import_axios.default.create({
66
67
  }
67
68
  });
68
69
  var QUERY_CACHE_TTL_MS = 6e4;
70
+ var QUERY_CACHE_MAX_ENTRIES = 500;
69
71
  var queryCache = /* @__PURE__ */ new Map();
70
72
  function pruneExpiredQueryCache(now) {
71
73
  for (const [key, entry] of queryCache.entries()) {
72
- if (entry.expiresAt <= now) {
73
- queryCache.delete(key);
74
+ if (entry.expiresAt > now) {
75
+ break;
74
76
  }
77
+ queryCache.delete(key);
78
+ }
79
+ }
80
+ function evictOverflowQueryCache() {
81
+ while (queryCache.size > QUERY_CACHE_MAX_ENTRIES) {
82
+ const oldestKey = queryCache.keys().next().value;
83
+ if (oldestKey === void 0) {
84
+ break;
85
+ }
86
+ queryCache.delete(oldestKey);
75
87
  }
76
88
  }
77
89
  function getCacheKey(endpoint, params) {
@@ -85,6 +97,38 @@ function getCacheKey(endpoint, params) {
85
97
  params.sort_order ?? null
86
98
  ]);
87
99
  }
100
+ function isRecord(value) {
101
+ return typeof value === "object" && value !== null;
102
+ }
103
+ function validateFdicResponseShape(endpoint, payload) {
104
+ if (!isRecord(payload)) {
105
+ throw new Error(
106
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected an object payload.`
107
+ );
108
+ }
109
+ const { data, meta } = payload;
110
+ if (!Array.isArray(data)) {
111
+ throw new Error(
112
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected 'data' to be an array.`
113
+ );
114
+ }
115
+ if (!isRecord(meta) || typeof meta.total !== "number") {
116
+ throw new Error(
117
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected 'meta.total' to be a number.`
118
+ );
119
+ }
120
+ return {
121
+ data: data.map((item, index) => {
122
+ if (!isRecord(item) || !isRecord(item.data)) {
123
+ throw new Error(
124
+ `Unexpected FDIC API response shape for endpoint ${endpoint}: expected data[${index}] to contain an object 'data' property.`
125
+ );
126
+ }
127
+ return { data: item.data };
128
+ }),
129
+ meta: { total: meta.total }
130
+ };
131
+ }
88
132
  async function queryEndpoint(endpoint, params, options = {}) {
89
133
  if (options.signal?.aborted) {
90
134
  throw new Error("FDIC API request was canceled before it started.");
@@ -112,7 +156,7 @@ async function queryEndpoint(endpoint, params, options = {}) {
112
156
  params: queryParams,
113
157
  signal: options.signal
114
158
  });
115
- return response.data;
159
+ return validateFdicResponseShape(endpoint, response.data);
116
160
  } catch (err) {
117
161
  if (options.signal?.aborted || typeof err === "object" && err !== null && "code" in err && err.code === "ERR_CANCELED" || err instanceof DOMException && err.name === "AbortError") {
118
162
  throw new Error("FDIC API request was canceled.");
@@ -144,6 +188,7 @@ async function queryEndpoint(endpoint, params, options = {}) {
144
188
  expiresAt: now + QUERY_CACHE_TTL_MS,
145
189
  value: requestPromise
146
190
  });
191
+ evictOverflowQueryCache();
147
192
  }
148
193
  try {
149
194
  return await requestPromise;
@@ -155,7 +200,14 @@ async function queryEndpoint(endpoint, params, options = {}) {
155
200
  }
156
201
  }
157
202
  function extractRecords(response) {
158
- return response.data.map((item) => item.data);
203
+ return response.data.map((item, index) => {
204
+ if (!isRecord(item) || !isRecord(item.data)) {
205
+ throw new Error(
206
+ `Unexpected FDIC API response shape: expected data[${index}] to contain an object 'data' property.`
207
+ );
208
+ }
209
+ return item.data;
210
+ });
159
211
  }
160
212
  function buildPaginationInfo(total, offset, count) {
161
213
  const has_more = total > offset + count;
@@ -167,6 +219,10 @@ function buildPaginationInfo(total, offset, count) {
167
219
  ...has_more ? { next_offset: offset + count } : {}
168
220
  };
169
221
  }
222
+ function buildTruncationWarning(label, total, count, guidance) {
223
+ if (total <= count) return void 0;
224
+ return `${label} truncated to ${count.toLocaleString()} records out of ${total.toLocaleString()} matched rows. ${guidance}`;
225
+ }
170
226
  function truncateIfNeeded(text, charLimit) {
171
227
  if (text.length <= charLimit) return text;
172
228
  return text.slice(0, charLimit) + `
@@ -1084,9 +1140,40 @@ Prefer concise human-readable summaries or tables when answering users. Structur
1084
1140
 
1085
1141
  // src/tools/analysis.ts
1086
1142
  var import_zod7 = require("zod");
1143
+
1144
+ // src/tools/shared/queryUtils.ts
1087
1145
  var CHUNK_SIZE = 25;
1088
1146
  var MAX_CONCURRENCY = 4;
1089
1147
  var ANALYSIS_TIMEOUT_MS = 9e4;
1148
+ function asNumber(value) {
1149
+ return typeof value === "number" ? value : null;
1150
+ }
1151
+ function buildCertFilters(certs) {
1152
+ const filters = [];
1153
+ for (let i = 0; i < certs.length; i += CHUNK_SIZE) {
1154
+ const chunk = certs.slice(i, i + CHUNK_SIZE);
1155
+ filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1156
+ }
1157
+ return filters;
1158
+ }
1159
+ async function mapWithConcurrency(values, limit, mapper) {
1160
+ const results = new Array(values.length);
1161
+ let nextIndex = 0;
1162
+ async function worker() {
1163
+ while (true) {
1164
+ const currentIndex = nextIndex;
1165
+ nextIndex += 1;
1166
+ if (currentIndex >= values.length) return;
1167
+ results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1168
+ }
1169
+ }
1170
+ await Promise.all(
1171
+ Array.from({ length: Math.min(limit, values.length) }, () => worker())
1172
+ );
1173
+ return results;
1174
+ }
1175
+
1176
+ // src/tools/analysis.ts
1090
1177
  var SortFieldSchema = import_zod7.z.enum([
1091
1178
  "asset_growth",
1092
1179
  "asset_growth_pct",
@@ -1135,32 +1222,9 @@ var SnapshotAnalysisSchema = import_zod7.z.object({
1135
1222
  });
1136
1223
  }
1137
1224
  });
1138
- function asNumber(value) {
1139
- return typeof value === "number" ? value : null;
1140
- }
1141
- function buildCertFilters(certs) {
1142
- const filters = [];
1143
- for (let i = 0; i < certs.length; i += CHUNK_SIZE) {
1144
- const chunk = certs.slice(i, i + CHUNK_SIZE);
1145
- filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1146
- }
1147
- return filters;
1148
- }
1149
- async function mapWithConcurrency(values, limit, mapper) {
1150
- const results = new Array(values.length);
1151
- let nextIndex = 0;
1152
- async function worker() {
1153
- while (true) {
1154
- const currentIndex = nextIndex;
1155
- nextIndex += 1;
1156
- if (currentIndex >= values.length) return;
1157
- results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1158
- }
1159
- }
1160
- await Promise.all(
1161
- Array.from({ length: Math.min(limit, values.length) }, () => worker())
1162
- );
1163
- return results;
1225
+ function maxOrNull(values) {
1226
+ const nonNullValues = values.filter((value) => value !== null);
1227
+ return nonNullValues.length > 0 ? Math.max(...nonNullValues) : null;
1164
1228
  }
1165
1229
  function ratio(numerator, denominator) {
1166
1230
  if (numerator === null || denominator === null || denominator === 0) {
@@ -1344,7 +1408,7 @@ function summarizeTimeSeries(records, demographicsByDate, institution) {
1344
1408
  const depositsPerOfficeEnd = ratio(depEnd, officesEnd);
1345
1409
  const depositsToAssetsStart = ratio(depStart, assetStart);
1346
1410
  const depositsToAssetsEnd = ratio(depEnd, assetEnd);
1347
- const peakAsset = Math.max(...assetSeries.filter((value) => value !== null));
1411
+ const peakAsset = maxOrNull(assetSeries);
1348
1412
  const troughRoaValues = roaSeries.filter((value) => value !== null);
1349
1413
  const troughRoa = troughRoaValues.length > 0 ? Math.min(...troughRoaValues) : null;
1350
1414
  const comparison = {
@@ -1467,29 +1531,46 @@ async function fetchBatchedRecordsForDates(endpoint, certs, repdteFilters, field
1467
1531
  certFilter
1468
1532
  }))
1469
1533
  );
1470
- const responses = await mapWithConcurrency(tasks, MAX_CONCURRENCY, async (task) => {
1471
- const response = await queryEndpoint(endpoint, {
1472
- filters: `(${task.certFilter}) AND ${task.repdteFilter}`,
1473
- fields,
1474
- limit: 1e4,
1475
- offset: 0,
1476
- sort_by: "CERT",
1477
- sort_order: "ASC"
1478
- }, { signal });
1479
- return { repdteFilter: task.repdteFilter, response };
1480
- });
1534
+ const responses = await mapWithConcurrency(
1535
+ tasks,
1536
+ MAX_CONCURRENCY,
1537
+ async (task) => {
1538
+ const response = await queryEndpoint(
1539
+ endpoint,
1540
+ {
1541
+ filters: `(${task.certFilter}) AND ${task.repdteFilter}`,
1542
+ fields,
1543
+ limit: 1e4,
1544
+ offset: 0,
1545
+ sort_by: "CERT",
1546
+ sort_order: "ASC"
1547
+ },
1548
+ { signal }
1549
+ );
1550
+ return { repdteFilter: task.repdteFilter, response };
1551
+ }
1552
+ );
1481
1553
  const byDate = /* @__PURE__ */ new Map();
1554
+ const warnings = /* @__PURE__ */ new Set();
1482
1555
  for (const { repdteFilter, response } of responses) {
1483
1556
  if (!byDate.has(repdteFilter)) {
1484
1557
  byDate.set(repdteFilter, /* @__PURE__ */ new Map());
1485
1558
  }
1559
+ const records = extractRecords(response);
1560
+ const warning = buildTruncationWarning(
1561
+ `${endpoint} batch for ${repdteFilter}`,
1562
+ response.meta.total,
1563
+ records.length,
1564
+ "Narrow the comparison set with institution_filters or certs for complete analysis."
1565
+ );
1566
+ if (warning) warnings.add(warning);
1486
1567
  const target = byDate.get(repdteFilter);
1487
- for (const record of extractRecords(response)) {
1568
+ for (const record of records) {
1488
1569
  const cert = asNumber(record.CERT);
1489
1570
  if (cert !== null) target.set(cert, record);
1490
1571
  }
1491
1572
  }
1492
- return byDate;
1573
+ return { byDate, warnings: [...warnings] };
1493
1574
  }
1494
1575
  async function fetchSeriesRecords(endpoint, certs, startRepdte, endRepdte, fields, signal) {
1495
1576
  const certFilters = buildCertFilters(certs);
@@ -1506,15 +1587,24 @@ async function fetchSeriesRecords(endpoint, certs, startRepdte, endRepdte, field
1506
1587
  }, { signal })
1507
1588
  );
1508
1589
  const grouped = /* @__PURE__ */ new Map();
1590
+ const warnings = /* @__PURE__ */ new Set();
1509
1591
  for (const response of responses) {
1510
- for (const record of extractRecords(response)) {
1592
+ const records = extractRecords(response);
1593
+ const warning = buildTruncationWarning(
1594
+ `${endpoint} batch for REPDTE:[${startRepdte} TO ${endRepdte}]`,
1595
+ response.meta.total,
1596
+ records.length,
1597
+ "Narrow the comparison set with certs or a shorter date range for complete analysis."
1598
+ );
1599
+ if (warning) warnings.add(warning);
1600
+ for (const record of records) {
1511
1601
  const cert = asNumber(record.CERT);
1512
1602
  if (cert === null) continue;
1513
1603
  if (!grouped.has(cert)) grouped.set(cert, []);
1514
1604
  grouped.get(cert).push(record);
1515
1605
  }
1516
1606
  }
1517
- return grouped;
1607
+ return { grouped, warnings: [...warnings] };
1518
1608
  }
1519
1609
  function buildSnapshotComparison(cert, institution, startFinancial, endFinancial, startDemo, endDemo, startRepdte, endRepdte) {
1520
1610
  const assetStart = asNumber(startFinancial.ASSET);
@@ -1671,8 +1761,9 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1671
1761
  )
1672
1762
  );
1673
1763
  let comparisons = [];
1764
+ const warnings = rosterResult.warning ? [rosterResult.warning] : [];
1674
1765
  if (analysis_mode === "timeseries") {
1675
- const [financialSeries, demographicsSeries] = await Promise.all([
1766
+ const [financialSeriesResult, demographicsSeriesResult] = await Promise.all([
1676
1767
  fetchSeriesRecords(
1677
1768
  ENDPOINTS.FINANCIALS,
1678
1769
  candidateCerts,
@@ -1688,8 +1779,17 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1688
1779
  end_repdte,
1689
1780
  "CERT,REPDTE,OFFTOT,OFFSTATE,CBSANAME",
1690
1781
  controller.signal
1691
- ) : Promise.resolve(/* @__PURE__ */ new Map())
1782
+ ) : Promise.resolve({
1783
+ grouped: /* @__PURE__ */ new Map(),
1784
+ warnings: []
1785
+ })
1692
1786
  ]);
1787
+ warnings.push(
1788
+ ...financialSeriesResult.warnings,
1789
+ ...demographicsSeriesResult.warnings
1790
+ );
1791
+ const financialSeries = financialSeriesResult.grouped;
1792
+ const demographicsSeries = demographicsSeriesResult.grouped;
1693
1793
  comparisons = candidateCerts.map(
1694
1794
  (cert) => summarizeTimeSeries(
1695
1795
  financialSeries.get(cert) ?? [],
@@ -1703,7 +1803,7 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1703
1803
  )
1704
1804
  ).filter((comparison) => comparison !== null);
1705
1805
  } else {
1706
- const [financialSnapshots, demographicSnapshots] = await Promise.all([
1806
+ const [financialSnapshotsResult, demographicSnapshotsResult] = await Promise.all([
1707
1807
  fetchBatchedRecordsForDates(
1708
1808
  ENDPOINTS.FINANCIALS,
1709
1809
  candidateCerts,
@@ -1717,8 +1817,17 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1717
1817
  [`REPDTE:${start_repdte}`, `REPDTE:${end_repdte}`],
1718
1818
  "CERT,REPDTE,OFFTOT,OFFSTATE,CBSANAME",
1719
1819
  controller.signal
1720
- ) : Promise.resolve(/* @__PURE__ */ new Map())
1820
+ ) : Promise.resolve({
1821
+ byDate: /* @__PURE__ */ new Map(),
1822
+ warnings: []
1823
+ })
1721
1824
  ]);
1825
+ warnings.push(
1826
+ ...financialSnapshotsResult.warnings,
1827
+ ...demographicSnapshotsResult.warnings
1828
+ );
1829
+ const financialSnapshots = financialSnapshotsResult.byDate;
1830
+ const demographicSnapshots = demographicSnapshotsResult.byDate;
1722
1831
  const startFinancials = financialSnapshots.get(`REPDTE:${start_repdte}`) ?? /* @__PURE__ */ new Map();
1723
1832
  const endFinancials = financialSnapshots.get(`REPDTE:${end_repdte}`) ?? /* @__PURE__ */ new Map();
1724
1833
  const startDemographics = demographicSnapshots.get(`REPDTE:${start_repdte}`) ?? /* @__PURE__ */ new Map();
@@ -1754,14 +1863,14 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1754
1863
  analysis_mode,
1755
1864
  sort_by,
1756
1865
  sort_order,
1757
- warnings: rosterResult.warning ? [rosterResult.warning] : [],
1866
+ warnings,
1758
1867
  insights: buildTopLevelInsights(sortedComparisons),
1759
1868
  ...pagination,
1760
1869
  comparisons: ranked
1761
1870
  };
1762
1871
  const text = truncateIfNeeded(
1763
1872
  [
1764
- rosterResult.warning ? `Warning: ${rosterResult.warning}` : null,
1873
+ ...warnings.map((warning) => `Warning: ${warning}`),
1765
1874
  formatComparisonText(output)
1766
1875
  ].filter((value) => value !== null).join("\n\n"),
1767
1876
  CHARACTER_LIMIT
@@ -1788,9 +1897,8 @@ Returns concise comparison text plus structured deltas, derived metrics, and ins
1788
1897
 
1789
1898
  // src/tools/peerGroup.ts
1790
1899
  var import_zod8 = require("zod");
1791
- function asNumber2(value) {
1792
- return typeof value === "number" ? value : null;
1793
- }
1900
+
1901
+ // src/tools/shared/financialMetrics.ts
1794
1902
  function safeRatio(numerator, denominator) {
1795
1903
  if (numerator === null || denominator === null || denominator === 0) {
1796
1904
  return null;
@@ -1804,14 +1912,14 @@ function safeRatioPositiveDenom(numerator, denominator) {
1804
1912
  return numerator / denominator;
1805
1913
  }
1806
1914
  function deriveMetrics(raw) {
1807
- const asset = asNumber2(raw.ASSET);
1808
- const dep = asNumber2(raw.DEP);
1809
- const eqtot = asNumber2(raw.EQTOT);
1810
- const lnlsnet = asNumber2(raw.LNLSNET);
1811
- const intinc = asNumber2(raw.INTINC);
1812
- const eintexp = asNumber2(raw.EINTEXP);
1813
- const nonii = asNumber2(raw.NONII);
1814
- const nonix = asNumber2(raw.NONIX);
1915
+ const asset = asNumber(raw.ASSET);
1916
+ const dep = asNumber(raw.DEP);
1917
+ const eqtot = asNumber(raw.EQTOT);
1918
+ const lnlsnet = asNumber(raw.LNLSNET);
1919
+ const intinc = asNumber(raw.INTINC);
1920
+ const eintexp = asNumber(raw.EINTEXP);
1921
+ const nonii = asNumber(raw.NONII);
1922
+ const nonix = asNumber(raw.NONIX);
1815
1923
  const netInterestIncome = intinc !== null && eintexp !== null ? intinc - eintexp : null;
1816
1924
  const revenueDenominator = netInterestIncome !== null && nonii !== null ? netInterestIncome + nonii : null;
1817
1925
  const equityRatioRaw = safeRatio(eqtot, asset);
@@ -1819,9 +1927,9 @@ function deriveMetrics(raw) {
1819
1927
  return {
1820
1928
  asset,
1821
1929
  dep,
1822
- roa: asNumber2(raw.ROA),
1823
- roe: asNumber2(raw.ROE),
1824
- netnim: asNumber2(raw.NETNIM),
1930
+ roa: asNumber(raw.ROA),
1931
+ roe: asNumber(raw.ROE),
1932
+ netnim: asNumber(raw.NETNIM),
1825
1933
  equity_ratio: equityRatioRaw !== null ? equityRatioRaw * 100 : null,
1826
1934
  efficiency_ratio: efficiencyRatioRaw !== null ? efficiencyRatioRaw * 100 : null,
1827
1935
  loan_to_deposit: safeRatio(lnlsnet, dep),
@@ -1836,6 +1944,8 @@ function computeMedian(values) {
1836
1944
  if (sorted.length % 2 === 1) return sorted[mid];
1837
1945
  return (sorted[mid - 1] + sorted[mid]) / 2;
1838
1946
  }
1947
+
1948
+ // src/tools/peerGroup.ts
1839
1949
  function computeCompetitionRank(subjectValue, peerValues, higherIsBetter) {
1840
1950
  if (peerValues.length === 0) return null;
1841
1951
  const ascending = higherIsBetter === false;
@@ -1850,7 +1960,7 @@ function computeCompetitionRank(subjectValue, peerValues, higherIsBetter) {
1850
1960
  }
1851
1961
  }
1852
1962
  const rank = ranks.get(subjectValue);
1853
- const of = peerValues.length;
1963
+ const of = all.length;
1854
1964
  const percentile = Math.round((1 - (rank - 1) / of) * 100);
1855
1965
  return { rank, of, percentile };
1856
1966
  }
@@ -1961,34 +2071,7 @@ var PeerGroupInputSchema = import_zod8.z.object({
1961
2071
  });
1962
2072
  }
1963
2073
  });
1964
- var CHUNK_SIZE2 = 25;
1965
- var MAX_CONCURRENCY2 = 4;
1966
- var ANALYSIS_TIMEOUT_MS2 = 9e4;
1967
2074
  var FINANCIAL_FIELDS = "CERT,ASSET,DEP,NETINC,ROA,ROE,NETNIM,EQTOT,LNLSNET,INTINC,EINTEXP,NONII,NONIX";
1968
- function buildCertFilters2(certs) {
1969
- const filters = [];
1970
- for (let i = 0; i < certs.length; i += CHUNK_SIZE2) {
1971
- const chunk = certs.slice(i, i + CHUNK_SIZE2);
1972
- filters.push(chunk.map((cert) => `CERT:${cert}`).join(" OR "));
1973
- }
1974
- return filters;
1975
- }
1976
- async function mapWithConcurrency2(values, limit, mapper) {
1977
- const results = new Array(values.length);
1978
- let nextIndex = 0;
1979
- async function worker() {
1980
- while (true) {
1981
- const currentIndex = nextIndex;
1982
- nextIndex += 1;
1983
- if (currentIndex >= values.length) return;
1984
- results[currentIndex] = await mapper(values[currentIndex], currentIndex);
1985
- }
1986
- }
1987
- await Promise.all(
1988
- Array.from({ length: Math.min(limit, values.length) }, () => worker())
1989
- );
1990
- return results;
1991
- }
1992
2075
  function formatMetricValue(key, value) {
1993
2076
  if (value === null) return "n/a";
1994
2077
  const def = METRIC_DEFINITIONS[key];
@@ -2071,7 +2154,7 @@ Metrics ranked (fixed order):
2071
2154
  - Equity Capital Ratio, Efficiency Ratio, Loan-to-Deposit Ratio
2072
2155
  - Deposits-to-Assets Ratio, Non-Interest Income Share
2073
2156
 
2074
- Rankings use competition rank (1, 2, 2, 4) with metric-specific denominators. Subject is excluded from peer set.
2157
+ Rankings use competition rank (1, 2, 2, 4). Rank, denominator, and percentile all use the same comparison set: matched peers plus the subject institution.
2075
2158
 
2076
2159
  Output includes:
2077
2160
  - Subject rankings and percentiles (when cert provided)
@@ -2090,7 +2173,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2090
2173
  },
2091
2174
  async (params) => {
2092
2175
  const controller = new AbortController();
2093
- const timeoutId = setTimeout(() => controller.abort(), ANALYSIS_TIMEOUT_MS2);
2176
+ const timeoutId = setTimeout(() => controller.abort(), ANALYSIS_TIMEOUT_MS);
2094
2177
  try {
2095
2178
  const warnings = [];
2096
2179
  let subjectProfile = null;
@@ -2175,7 +2258,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2175
2258
  }
2176
2259
  if (params.cert) {
2177
2260
  rosterRecords = rosterRecords.filter(
2178
- (r) => asNumber2(r.CERT) !== params.cert
2261
+ (r) => asNumber(r.CERT) !== params.cert
2179
2262
  );
2180
2263
  }
2181
2264
  const criteriaUsed = {
@@ -2227,12 +2310,12 @@ Override precedence: cert derives defaults, then explicit params override them.`
2227
2310
  structuredContent: output2
2228
2311
  };
2229
2312
  }
2230
- const peerCerts = rosterRecords.map((r) => asNumber2(r.CERT)).filter((c) => c !== null);
2231
- const certFilters = buildCertFilters2(peerCerts);
2313
+ const peerCerts = rosterRecords.map((r) => asNumber(r.CERT)).filter((c) => c !== null);
2314
+ const certFilters = buildCertFilters(peerCerts);
2232
2315
  const extraFieldsCsv = params.extra_fields && params.extra_fields.length > 0 ? "," + params.extra_fields.join(",") : "";
2233
- const financialResponses = await mapWithConcurrency2(
2316
+ const financialResponses = await mapWithConcurrency(
2234
2317
  certFilters,
2235
- MAX_CONCURRENCY2,
2318
+ MAX_CONCURRENCY,
2236
2319
  async (certFilter) => queryEndpoint(
2237
2320
  ENDPOINTS.FINANCIALS,
2238
2321
  {
@@ -2248,13 +2331,21 @@ Override precedence: cert derives defaults, then explicit params override them.`
2248
2331
  );
2249
2332
  const peerFinancialsByCert = /* @__PURE__ */ new Map();
2250
2333
  for (const response of financialResponses) {
2251
- for (const record of extractRecords(response)) {
2252
- const cert = asNumber2(record.CERT);
2334
+ const records = extractRecords(response);
2335
+ const warning = buildTruncationWarning(
2336
+ `financials batch for REPDTE:${params.repdte}`,
2337
+ response.meta.total,
2338
+ records.length,
2339
+ "Narrow the peer group criteria for complete analysis."
2340
+ );
2341
+ if (warning && !warnings.includes(warning)) warnings.push(warning);
2342
+ for (const record of records) {
2343
+ const cert = asNumber(record.CERT);
2253
2344
  if (cert !== null) peerFinancialsByCert.set(cert, record);
2254
2345
  }
2255
2346
  }
2256
2347
  const rosterByCert = new Map(
2257
- rosterRecords.map((r) => [asNumber2(r.CERT), r]).filter(
2348
+ rosterRecords.map((r) => [asNumber(r.CERT), r]).filter(
2258
2349
  (e) => e[0] !== null
2259
2350
  )
2260
2351
  );
@@ -2351,7 +2442,7 @@ Override precedence: cert derives defaults, then explicit params override them.`
2351
2442
  if (controller.signal.aborted) {
2352
2443
  return formatToolError(
2353
2444
  new Error(
2354
- `Peer group analysis timed out after ${Math.floor(ANALYSIS_TIMEOUT_MS2 / 1e3)} seconds. Narrow the peer group criteria and try again.`
2445
+ `Peer group analysis timed out after ${Math.floor(ANALYSIS_TIMEOUT_MS / 1e3)} seconds. Narrow the peer group criteria and try again.`
2355
2446
  )
2356
2447
  );
2357
2448
  }
@@ -2386,6 +2477,16 @@ async function runStdio() {
2386
2477
  await server.connect(transport);
2387
2478
  console.error("FDIC BankFind MCP server running on stdio");
2388
2479
  }
2480
+ function parseHttpPort(rawPort) {
2481
+ const port = Number.parseInt(rawPort ?? "3000", 10);
2482
+ if (Number.isNaN(port)) {
2483
+ throw new Error(`Invalid PORT value: ${rawPort ?? ""}`);
2484
+ }
2485
+ if (port < 0 || port > 65535) {
2486
+ throw new Error(`PORT must be between 0 and 65535. Received: ${port}`);
2487
+ }
2488
+ return port;
2489
+ }
2389
2490
  function createApp() {
2390
2491
  const app = (0, import_express.default)();
2391
2492
  app.use(import_express.default.json());
@@ -2429,7 +2530,7 @@ function createApp() {
2429
2530
  }
2430
2531
  async function runHTTP() {
2431
2532
  const app = createApp();
2432
- const port = parseInt(process.env.PORT ?? "3000");
2533
+ const port = parseHttpPort(process.env.PORT);
2433
2534
  app.listen(port, () => {
2434
2535
  console.error(
2435
2536
  `FDIC BankFind MCP server running on http://localhost:${port}/mcp`
@@ -2448,5 +2549,6 @@ async function main() {
2448
2549
  0 && (module.exports = {
2449
2550
  createApp,
2450
2551
  createServer,
2451
- main
2552
+ main,
2553
+ parseHttpPort
2452
2554
  });
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "fdic-mcp-server",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "MCP server for the FDIC BankFind Suite API",
5
+ "mcpName": "io.github.jflamb/fdic-mcp-server",
5
6
  "main": "dist/server.js",
6
7
  "files": [
7
8
  "dist",
@@ -20,6 +21,7 @@
20
21
  "prepack": "npm run build",
21
22
  "prepublishOnly": "npm run typecheck && npm test && npm run build",
22
23
  "pack:check": "npm pack --dry-run",
24
+ "registry:sync": "node scripts/sync-server-json.mjs",
23
25
  "start": "node dist/index.js",
24
26
  "dev": "ts-node src/index.ts",
25
27
  "deploy:local": "bash scripts/deploy-local.sh"