fdic-mcp-server 1.28.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 +574 -21
  2. package/dist/server.js +574 -21
  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.28.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;
@@ -32130,6 +32130,13 @@ var PeerHealthProxySummarySchema = import_zod2.z.object({
32130
32130
  gaps: import_zod2.z.array(import_zod2.z.string())
32131
32131
  })
32132
32132
  });
32133
+ var DeprecationNoticeSchema = import_zod2.z.object({
32134
+ path: import_zod2.z.string(),
32135
+ status: import_zod2.z.literal("deprecated"),
32136
+ replacement: import_zod2.z.string(),
32137
+ removal_target: import_zod2.z.literal("future_major_release"),
32138
+ note: import_zod2.z.string()
32139
+ });
32133
32140
  var FdicPeerHealthOutputSchema = import_zod2.z.object({
32134
32141
  model: import_zod2.z.literal("public_camels_proxy_v1"),
32135
32142
  official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
@@ -32143,6 +32150,7 @@ var FdicPeerHealthOutputSchema = import_zod2.z.object({
32143
32150
  subject_rank: import_zod2.z.number().int().nullable(),
32144
32151
  metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32145
32152
  institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32153
+ deprecations: import_zod2.z.array(DeprecationNoticeSchema),
32146
32154
  peer_context: import_zod2.z.object({
32147
32155
  peer_count: import_zod2.z.number().int(),
32148
32156
  peer_definition: import_zod2.z.string(),
@@ -35928,6 +35936,15 @@ function computePeerStats(subjectValue, peerValues, options) {
35928
35936
  }
35929
35937
 
35930
35938
  // src/tools/peerHealth.ts
35939
+ var PEER_HEALTH_DEPRECATIONS = [
35940
+ {
35941
+ path: "peer_context.subject_percentiles",
35942
+ status: "deprecated",
35943
+ replacement: "metrics",
35944
+ removal_target: "future_major_release",
35945
+ note: "Use metrics[] for new subject-vs-peer UI bindings. The legacy camelCase percentile map remains for backward compatibility until a coordinated major release."
35946
+ }
35947
+ ];
35931
35948
  var PEER_METRICS = [
35932
35949
  // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35933
35950
  // New UI consumers should bind to the flat metrics[].name snake_case values instead.
@@ -36014,7 +36031,7 @@ Three usage modes:
36014
36031
 
36015
36032
  Optionally provide cert to highlight a subject institution's position in the ranking.
36016
36033
 
36017
- Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy_summary, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array and proxy_summary is a flattened subject proxy for UI binding while peer_context and proxy preserve the legacy detailed payloads. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
36034
+ Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy_summary, proxy, deprecations}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics[] is the preferred subject-vs-peer array for new UI bindings and proxy_summary is a flattened subject proxy. peer_context.subject_percentiles is deprecated, remains for backward compatibility, and is targeted for removal only in a future coordinated major release. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
36018
36035
 
36019
36036
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
36020
36037
  inputSchema: PeerHealthInputSchema,
@@ -36412,7 +36429,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36412
36429
  subject_rank: subjectRank,
36413
36430
  metrics: metricRows,
36414
36431
  institutions: returned,
36415
- peer_context: peerContext
36432
+ peer_context: peerContext,
36433
+ deprecations: PEER_HEALTH_DEPRECATIONS
36416
36434
  }
36417
36435
  };
36418
36436
  } catch (err) {
@@ -38717,8 +38735,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38717
38735
  );
38718
38736
  }
38719
38737
 
38720
- // src/tools/chatgptRetrieval.ts
38738
+ // src/tools/qbpLite.ts
38721
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");
38722
39274
 
38723
39275
  // src/tools/shared/chatgptUrls.ts
38724
39276
  var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
@@ -38738,11 +39290,11 @@ function getBranchCitationUrl() {
38738
39290
  }
38739
39291
 
38740
39292
  // src/tools/chatgptRetrieval.ts
38741
- var SearchInputSchema = import_zod21.z.object({
38742
- 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.")
38743
39295
  });
38744
- var FetchInputSchema = import_zod21.z.object({
38745
- id: import_zod21.z.string().min(1).describe(
39296
+ var FetchInputSchema = import_zod22.z.object({
39297
+ id: import_zod22.z.string().min(1).describe(
38746
39298
  "Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
38747
39299
  )
38748
39300
  });
@@ -39180,7 +39732,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
39180
39732
  }
39181
39733
 
39182
39734
  // src/tools/chatgptBankDeepDive.ts
39183
- var import_zod22 = require("zod");
39735
+ var import_zod23 = require("zod");
39184
39736
 
39185
39737
  // src/resources/chatgptAppResources.ts
39186
39738
  var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
@@ -39441,9 +39993,9 @@ function registerChatGptAppResources(server) {
39441
39993
  }
39442
39994
 
39443
39995
  // src/tools/chatgptBankDeepDive.ts
39444
- var BankDeepDiveInputSchema = import_zod22.z.object({
39445
- cert: import_zod22.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39446
- 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(
39447
39999
  "Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
39448
40000
  )
39449
40001
  });
@@ -39742,28 +40294,28 @@ function registerSchemaResources(server) {
39742
40294
  }
39743
40295
 
39744
40296
  // src/prompts/workflows.ts
39745
- var import_zod23 = require("zod");
40297
+ var import_zod24 = require("zod");
39746
40298
  var BankDeepDiveArgs = {
39747
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39748
- 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(
39749
40301
  "Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
39750
40302
  )
39751
40303
  };
39752
40304
  var FailureForensicsArgs = {
39753
- bank: import_zod23.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
39754
- 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(
39755
40307
  "Number of pre-failure quarters to reconstruct (default 12 if omitted)."
39756
40308
  )
39757
40309
  };
39758
40310
  var PortfolioSurveillanceArgs = {
39759
- scope: import_zod23.z.string().min(1).describe(
40311
+ scope: import_zod24.z.string().min(1).describe(
39760
40312
  "Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
39761
40313
  ),
39762
- 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.")
39763
40315
  };
39764
40316
  var ExaminerOverlayArgs = {
39765
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39766
- 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(
39767
40319
  "Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
39768
40320
  )
39769
40321
  };
@@ -39936,6 +40488,7 @@ function createServer(options = {}) {
39936
40488
  registerFranchiseFootprintTools(server);
39937
40489
  registerHoldingCompanyProfileTools(server);
39938
40490
  registerRegionalContextTools(server);
40491
+ registerQbpLiteTools(server);
39939
40492
  }
39940
40493
  if (profile.chatgptCanonical || profile.chatgptAliases) {
39941
40494
  registerChatGptRetrievalTools(server, {