fdic-mcp-server 1.29.0 → 1.30.0

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 (3) hide show
  1. package/dist/index.js +554 -19
  2. package/dist/server.js +554 -19
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
32
32
  var import_express2 = __toESM(require("express"));
33
33
 
34
34
  // src/constants.ts
35
- var VERSION = true ? "1.29.0" : process.env.npm_package_version ?? "0.0.0-dev";
35
+ var VERSION = true ? "1.30.0" : process.env.npm_package_version ?? "0.0.0-dev";
36
36
  var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
37
37
  var CHARACTER_LIMIT = 5e4;
38
38
  var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
@@ -38735,8 +38735,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38735
38735
  );
38736
38736
  }
38737
38737
 
38738
- // src/tools/chatgptRetrieval.ts
38738
+ // src/tools/qbpLite.ts
38739
38739
  var import_zod21 = require("zod");
38740
+ var QBP_LITE_FETCH_LIMIT = 1e4;
38741
+ var QBP_LITE_FIELDS = [
38742
+ "CERT",
38743
+ "NAME",
38744
+ "REPDTE",
38745
+ "CB",
38746
+ "SPECGRP",
38747
+ "SPECGRPDESC",
38748
+ "ASSET",
38749
+ "DEP",
38750
+ "DEPDOM",
38751
+ "NETINC",
38752
+ "ROA",
38753
+ "ROE",
38754
+ "NIMY",
38755
+ "ERNAST",
38756
+ "LNLSNET",
38757
+ "LNRE",
38758
+ "LNCI",
38759
+ "LNCON",
38760
+ "LNCRCD",
38761
+ "LNAG",
38762
+ "SC",
38763
+ "EQTOT",
38764
+ "NCLNLSR",
38765
+ "NTLNLSR",
38766
+ "RBC",
38767
+ "RBC1AAJ",
38768
+ "IDT1RWAJR",
38769
+ "RBCT1",
38770
+ "RBCT1C",
38771
+ "RBCT1CER",
38772
+ "RBCRWAJ",
38773
+ "RWAJ",
38774
+ "RWAJT",
38775
+ "P3RER",
38776
+ "P3CIR",
38777
+ "P3CONR",
38778
+ "P3CRCDR",
38779
+ "P3AGR",
38780
+ "NTRER",
38781
+ "NTCIR",
38782
+ "NTCONR",
38783
+ "NTCRCDR",
38784
+ "NTAGR"
38785
+ ].join(",");
38786
+ var QbpLiteSchema = import_zod21.z.object({
38787
+ repdte: import_zod21.z.string().regex(/^\d{8}$/).optional().describe(
38788
+ "Quarter-end Report Date (REPDTE) in YYYYMMDD format. If omitted, the tool searches backward from the latest likely published quarter until data is found."
38789
+ ),
38790
+ trend_quarters: import_zod21.z.number().int().min(8).max(40).default(20).describe(
38791
+ "Number of quarterly observations to return for trend charts, including the current quarter. Default 20 quarters."
38792
+ ),
38793
+ include_community_banks: import_zod21.z.boolean().default(true).describe(
38794
+ "Include a compact community-bank-vs-industry comparison using the public community-bank flag."
38795
+ )
38796
+ });
38797
+ var EXECUTIVE_METRICS = [
38798
+ {
38799
+ id: "institution_count",
38800
+ label: "Number of institutions reporting",
38801
+ unit: "count"
38802
+ },
38803
+ { id: "total_assets", label: "Total assets", unit: "$thousands" },
38804
+ {
38805
+ id: "total_loans_and_leases",
38806
+ label: "Total loans and leases",
38807
+ unit: "$thousands"
38808
+ },
38809
+ { id: "domestic_deposits", label: "Domestic deposits", unit: "$thousands" },
38810
+ { id: "net_income", label: "Net income", unit: "$thousands" },
38811
+ { id: "roa_pct", label: "Return on assets", unit: "percent" },
38812
+ { id: "roe_pct", label: "Return on equity", unit: "percent" },
38813
+ { id: "net_interest_margin_pct", label: "Net interest margin", unit: "percent" },
38814
+ { id: "noncurrent_loans_pct", label: "Noncurrent loans to loans", unit: "percent" },
38815
+ { id: "net_chargeoffs_pct", label: "Net charge-offs to loans", unit: "percent" },
38816
+ { id: "leverage_ratio_pct", label: "Core capital (leverage) ratio", unit: "percent" }
38817
+ ];
38818
+ function sumField(records, field) {
38819
+ let total = 0;
38820
+ let seen = false;
38821
+ for (const record of records) {
38822
+ const value = asNumber(record[field]);
38823
+ if (value !== null) {
38824
+ total += value;
38825
+ seen = true;
38826
+ }
38827
+ }
38828
+ return seen ? total : null;
38829
+ }
38830
+ function weightedAverage(records, valueField, weightField) {
38831
+ let weightedSum = 0;
38832
+ let weightSum = 0;
38833
+ for (const record of records) {
38834
+ const value = asNumber(record[valueField]);
38835
+ const weight = asNumber(record[weightField]);
38836
+ if (value !== null && weight !== null && weight > 0) {
38837
+ weightedSum += value * weight;
38838
+ weightSum += weight;
38839
+ }
38840
+ }
38841
+ return weightSum > 0 ? weightedSum / weightSum : null;
38842
+ }
38843
+ function pctChange2(current, prior) {
38844
+ if (current === null || prior === null || prior === 0) return null;
38845
+ return (current - prior) / prior * 100;
38846
+ }
38847
+ function change2(current, prior) {
38848
+ if (current === null || prior === null) return null;
38849
+ return current - prior;
38850
+ }
38851
+ function dividePct(numerator, denominator) {
38852
+ if (numerator === null || denominator === null || denominator === 0) return null;
38853
+ return numerator / denominator * 100;
38854
+ }
38855
+ function sumRatioPct(records, numeratorField, denominatorField) {
38856
+ return dividePct(sumField(records, numeratorField), sumField(records, denominatorField));
38857
+ }
38858
+ function aggregateFinancialRecords(records) {
38859
+ const totalAssets = sumField(records, "ASSET");
38860
+ const totalLoans = sumField(records, "LNLSNET");
38861
+ const equityCapital = sumField(records, "EQTOT");
38862
+ return {
38863
+ institution_count: records.length,
38864
+ total_assets: totalAssets,
38865
+ total_loans_and_leases: totalLoans,
38866
+ domestic_deposits: sumField(records, "DEPDOM"),
38867
+ total_deposits: sumField(records, "DEP"),
38868
+ securities: sumField(records, "SC"),
38869
+ equity_capital: equityCapital,
38870
+ net_income: sumField(records, "NETINC"),
38871
+ roa_pct: weightedAverage(records, "ROA", "ASSET"),
38872
+ roe_pct: weightedAverage(records, "ROE", "EQTOT"),
38873
+ net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST"),
38874
+ noncurrent_loans_pct: weightedAverage(records, "NCLNLSR", "LNLSNET"),
38875
+ net_chargeoffs_pct: weightedAverage(records, "NTLNLSR", "LNLSNET"),
38876
+ leverage_ratio_pct: weightedAverage(records, "RBC1AAJ", "ASSET"),
38877
+ common_equity_tier1_ratio_pct: sumRatioPct(records, "RBCT1C", "RWAJ") ?? weightedAverage(records, "RBCT1CER", "RWAJ"),
38878
+ tier1_risk_based_ratio_pct: sumRatioPct(records, "RBCT1", "RWAJ") ?? weightedAverage(records, "IDT1RWAJR", "RWAJ"),
38879
+ total_risk_based_ratio_pct: sumRatioPct(records, "RBC", "RWAJT") ?? weightedAverage(records, "RBCRWAJ", "RWAJT")
38880
+ };
38881
+ }
38882
+ function buildExecutiveSnapshot(current, priorQuarter, yearAgo) {
38883
+ return EXECUTIVE_METRICS.map(({ id, label, unit }) => {
38884
+ const currentValue = current[id];
38885
+ const priorValue = priorQuarter?.[id] ?? null;
38886
+ const yearAgoValue = yearAgo?.[id] ?? null;
38887
+ return {
38888
+ id,
38889
+ label,
38890
+ unit,
38891
+ change_unit: unit === "percent" ? "percentage_points" : unit,
38892
+ current: currentValue,
38893
+ prior_quarter_change: change2(currentValue, priorValue),
38894
+ prior_quarter_change_pct: unit === "percent" ? null : pctChange2(currentValue, priorValue),
38895
+ year_over_year_change: change2(currentValue, yearAgoValue),
38896
+ year_over_year_change_pct: unit === "percent" ? null : pctChange2(currentValue, yearAgoValue)
38897
+ };
38898
+ });
38899
+ }
38900
+ function isCommunityBank(record) {
38901
+ const value = record.CB;
38902
+ return value === 1 || value === "1";
38903
+ }
38904
+ function assetSizeGroup(asset) {
38905
+ if (asset === null) return "Unknown";
38906
+ if (asset < 1e5) return "Assets < $100 Million";
38907
+ if (asset < 1e6) return "Assets $100 Million - $1 Billion";
38908
+ if (asset < 1e7) return "Assets $1 Billion - $10 Billion";
38909
+ if (asset < 25e7) return "Assets $10 Billion - $250 Billion";
38910
+ return "Assets > $250 Billion";
38911
+ }
38912
+ function buildSeriesPoint(repdte, records) {
38913
+ const metrics = aggregateFinancialRecords(records);
38914
+ return {
38915
+ repdte,
38916
+ ...metrics
38917
+ };
38918
+ }
38919
+ function buildAssetSizeNimSeries(groupedRecords) {
38920
+ const rows = [];
38921
+ for (const [repdte, records] of groupedRecords) {
38922
+ const groups = /* @__PURE__ */ new Map();
38923
+ for (const record of records) {
38924
+ const group = assetSizeGroup(asNumber(record.ASSET));
38925
+ const groupRecords = groups.get(group);
38926
+ if (groupRecords) {
38927
+ groupRecords.push(record);
38928
+ } else {
38929
+ groups.set(group, [record]);
38930
+ }
38931
+ }
38932
+ rows.push({
38933
+ repdte,
38934
+ group: "Industry",
38935
+ institution_count: records.length,
38936
+ total_assets: sumField(records, "ASSET"),
38937
+ net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST")
38938
+ });
38939
+ for (const [group, groupRecords] of groups) {
38940
+ rows.push({
38941
+ repdte,
38942
+ group,
38943
+ institution_count: groupRecords.length,
38944
+ total_assets: sumField(groupRecords, "ASSET"),
38945
+ net_interest_margin_pct: weightedAverage(groupRecords, "NIMY", "ERNAST")
38946
+ });
38947
+ }
38948
+ }
38949
+ return rows;
38950
+ }
38951
+ function buildLoanAndDepositSeries(series) {
38952
+ const byDate = new Map(series.map((point) => [point.repdte, point]));
38953
+ return series.map((point) => {
38954
+ const priorQuarter = getPriorQuarterDates(point.repdte, 1)[0];
38955
+ const yearAgo = getReportDateOneYearPrior(point.repdte);
38956
+ const priorPoint = byDate.get(priorQuarter);
38957
+ const yearAgoPoint = byDate.get(yearAgo);
38958
+ return {
38959
+ repdte: point.repdte,
38960
+ total_loans_and_leases: point.total_loans_and_leases,
38961
+ quarterly_loan_change: change2(
38962
+ point.total_loans_and_leases,
38963
+ priorPoint?.total_loans_and_leases ?? null
38964
+ ),
38965
+ loan_growth_12_month_pct: pctChange2(
38966
+ point.total_loans_and_leases,
38967
+ yearAgoPoint?.total_loans_and_leases ?? null
38968
+ ),
38969
+ domestic_deposits: point.domestic_deposits,
38970
+ quarterly_domestic_deposit_change: change2(
38971
+ point.domestic_deposits,
38972
+ priorPoint?.domestic_deposits ?? null
38973
+ ),
38974
+ domestic_deposit_growth_12_month_pct: pctChange2(
38975
+ point.domestic_deposits,
38976
+ yearAgoPoint?.domestic_deposits ?? null
38977
+ )
38978
+ };
38979
+ });
38980
+ }
38981
+ function buildPortfolioPerformance(records) {
38982
+ const portfolios = [
38983
+ {
38984
+ id: "real_estate",
38985
+ label: "Real estate loans",
38986
+ balanceField: "LNRE",
38987
+ noncurrentRateField: "P3RER",
38988
+ chargeoffRateField: "NTRER"
38989
+ },
38990
+ {
38991
+ id: "commercial_industrial",
38992
+ label: "Commercial and industrial loans",
38993
+ balanceField: "LNCI",
38994
+ noncurrentRateField: "P3CIR",
38995
+ chargeoffRateField: "NTCIR"
38996
+ },
38997
+ {
38998
+ id: "consumer",
38999
+ label: "Consumer loans",
39000
+ balanceField: "LNCON",
39001
+ noncurrentRateField: "P3CONR",
39002
+ chargeoffRateField: "NTCONR"
39003
+ },
39004
+ {
39005
+ id: "credit_card",
39006
+ label: "Credit card loans",
39007
+ balanceField: "LNCRCD",
39008
+ noncurrentRateField: "P3CRCDR",
39009
+ chargeoffRateField: "NTCRCDR"
39010
+ },
39011
+ {
39012
+ id: "farm",
39013
+ label: "Farm loans",
39014
+ balanceField: "LNAG",
39015
+ noncurrentRateField: "P3AGR",
39016
+ chargeoffRateField: "NTAGR"
39017
+ }
39018
+ ];
39019
+ return portfolios.map((portfolio) => ({
39020
+ id: portfolio.id,
39021
+ label: portfolio.label,
39022
+ balance: sumField(records, portfolio.balanceField),
39023
+ balance_share_of_total_loans_pct: dividePct(
39024
+ sumField(records, portfolio.balanceField),
39025
+ sumField(records, "LNLSNET")
39026
+ ),
39027
+ noncurrent_rate_pct: weightedAverage(
39028
+ records,
39029
+ portfolio.noncurrentRateField,
39030
+ portfolio.balanceField
39031
+ ),
39032
+ net_chargeoff_rate_pct: weightedAverage(
39033
+ records,
39034
+ portfolio.chargeoffRateField,
39035
+ portfolio.balanceField
39036
+ )
39037
+ }));
39038
+ }
39039
+ function buildCommunityComparison(groupedRecords) {
39040
+ const rows = [];
39041
+ for (const [repdte, records] of groupedRecords) {
39042
+ for (const [group, groupRecords] of [
39043
+ ["Industry", records],
39044
+ ["Community Banks", records.filter(isCommunityBank)]
39045
+ ]) {
39046
+ const metrics = aggregateFinancialRecords(groupRecords);
39047
+ rows.push({
39048
+ repdte,
39049
+ group,
39050
+ institution_count: metrics.institution_count,
39051
+ total_assets: metrics.total_assets,
39052
+ total_loans_and_leases: metrics.total_loans_and_leases,
39053
+ domestic_deposits: metrics.domestic_deposits,
39054
+ net_income: metrics.net_income,
39055
+ roa_pct: metrics.roa_pct,
39056
+ net_interest_margin_pct: metrics.net_interest_margin_pct,
39057
+ noncurrent_loans_pct: metrics.noncurrent_loans_pct,
39058
+ net_chargeoffs_pct: metrics.net_chargeoffs_pct
39059
+ });
39060
+ }
39061
+ }
39062
+ return rows;
39063
+ }
39064
+ function buildTruncationWarning2(repdte, returnedCount, totalCount, limit = QBP_LITE_FETCH_LIMIT) {
39065
+ if (returnedCount >= limit && totalCount > limit) {
39066
+ return `REPDTE ${repdte}: results truncated at ${limit.toLocaleString()} of ${totalCount.toLocaleString()} institutions.`;
39067
+ }
39068
+ return null;
39069
+ }
39070
+ async function fetchRecordsForRepdte(repdte) {
39071
+ const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
39072
+ filters: `REPDTE:${repdte}`,
39073
+ fields: QBP_LITE_FIELDS,
39074
+ limit: QBP_LITE_FETCH_LIMIT,
39075
+ sort_by: "CERT",
39076
+ sort_order: "ASC"
39077
+ });
39078
+ const records = extractRecords(response);
39079
+ return {
39080
+ records,
39081
+ total: response.meta.total,
39082
+ truncationWarning: buildTruncationWarning2(
39083
+ repdte,
39084
+ records.length,
39085
+ response.meta.total
39086
+ )
39087
+ };
39088
+ }
39089
+ async function hasFinancialData(repdte) {
39090
+ const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
39091
+ filters: `REPDTE:${repdte}`,
39092
+ fields: "CERT,REPDTE",
39093
+ limit: 1
39094
+ });
39095
+ return response.meta.total > 0;
39096
+ }
39097
+ async function resolveReportDate(params) {
39098
+ if (params.repdte) {
39099
+ const validationError = validateQuarterEndDate(params.repdte, "repdte");
39100
+ if (validationError) {
39101
+ throw new Error(validationError);
39102
+ }
39103
+ return params.repdte;
39104
+ }
39105
+ const candidates = [getDefaultReportDate(), ...getPriorQuarterDates(getDefaultReportDate(), 7)];
39106
+ for (const candidate of candidates) {
39107
+ if (await hasFinancialData(candidate)) {
39108
+ return candidate;
39109
+ }
39110
+ }
39111
+ throw new Error("No BankFind financial data found in the latest eight expected reporting quarters.");
39112
+ }
39113
+ async function buildQbpLiteData(params) {
39114
+ const trendQuarters = params.trend_quarters ?? 20;
39115
+ const includeCommunityBanks = params.include_community_banks ?? true;
39116
+ const repdte = await resolveReportDate(params);
39117
+ const priorQuarterRepdte = getPriorQuarterDates(repdte, 1)[0];
39118
+ const yearAgoRepdte = getReportDateOneYearPrior(repdte);
39119
+ const trendDates = [...getPriorQuarterDates(repdte, trendQuarters - 1)].reverse();
39120
+ const requiredDates = [.../* @__PURE__ */ new Set([...trendDates, repdte, priorQuarterRepdte, yearAgoRepdte])].sort();
39121
+ const recordsByDate = /* @__PURE__ */ new Map();
39122
+ const warnings = [];
39123
+ await mapWithConcurrency(requiredDates, 4, async (date) => {
39124
+ const { records, truncationWarning } = await fetchRecordsForRepdte(date);
39125
+ recordsByDate.set(date, records);
39126
+ if (records.length === 0) {
39127
+ warnings.push(`No financial records found for REPDTE ${date}.`);
39128
+ }
39129
+ if (truncationWarning) {
39130
+ warnings.push(truncationWarning);
39131
+ }
39132
+ });
39133
+ const currentRecords = recordsByDate.get(repdte) ?? [];
39134
+ if (currentRecords.length === 0) {
39135
+ throw new Error(`No financial records found for current REPDTE ${repdte}.`);
39136
+ }
39137
+ const trendRecords = new Map(
39138
+ [...trendDates, repdte].filter((date) => (recordsByDate.get(date)?.length ?? 0) > 0).map((date) => [date, recordsByDate.get(date) ?? []])
39139
+ );
39140
+ const trendSeries = [...trendRecords.entries()].map(
39141
+ ([date, records]) => buildSeriesPoint(date, records)
39142
+ );
39143
+ const current = aggregateFinancialRecords(currentRecords);
39144
+ const priorQuarter = recordsByDate.has(priorQuarterRepdte) ? aggregateFinancialRecords(recordsByDate.get(priorQuarterRepdte) ?? []) : null;
39145
+ const yearAgo = recordsByDate.has(yearAgoRepdte) ? aggregateFinancialRecords(recordsByDate.get(yearAgoRepdte) ?? []) : null;
39146
+ return {
39147
+ report: {
39148
+ title: "QBP Lite: FDIC-Insured Institutions",
39149
+ repdte,
39150
+ prior_quarter_repdte: priorQuarterRepdte,
39151
+ year_ago_repdte: yearAgoRepdte,
39152
+ trend_start_repdte: trendSeries[0]?.repdte ?? repdte,
39153
+ trend_end_repdte: repdte
39154
+ },
39155
+ executive_snapshot: buildExecutiveSnapshot(current, priorQuarter, yearAgo),
39156
+ charts: {
39157
+ quarterly_net_income_and_roa: trendSeries.map((point) => ({
39158
+ repdte: point.repdte,
39159
+ net_income: point.net_income,
39160
+ roa_pct: point.roa_pct
39161
+ })),
39162
+ net_interest_margin_by_asset_size: buildAssetSizeNimSeries(trendRecords),
39163
+ loans_and_deposits: buildLoanAndDepositSeries(trendSeries),
39164
+ credit_quality: trendSeries.map((point) => ({
39165
+ repdte: point.repdte,
39166
+ noncurrent_loans_pct: point.noncurrent_loans_pct,
39167
+ net_chargeoffs_pct: point.net_chargeoffs_pct
39168
+ })),
39169
+ loan_performance_by_portfolio: buildPortfolioPerformance(currentRecords),
39170
+ capital_ratios: trendSeries.map((point) => ({
39171
+ repdte: point.repdte,
39172
+ leverage_ratio_pct: point.leverage_ratio_pct,
39173
+ common_equity_tier1_ratio_pct: point.common_equity_tier1_ratio_pct,
39174
+ tier1_risk_based_ratio_pct: point.tier1_risk_based_ratio_pct,
39175
+ total_risk_based_ratio_pct: point.total_risk_based_ratio_pct
39176
+ })),
39177
+ community_banks_vs_industry: includeCommunityBanks ? buildCommunityComparison(trendRecords) : []
39178
+ },
39179
+ data_notes: {
39180
+ source_dataset: "FDIC BankFind financials endpoint",
39181
+ date_field: "REPDTE",
39182
+ dollar_units: "Thousands of dollars unless converted by the client.",
39183
+ aggregation_notes: [
39184
+ "Dollar fields are summed across reporting institutions.",
39185
+ "ROA, ROE, credit-quality ratios, and leverage ratio are weighted using the closest available public denominator field.",
39186
+ "Leverage ratio is asset-weighted using period-end ASSET as a proxy for average assets because the average-asset denominator is not exposed as a separate public BankFind field.",
39187
+ "Net interest margin is weighted by earning assets.",
39188
+ "Risk-based capital ratios are calculated from summed public capital-dollar and risk-weighted-asset fields when available, with public ratio fields used only as a fallback.",
39189
+ "The report uses public quarterly Call Report-derived BankFind data; it is not an official FDIC QBP publication."
39190
+ ],
39191
+ known_exclusions: [
39192
+ "Official Problem Bank List counts are excluded because they depend on confidential CAMELS ratings.",
39193
+ "Deposit Insurance Fund balance, reserve ratio, assessments earned, and fund income or expense are excluded because they are DIF accounting data rather than BankFind institution financials.",
39194
+ "Assessment-rate distribution data is excluded because public BankFind financials do not provide institution assessment-rate ranges.",
39195
+ "Community-bank merger-adjusted prior-period series are excluded; community-bank comparisons use the public CB flag available in BankFind records."
39196
+ ],
39197
+ warnings
39198
+ }
39199
+ };
39200
+ }
39201
+ function formatMetricValue2(value, unit) {
39202
+ if (value === null) return "n/a";
39203
+ if (unit === "percent") return `${value.toFixed(2)}%`;
39204
+ return Math.round(value).toLocaleString();
39205
+ }
39206
+ function formatChangeValue(value, unit) {
39207
+ if (value === null) return "n/a";
39208
+ if (unit === "percentage_points") return `${value.toFixed(2)} ppts`;
39209
+ return Math.round(value).toLocaleString();
39210
+ }
39211
+ function formatQbpLiteText(data) {
39212
+ const lines = [
39213
+ `${data.report.title}`,
39214
+ `Quarter ended: ${data.report.repdte}`,
39215
+ `Prior quarter: ${data.report.prior_quarter_repdte} | Year ago: ${data.report.year_ago_repdte}`,
39216
+ "",
39217
+ "Executive Snapshot"
39218
+ ];
39219
+ for (const metric of data.executive_snapshot) {
39220
+ const current = formatMetricValue2(metric.current, metric.unit);
39221
+ const qoq = formatChangeValue(
39222
+ metric.prior_quarter_change,
39223
+ metric.change_unit
39224
+ );
39225
+ const yoy = formatChangeValue(
39226
+ metric.year_over_year_change,
39227
+ metric.change_unit
39228
+ );
39229
+ lines.push(`- ${metric.label}: ${current}; QoQ change ${qoq}; YoY change ${yoy}`);
39230
+ }
39231
+ lines.push(
39232
+ "",
39233
+ "Chart-ready datasets included: quarterly_net_income_and_roa, net_interest_margin_by_asset_size, loans_and_deposits, credit_quality, loan_performance_by_portfolio, capital_ratios, community_banks_vs_industry.",
39234
+ "",
39235
+ "Known exclusions: official Problem Bank List counts, DIF accounting, assessment-rate distributions, and merger-adjusted community-bank prior-period series."
39236
+ );
39237
+ return truncateIfNeeded(
39238
+ lines.join("\n"),
39239
+ CHARACTER_LIMIT,
39240
+ "Use structuredContent for the complete QBP Lite dataset."
39241
+ );
39242
+ }
39243
+ function registerQbpLiteTools(server) {
39244
+ server.registerTool(
39245
+ "fdic_qbp_lite_data",
39246
+ {
39247
+ title: "Generate QBP Lite Data Bundle",
39248
+ description: "Build chart-ready data for a concise QBP Lite report from reproducible public BankFind quarterly financials. Includes executive snapshot metrics, trend series, community-bank comparison data, source notes, and explicit exclusions for non-public or non-BankFind QBP items.",
39249
+ inputSchema: QbpLiteSchema,
39250
+ outputSchema: FdicAnalysisOutputSchema,
39251
+ annotations: {
39252
+ readOnlyHint: true,
39253
+ destructiveHint: false,
39254
+ idempotentHint: true,
39255
+ openWorldHint: true
39256
+ }
39257
+ },
39258
+ async (params) => {
39259
+ try {
39260
+ const data = await buildQbpLiteData(params);
39261
+ return {
39262
+ content: [{ type: "text", text: formatQbpLiteText(data) }],
39263
+ structuredContent: data
39264
+ };
39265
+ } catch (err) {
39266
+ return formatToolError(err);
39267
+ }
39268
+ }
39269
+ );
39270
+ }
39271
+
39272
+ // src/tools/chatgptRetrieval.ts
39273
+ var import_zod22 = require("zod");
38740
39274
 
38741
39275
  // src/tools/shared/chatgptUrls.ts
38742
39276
  var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
@@ -38756,11 +39290,11 @@ function getBranchCitationUrl() {
38756
39290
  }
38757
39291
 
38758
39292
  // src/tools/chatgptRetrieval.ts
38759
- var SearchInputSchema = import_zod21.z.object({
38760
- query: import_zod21.z.string().min(1).describe("Natural-language search query.")
39293
+ var SearchInputSchema = import_zod22.z.object({
39294
+ query: import_zod22.z.string().min(1).describe("Natural-language search query.")
38761
39295
  });
38762
- var FetchInputSchema = import_zod21.z.object({
38763
- id: import_zod21.z.string().min(1).describe(
39296
+ var FetchInputSchema = import_zod22.z.object({
39297
+ id: import_zod22.z.string().min(1).describe(
38764
39298
  "Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
38765
39299
  )
38766
39300
  });
@@ -39198,7 +39732,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
39198
39732
  }
39199
39733
 
39200
39734
  // src/tools/chatgptBankDeepDive.ts
39201
- var import_zod22 = require("zod");
39735
+ var import_zod23 = require("zod");
39202
39736
 
39203
39737
  // src/resources/chatgptAppResources.ts
39204
39738
  var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
@@ -39459,9 +39993,9 @@ function registerChatGptAppResources(server) {
39459
39993
  }
39460
39994
 
39461
39995
  // src/tools/chatgptBankDeepDive.ts
39462
- var BankDeepDiveInputSchema = import_zod22.z.object({
39463
- cert: import_zod22.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39464
- repdte: import_zod22.z.string().regex(/^\d{8}$/).optional().describe(
39996
+ var BankDeepDiveInputSchema = import_zod23.z.object({
39997
+ cert: import_zod23.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39998
+ repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
39465
39999
  "Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
39466
40000
  )
39467
40001
  });
@@ -39760,28 +40294,28 @@ function registerSchemaResources(server) {
39760
40294
  }
39761
40295
 
39762
40296
  // src/prompts/workflows.ts
39763
- var import_zod23 = require("zod");
40297
+ var import_zod24 = require("zod");
39764
40298
  var BankDeepDiveArgs = {
39765
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39766
- repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
40299
+ bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
40300
+ repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe(
39767
40301
  "Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
39768
40302
  )
39769
40303
  };
39770
40304
  var FailureForensicsArgs = {
39771
- bank: import_zod23.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
39772
- lookback_quarters: import_zod23.z.string().regex(/^\d+$/).optional().describe(
40305
+ bank: import_zod24.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
40306
+ lookback_quarters: import_zod24.z.string().regex(/^\d+$/).optional().describe(
39773
40307
  "Number of pre-failure quarters to reconstruct (default 12 if omitted)."
39774
40308
  )
39775
40309
  };
39776
40310
  var PortfolioSurveillanceArgs = {
39777
- scope: import_zod23.z.string().min(1).describe(
40311
+ scope: import_zod24.z.string().min(1).describe(
39778
40312
  "Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
39779
40313
  ),
39780
- repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
40314
+ repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
39781
40315
  };
39782
40316
  var ExaminerOverlayArgs = {
39783
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39784
- qualitative_notes: import_zod23.z.string().optional().describe(
40317
+ bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
40318
+ qualitative_notes: import_zod24.z.string().optional().describe(
39785
40319
  "Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
39786
40320
  )
39787
40321
  };
@@ -39954,6 +40488,7 @@ function createServer(options = {}) {
39954
40488
  registerFranchiseFootprintTools(server);
39955
40489
  registerHoldingCompanyProfileTools(server);
39956
40490
  registerRegionalContextTools(server);
40491
+ registerQbpLiteTools(server);
39957
40492
  }
39958
40493
  if (profile.chatgptCanonical || profile.chatgptAliases) {
39959
40494
  registerChatGptRetrievalTools(server, {
package/dist/server.js CHANGED
@@ -47,7 +47,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
47
47
  var import_express2 = __toESM(require("express"));
48
48
 
49
49
  // src/constants.ts
50
- var VERSION = true ? "1.29.0" : process.env.npm_package_version ?? "0.0.0-dev";
50
+ var VERSION = true ? "1.30.0" : process.env.npm_package_version ?? "0.0.0-dev";
51
51
  var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
52
52
  var CHARACTER_LIMIT = 5e4;
53
53
  var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
@@ -38750,8 +38750,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38750
38750
  );
38751
38751
  }
38752
38752
 
38753
- // src/tools/chatgptRetrieval.ts
38753
+ // src/tools/qbpLite.ts
38754
38754
  var import_zod21 = require("zod");
38755
+ var QBP_LITE_FETCH_LIMIT = 1e4;
38756
+ var QBP_LITE_FIELDS = [
38757
+ "CERT",
38758
+ "NAME",
38759
+ "REPDTE",
38760
+ "CB",
38761
+ "SPECGRP",
38762
+ "SPECGRPDESC",
38763
+ "ASSET",
38764
+ "DEP",
38765
+ "DEPDOM",
38766
+ "NETINC",
38767
+ "ROA",
38768
+ "ROE",
38769
+ "NIMY",
38770
+ "ERNAST",
38771
+ "LNLSNET",
38772
+ "LNRE",
38773
+ "LNCI",
38774
+ "LNCON",
38775
+ "LNCRCD",
38776
+ "LNAG",
38777
+ "SC",
38778
+ "EQTOT",
38779
+ "NCLNLSR",
38780
+ "NTLNLSR",
38781
+ "RBC",
38782
+ "RBC1AAJ",
38783
+ "IDT1RWAJR",
38784
+ "RBCT1",
38785
+ "RBCT1C",
38786
+ "RBCT1CER",
38787
+ "RBCRWAJ",
38788
+ "RWAJ",
38789
+ "RWAJT",
38790
+ "P3RER",
38791
+ "P3CIR",
38792
+ "P3CONR",
38793
+ "P3CRCDR",
38794
+ "P3AGR",
38795
+ "NTRER",
38796
+ "NTCIR",
38797
+ "NTCONR",
38798
+ "NTCRCDR",
38799
+ "NTAGR"
38800
+ ].join(",");
38801
+ var QbpLiteSchema = import_zod21.z.object({
38802
+ repdte: import_zod21.z.string().regex(/^\d{8}$/).optional().describe(
38803
+ "Quarter-end Report Date (REPDTE) in YYYYMMDD format. If omitted, the tool searches backward from the latest likely published quarter until data is found."
38804
+ ),
38805
+ trend_quarters: import_zod21.z.number().int().min(8).max(40).default(20).describe(
38806
+ "Number of quarterly observations to return for trend charts, including the current quarter. Default 20 quarters."
38807
+ ),
38808
+ include_community_banks: import_zod21.z.boolean().default(true).describe(
38809
+ "Include a compact community-bank-vs-industry comparison using the public community-bank flag."
38810
+ )
38811
+ });
38812
+ var EXECUTIVE_METRICS = [
38813
+ {
38814
+ id: "institution_count",
38815
+ label: "Number of institutions reporting",
38816
+ unit: "count"
38817
+ },
38818
+ { id: "total_assets", label: "Total assets", unit: "$thousands" },
38819
+ {
38820
+ id: "total_loans_and_leases",
38821
+ label: "Total loans and leases",
38822
+ unit: "$thousands"
38823
+ },
38824
+ { id: "domestic_deposits", label: "Domestic deposits", unit: "$thousands" },
38825
+ { id: "net_income", label: "Net income", unit: "$thousands" },
38826
+ { id: "roa_pct", label: "Return on assets", unit: "percent" },
38827
+ { id: "roe_pct", label: "Return on equity", unit: "percent" },
38828
+ { id: "net_interest_margin_pct", label: "Net interest margin", unit: "percent" },
38829
+ { id: "noncurrent_loans_pct", label: "Noncurrent loans to loans", unit: "percent" },
38830
+ { id: "net_chargeoffs_pct", label: "Net charge-offs to loans", unit: "percent" },
38831
+ { id: "leverage_ratio_pct", label: "Core capital (leverage) ratio", unit: "percent" }
38832
+ ];
38833
+ function sumField(records, field) {
38834
+ let total = 0;
38835
+ let seen = false;
38836
+ for (const record of records) {
38837
+ const value = asNumber(record[field]);
38838
+ if (value !== null) {
38839
+ total += value;
38840
+ seen = true;
38841
+ }
38842
+ }
38843
+ return seen ? total : null;
38844
+ }
38845
+ function weightedAverage(records, valueField, weightField) {
38846
+ let weightedSum = 0;
38847
+ let weightSum = 0;
38848
+ for (const record of records) {
38849
+ const value = asNumber(record[valueField]);
38850
+ const weight = asNumber(record[weightField]);
38851
+ if (value !== null && weight !== null && weight > 0) {
38852
+ weightedSum += value * weight;
38853
+ weightSum += weight;
38854
+ }
38855
+ }
38856
+ return weightSum > 0 ? weightedSum / weightSum : null;
38857
+ }
38858
+ function pctChange2(current, prior) {
38859
+ if (current === null || prior === null || prior === 0) return null;
38860
+ return (current - prior) / prior * 100;
38861
+ }
38862
+ function change2(current, prior) {
38863
+ if (current === null || prior === null) return null;
38864
+ return current - prior;
38865
+ }
38866
+ function dividePct(numerator, denominator) {
38867
+ if (numerator === null || denominator === null || denominator === 0) return null;
38868
+ return numerator / denominator * 100;
38869
+ }
38870
+ function sumRatioPct(records, numeratorField, denominatorField) {
38871
+ return dividePct(sumField(records, numeratorField), sumField(records, denominatorField));
38872
+ }
38873
+ function aggregateFinancialRecords(records) {
38874
+ const totalAssets = sumField(records, "ASSET");
38875
+ const totalLoans = sumField(records, "LNLSNET");
38876
+ const equityCapital = sumField(records, "EQTOT");
38877
+ return {
38878
+ institution_count: records.length,
38879
+ total_assets: totalAssets,
38880
+ total_loans_and_leases: totalLoans,
38881
+ domestic_deposits: sumField(records, "DEPDOM"),
38882
+ total_deposits: sumField(records, "DEP"),
38883
+ securities: sumField(records, "SC"),
38884
+ equity_capital: equityCapital,
38885
+ net_income: sumField(records, "NETINC"),
38886
+ roa_pct: weightedAverage(records, "ROA", "ASSET"),
38887
+ roe_pct: weightedAverage(records, "ROE", "EQTOT"),
38888
+ net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST"),
38889
+ noncurrent_loans_pct: weightedAverage(records, "NCLNLSR", "LNLSNET"),
38890
+ net_chargeoffs_pct: weightedAverage(records, "NTLNLSR", "LNLSNET"),
38891
+ leverage_ratio_pct: weightedAverage(records, "RBC1AAJ", "ASSET"),
38892
+ common_equity_tier1_ratio_pct: sumRatioPct(records, "RBCT1C", "RWAJ") ?? weightedAverage(records, "RBCT1CER", "RWAJ"),
38893
+ tier1_risk_based_ratio_pct: sumRatioPct(records, "RBCT1", "RWAJ") ?? weightedAverage(records, "IDT1RWAJR", "RWAJ"),
38894
+ total_risk_based_ratio_pct: sumRatioPct(records, "RBC", "RWAJT") ?? weightedAverage(records, "RBCRWAJ", "RWAJT")
38895
+ };
38896
+ }
38897
+ function buildExecutiveSnapshot(current, priorQuarter, yearAgo) {
38898
+ return EXECUTIVE_METRICS.map(({ id, label, unit }) => {
38899
+ const currentValue = current[id];
38900
+ const priorValue = priorQuarter?.[id] ?? null;
38901
+ const yearAgoValue = yearAgo?.[id] ?? null;
38902
+ return {
38903
+ id,
38904
+ label,
38905
+ unit,
38906
+ change_unit: unit === "percent" ? "percentage_points" : unit,
38907
+ current: currentValue,
38908
+ prior_quarter_change: change2(currentValue, priorValue),
38909
+ prior_quarter_change_pct: unit === "percent" ? null : pctChange2(currentValue, priorValue),
38910
+ year_over_year_change: change2(currentValue, yearAgoValue),
38911
+ year_over_year_change_pct: unit === "percent" ? null : pctChange2(currentValue, yearAgoValue)
38912
+ };
38913
+ });
38914
+ }
38915
+ function isCommunityBank(record) {
38916
+ const value = record.CB;
38917
+ return value === 1 || value === "1";
38918
+ }
38919
+ function assetSizeGroup(asset) {
38920
+ if (asset === null) return "Unknown";
38921
+ if (asset < 1e5) return "Assets < $100 Million";
38922
+ if (asset < 1e6) return "Assets $100 Million - $1 Billion";
38923
+ if (asset < 1e7) return "Assets $1 Billion - $10 Billion";
38924
+ if (asset < 25e7) return "Assets $10 Billion - $250 Billion";
38925
+ return "Assets > $250 Billion";
38926
+ }
38927
+ function buildSeriesPoint(repdte, records) {
38928
+ const metrics = aggregateFinancialRecords(records);
38929
+ return {
38930
+ repdte,
38931
+ ...metrics
38932
+ };
38933
+ }
38934
+ function buildAssetSizeNimSeries(groupedRecords) {
38935
+ const rows = [];
38936
+ for (const [repdte, records] of groupedRecords) {
38937
+ const groups = /* @__PURE__ */ new Map();
38938
+ for (const record of records) {
38939
+ const group = assetSizeGroup(asNumber(record.ASSET));
38940
+ const groupRecords = groups.get(group);
38941
+ if (groupRecords) {
38942
+ groupRecords.push(record);
38943
+ } else {
38944
+ groups.set(group, [record]);
38945
+ }
38946
+ }
38947
+ rows.push({
38948
+ repdte,
38949
+ group: "Industry",
38950
+ institution_count: records.length,
38951
+ total_assets: sumField(records, "ASSET"),
38952
+ net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST")
38953
+ });
38954
+ for (const [group, groupRecords] of groups) {
38955
+ rows.push({
38956
+ repdte,
38957
+ group,
38958
+ institution_count: groupRecords.length,
38959
+ total_assets: sumField(groupRecords, "ASSET"),
38960
+ net_interest_margin_pct: weightedAverage(groupRecords, "NIMY", "ERNAST")
38961
+ });
38962
+ }
38963
+ }
38964
+ return rows;
38965
+ }
38966
+ function buildLoanAndDepositSeries(series) {
38967
+ const byDate = new Map(series.map((point) => [point.repdte, point]));
38968
+ return series.map((point) => {
38969
+ const priorQuarter = getPriorQuarterDates(point.repdte, 1)[0];
38970
+ const yearAgo = getReportDateOneYearPrior(point.repdte);
38971
+ const priorPoint = byDate.get(priorQuarter);
38972
+ const yearAgoPoint = byDate.get(yearAgo);
38973
+ return {
38974
+ repdte: point.repdte,
38975
+ total_loans_and_leases: point.total_loans_and_leases,
38976
+ quarterly_loan_change: change2(
38977
+ point.total_loans_and_leases,
38978
+ priorPoint?.total_loans_and_leases ?? null
38979
+ ),
38980
+ loan_growth_12_month_pct: pctChange2(
38981
+ point.total_loans_and_leases,
38982
+ yearAgoPoint?.total_loans_and_leases ?? null
38983
+ ),
38984
+ domestic_deposits: point.domestic_deposits,
38985
+ quarterly_domestic_deposit_change: change2(
38986
+ point.domestic_deposits,
38987
+ priorPoint?.domestic_deposits ?? null
38988
+ ),
38989
+ domestic_deposit_growth_12_month_pct: pctChange2(
38990
+ point.domestic_deposits,
38991
+ yearAgoPoint?.domestic_deposits ?? null
38992
+ )
38993
+ };
38994
+ });
38995
+ }
38996
+ function buildPortfolioPerformance(records) {
38997
+ const portfolios = [
38998
+ {
38999
+ id: "real_estate",
39000
+ label: "Real estate loans",
39001
+ balanceField: "LNRE",
39002
+ noncurrentRateField: "P3RER",
39003
+ chargeoffRateField: "NTRER"
39004
+ },
39005
+ {
39006
+ id: "commercial_industrial",
39007
+ label: "Commercial and industrial loans",
39008
+ balanceField: "LNCI",
39009
+ noncurrentRateField: "P3CIR",
39010
+ chargeoffRateField: "NTCIR"
39011
+ },
39012
+ {
39013
+ id: "consumer",
39014
+ label: "Consumer loans",
39015
+ balanceField: "LNCON",
39016
+ noncurrentRateField: "P3CONR",
39017
+ chargeoffRateField: "NTCONR"
39018
+ },
39019
+ {
39020
+ id: "credit_card",
39021
+ label: "Credit card loans",
39022
+ balanceField: "LNCRCD",
39023
+ noncurrentRateField: "P3CRCDR",
39024
+ chargeoffRateField: "NTCRCDR"
39025
+ },
39026
+ {
39027
+ id: "farm",
39028
+ label: "Farm loans",
39029
+ balanceField: "LNAG",
39030
+ noncurrentRateField: "P3AGR",
39031
+ chargeoffRateField: "NTAGR"
39032
+ }
39033
+ ];
39034
+ return portfolios.map((portfolio) => ({
39035
+ id: portfolio.id,
39036
+ label: portfolio.label,
39037
+ balance: sumField(records, portfolio.balanceField),
39038
+ balance_share_of_total_loans_pct: dividePct(
39039
+ sumField(records, portfolio.balanceField),
39040
+ sumField(records, "LNLSNET")
39041
+ ),
39042
+ noncurrent_rate_pct: weightedAverage(
39043
+ records,
39044
+ portfolio.noncurrentRateField,
39045
+ portfolio.balanceField
39046
+ ),
39047
+ net_chargeoff_rate_pct: weightedAverage(
39048
+ records,
39049
+ portfolio.chargeoffRateField,
39050
+ portfolio.balanceField
39051
+ )
39052
+ }));
39053
+ }
39054
+ function buildCommunityComparison(groupedRecords) {
39055
+ const rows = [];
39056
+ for (const [repdte, records] of groupedRecords) {
39057
+ for (const [group, groupRecords] of [
39058
+ ["Industry", records],
39059
+ ["Community Banks", records.filter(isCommunityBank)]
39060
+ ]) {
39061
+ const metrics = aggregateFinancialRecords(groupRecords);
39062
+ rows.push({
39063
+ repdte,
39064
+ group,
39065
+ institution_count: metrics.institution_count,
39066
+ total_assets: metrics.total_assets,
39067
+ total_loans_and_leases: metrics.total_loans_and_leases,
39068
+ domestic_deposits: metrics.domestic_deposits,
39069
+ net_income: metrics.net_income,
39070
+ roa_pct: metrics.roa_pct,
39071
+ net_interest_margin_pct: metrics.net_interest_margin_pct,
39072
+ noncurrent_loans_pct: metrics.noncurrent_loans_pct,
39073
+ net_chargeoffs_pct: metrics.net_chargeoffs_pct
39074
+ });
39075
+ }
39076
+ }
39077
+ return rows;
39078
+ }
39079
+ function buildTruncationWarning2(repdte, returnedCount, totalCount, limit = QBP_LITE_FETCH_LIMIT) {
39080
+ if (returnedCount >= limit && totalCount > limit) {
39081
+ return `REPDTE ${repdte}: results truncated at ${limit.toLocaleString()} of ${totalCount.toLocaleString()} institutions.`;
39082
+ }
39083
+ return null;
39084
+ }
39085
+ async function fetchRecordsForRepdte(repdte) {
39086
+ const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
39087
+ filters: `REPDTE:${repdte}`,
39088
+ fields: QBP_LITE_FIELDS,
39089
+ limit: QBP_LITE_FETCH_LIMIT,
39090
+ sort_by: "CERT",
39091
+ sort_order: "ASC"
39092
+ });
39093
+ const records = extractRecords(response);
39094
+ return {
39095
+ records,
39096
+ total: response.meta.total,
39097
+ truncationWarning: buildTruncationWarning2(
39098
+ repdte,
39099
+ records.length,
39100
+ response.meta.total
39101
+ )
39102
+ };
39103
+ }
39104
+ async function hasFinancialData(repdte) {
39105
+ const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
39106
+ filters: `REPDTE:${repdte}`,
39107
+ fields: "CERT,REPDTE",
39108
+ limit: 1
39109
+ });
39110
+ return response.meta.total > 0;
39111
+ }
39112
+ async function resolveReportDate(params) {
39113
+ if (params.repdte) {
39114
+ const validationError = validateQuarterEndDate(params.repdte, "repdte");
39115
+ if (validationError) {
39116
+ throw new Error(validationError);
39117
+ }
39118
+ return params.repdte;
39119
+ }
39120
+ const candidates = [getDefaultReportDate(), ...getPriorQuarterDates(getDefaultReportDate(), 7)];
39121
+ for (const candidate of candidates) {
39122
+ if (await hasFinancialData(candidate)) {
39123
+ return candidate;
39124
+ }
39125
+ }
39126
+ throw new Error("No BankFind financial data found in the latest eight expected reporting quarters.");
39127
+ }
39128
+ async function buildQbpLiteData(params) {
39129
+ const trendQuarters = params.trend_quarters ?? 20;
39130
+ const includeCommunityBanks = params.include_community_banks ?? true;
39131
+ const repdte = await resolveReportDate(params);
39132
+ const priorQuarterRepdte = getPriorQuarterDates(repdte, 1)[0];
39133
+ const yearAgoRepdte = getReportDateOneYearPrior(repdte);
39134
+ const trendDates = [...getPriorQuarterDates(repdte, trendQuarters - 1)].reverse();
39135
+ const requiredDates = [.../* @__PURE__ */ new Set([...trendDates, repdte, priorQuarterRepdte, yearAgoRepdte])].sort();
39136
+ const recordsByDate = /* @__PURE__ */ new Map();
39137
+ const warnings = [];
39138
+ await mapWithConcurrency(requiredDates, 4, async (date) => {
39139
+ const { records, truncationWarning } = await fetchRecordsForRepdte(date);
39140
+ recordsByDate.set(date, records);
39141
+ if (records.length === 0) {
39142
+ warnings.push(`No financial records found for REPDTE ${date}.`);
39143
+ }
39144
+ if (truncationWarning) {
39145
+ warnings.push(truncationWarning);
39146
+ }
39147
+ });
39148
+ const currentRecords = recordsByDate.get(repdte) ?? [];
39149
+ if (currentRecords.length === 0) {
39150
+ throw new Error(`No financial records found for current REPDTE ${repdte}.`);
39151
+ }
39152
+ const trendRecords = new Map(
39153
+ [...trendDates, repdte].filter((date) => (recordsByDate.get(date)?.length ?? 0) > 0).map((date) => [date, recordsByDate.get(date) ?? []])
39154
+ );
39155
+ const trendSeries = [...trendRecords.entries()].map(
39156
+ ([date, records]) => buildSeriesPoint(date, records)
39157
+ );
39158
+ const current = aggregateFinancialRecords(currentRecords);
39159
+ const priorQuarter = recordsByDate.has(priorQuarterRepdte) ? aggregateFinancialRecords(recordsByDate.get(priorQuarterRepdte) ?? []) : null;
39160
+ const yearAgo = recordsByDate.has(yearAgoRepdte) ? aggregateFinancialRecords(recordsByDate.get(yearAgoRepdte) ?? []) : null;
39161
+ return {
39162
+ report: {
39163
+ title: "QBP Lite: FDIC-Insured Institutions",
39164
+ repdte,
39165
+ prior_quarter_repdte: priorQuarterRepdte,
39166
+ year_ago_repdte: yearAgoRepdte,
39167
+ trend_start_repdte: trendSeries[0]?.repdte ?? repdte,
39168
+ trend_end_repdte: repdte
39169
+ },
39170
+ executive_snapshot: buildExecutiveSnapshot(current, priorQuarter, yearAgo),
39171
+ charts: {
39172
+ quarterly_net_income_and_roa: trendSeries.map((point) => ({
39173
+ repdte: point.repdte,
39174
+ net_income: point.net_income,
39175
+ roa_pct: point.roa_pct
39176
+ })),
39177
+ net_interest_margin_by_asset_size: buildAssetSizeNimSeries(trendRecords),
39178
+ loans_and_deposits: buildLoanAndDepositSeries(trendSeries),
39179
+ credit_quality: trendSeries.map((point) => ({
39180
+ repdte: point.repdte,
39181
+ noncurrent_loans_pct: point.noncurrent_loans_pct,
39182
+ net_chargeoffs_pct: point.net_chargeoffs_pct
39183
+ })),
39184
+ loan_performance_by_portfolio: buildPortfolioPerformance(currentRecords),
39185
+ capital_ratios: trendSeries.map((point) => ({
39186
+ repdte: point.repdte,
39187
+ leverage_ratio_pct: point.leverage_ratio_pct,
39188
+ common_equity_tier1_ratio_pct: point.common_equity_tier1_ratio_pct,
39189
+ tier1_risk_based_ratio_pct: point.tier1_risk_based_ratio_pct,
39190
+ total_risk_based_ratio_pct: point.total_risk_based_ratio_pct
39191
+ })),
39192
+ community_banks_vs_industry: includeCommunityBanks ? buildCommunityComparison(trendRecords) : []
39193
+ },
39194
+ data_notes: {
39195
+ source_dataset: "FDIC BankFind financials endpoint",
39196
+ date_field: "REPDTE",
39197
+ dollar_units: "Thousands of dollars unless converted by the client.",
39198
+ aggregation_notes: [
39199
+ "Dollar fields are summed across reporting institutions.",
39200
+ "ROA, ROE, credit-quality ratios, and leverage ratio are weighted using the closest available public denominator field.",
39201
+ "Leverage ratio is asset-weighted using period-end ASSET as a proxy for average assets because the average-asset denominator is not exposed as a separate public BankFind field.",
39202
+ "Net interest margin is weighted by earning assets.",
39203
+ "Risk-based capital ratios are calculated from summed public capital-dollar and risk-weighted-asset fields when available, with public ratio fields used only as a fallback.",
39204
+ "The report uses public quarterly Call Report-derived BankFind data; it is not an official FDIC QBP publication."
39205
+ ],
39206
+ known_exclusions: [
39207
+ "Official Problem Bank List counts are excluded because they depend on confidential CAMELS ratings.",
39208
+ "Deposit Insurance Fund balance, reserve ratio, assessments earned, and fund income or expense are excluded because they are DIF accounting data rather than BankFind institution financials.",
39209
+ "Assessment-rate distribution data is excluded because public BankFind financials do not provide institution assessment-rate ranges.",
39210
+ "Community-bank merger-adjusted prior-period series are excluded; community-bank comparisons use the public CB flag available in BankFind records."
39211
+ ],
39212
+ warnings
39213
+ }
39214
+ };
39215
+ }
39216
+ function formatMetricValue2(value, unit) {
39217
+ if (value === null) return "n/a";
39218
+ if (unit === "percent") return `${value.toFixed(2)}%`;
39219
+ return Math.round(value).toLocaleString();
39220
+ }
39221
+ function formatChangeValue(value, unit) {
39222
+ if (value === null) return "n/a";
39223
+ if (unit === "percentage_points") return `${value.toFixed(2)} ppts`;
39224
+ return Math.round(value).toLocaleString();
39225
+ }
39226
+ function formatQbpLiteText(data) {
39227
+ const lines = [
39228
+ `${data.report.title}`,
39229
+ `Quarter ended: ${data.report.repdte}`,
39230
+ `Prior quarter: ${data.report.prior_quarter_repdte} | Year ago: ${data.report.year_ago_repdte}`,
39231
+ "",
39232
+ "Executive Snapshot"
39233
+ ];
39234
+ for (const metric of data.executive_snapshot) {
39235
+ const current = formatMetricValue2(metric.current, metric.unit);
39236
+ const qoq = formatChangeValue(
39237
+ metric.prior_quarter_change,
39238
+ metric.change_unit
39239
+ );
39240
+ const yoy = formatChangeValue(
39241
+ metric.year_over_year_change,
39242
+ metric.change_unit
39243
+ );
39244
+ lines.push(`- ${metric.label}: ${current}; QoQ change ${qoq}; YoY change ${yoy}`);
39245
+ }
39246
+ lines.push(
39247
+ "",
39248
+ "Chart-ready datasets included: quarterly_net_income_and_roa, net_interest_margin_by_asset_size, loans_and_deposits, credit_quality, loan_performance_by_portfolio, capital_ratios, community_banks_vs_industry.",
39249
+ "",
39250
+ "Known exclusions: official Problem Bank List counts, DIF accounting, assessment-rate distributions, and merger-adjusted community-bank prior-period series."
39251
+ );
39252
+ return truncateIfNeeded(
39253
+ lines.join("\n"),
39254
+ CHARACTER_LIMIT,
39255
+ "Use structuredContent for the complete QBP Lite dataset."
39256
+ );
39257
+ }
39258
+ function registerQbpLiteTools(server) {
39259
+ server.registerTool(
39260
+ "fdic_qbp_lite_data",
39261
+ {
39262
+ title: "Generate QBP Lite Data Bundle",
39263
+ description: "Build chart-ready data for a concise QBP Lite report from reproducible public BankFind quarterly financials. Includes executive snapshot metrics, trend series, community-bank comparison data, source notes, and explicit exclusions for non-public or non-BankFind QBP items.",
39264
+ inputSchema: QbpLiteSchema,
39265
+ outputSchema: FdicAnalysisOutputSchema,
39266
+ annotations: {
39267
+ readOnlyHint: true,
39268
+ destructiveHint: false,
39269
+ idempotentHint: true,
39270
+ openWorldHint: true
39271
+ }
39272
+ },
39273
+ async (params) => {
39274
+ try {
39275
+ const data = await buildQbpLiteData(params);
39276
+ return {
39277
+ content: [{ type: "text", text: formatQbpLiteText(data) }],
39278
+ structuredContent: data
39279
+ };
39280
+ } catch (err) {
39281
+ return formatToolError(err);
39282
+ }
39283
+ }
39284
+ );
39285
+ }
39286
+
39287
+ // src/tools/chatgptRetrieval.ts
39288
+ var import_zod22 = require("zod");
38755
39289
 
38756
39290
  // src/tools/shared/chatgptUrls.ts
38757
39291
  var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
@@ -38771,11 +39305,11 @@ function getBranchCitationUrl() {
38771
39305
  }
38772
39306
 
38773
39307
  // src/tools/chatgptRetrieval.ts
38774
- var SearchInputSchema = import_zod21.z.object({
38775
- query: import_zod21.z.string().min(1).describe("Natural-language search query.")
39308
+ var SearchInputSchema = import_zod22.z.object({
39309
+ query: import_zod22.z.string().min(1).describe("Natural-language search query.")
38776
39310
  });
38777
- var FetchInputSchema = import_zod21.z.object({
38778
- id: import_zod21.z.string().min(1).describe(
39311
+ var FetchInputSchema = import_zod22.z.object({
39312
+ id: import_zod22.z.string().min(1).describe(
38779
39313
  "Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
38780
39314
  )
38781
39315
  });
@@ -39213,7 +39747,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
39213
39747
  }
39214
39748
 
39215
39749
  // src/tools/chatgptBankDeepDive.ts
39216
- var import_zod22 = require("zod");
39750
+ var import_zod23 = require("zod");
39217
39751
 
39218
39752
  // src/resources/chatgptAppResources.ts
39219
39753
  var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
@@ -39474,9 +40008,9 @@ function registerChatGptAppResources(server) {
39474
40008
  }
39475
40009
 
39476
40010
  // src/tools/chatgptBankDeepDive.ts
39477
- var BankDeepDiveInputSchema = import_zod22.z.object({
39478
- cert: import_zod22.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39479
- repdte: import_zod22.z.string().regex(/^\d{8}$/).optional().describe(
40011
+ var BankDeepDiveInputSchema = import_zod23.z.object({
40012
+ cert: import_zod23.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
40013
+ repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
39480
40014
  "Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
39481
40015
  )
39482
40016
  });
@@ -39775,28 +40309,28 @@ function registerSchemaResources(server) {
39775
40309
  }
39776
40310
 
39777
40311
  // src/prompts/workflows.ts
39778
- var import_zod23 = require("zod");
40312
+ var import_zod24 = require("zod");
39779
40313
  var BankDeepDiveArgs = {
39780
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39781
- repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
40314
+ bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
40315
+ repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe(
39782
40316
  "Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
39783
40317
  )
39784
40318
  };
39785
40319
  var FailureForensicsArgs = {
39786
- bank: import_zod23.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
39787
- lookback_quarters: import_zod23.z.string().regex(/^\d+$/).optional().describe(
40320
+ bank: import_zod24.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
40321
+ lookback_quarters: import_zod24.z.string().regex(/^\d+$/).optional().describe(
39788
40322
  "Number of pre-failure quarters to reconstruct (default 12 if omitted)."
39789
40323
  )
39790
40324
  };
39791
40325
  var PortfolioSurveillanceArgs = {
39792
- scope: import_zod23.z.string().min(1).describe(
40326
+ scope: import_zod24.z.string().min(1).describe(
39793
40327
  "Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
39794
40328
  ),
39795
- repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
40329
+ repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
39796
40330
  };
39797
40331
  var ExaminerOverlayArgs = {
39798
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39799
- qualitative_notes: import_zod23.z.string().optional().describe(
40332
+ bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
40333
+ qualitative_notes: import_zod24.z.string().optional().describe(
39800
40334
  "Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
39801
40335
  )
39802
40336
  };
@@ -39969,6 +40503,7 @@ function createServer(options = {}) {
39969
40503
  registerFranchiseFootprintTools(server);
39970
40504
  registerHoldingCompanyProfileTools(server);
39971
40505
  registerRegionalContextTools(server);
40506
+ registerQbpLiteTools(server);
39972
40507
  }
39973
40508
  if (profile.chatgptCanonical || profile.chatgptAliases) {
39974
40509
  registerChatGptRetrievalTools(server, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdic-mcp-server",
3
- "version": "1.29.0",
3
+ "version": "1.30.0",
4
4
  "description": "MCP server for the FDIC BankFind Suite API",
5
5
  "mcpName": "io.github.jflamb/fdic-mcp-server",
6
6
  "main": "dist/server.js",