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/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.28.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;
@@ -32145,6 +32145,13 @@ var PeerHealthProxySummarySchema = import_zod2.z.object({
32145
32145
  gaps: import_zod2.z.array(import_zod2.z.string())
32146
32146
  })
32147
32147
  });
32148
+ var DeprecationNoticeSchema = import_zod2.z.object({
32149
+ path: import_zod2.z.string(),
32150
+ status: import_zod2.z.literal("deprecated"),
32151
+ replacement: import_zod2.z.string(),
32152
+ removal_target: import_zod2.z.literal("future_major_release"),
32153
+ note: import_zod2.z.string()
32154
+ });
32148
32155
  var FdicPeerHealthOutputSchema = import_zod2.z.object({
32149
32156
  model: import_zod2.z.literal("public_camels_proxy_v1"),
32150
32157
  official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
@@ -32158,6 +32165,7 @@ var FdicPeerHealthOutputSchema = import_zod2.z.object({
32158
32165
  subject_rank: import_zod2.z.number().int().nullable(),
32159
32166
  metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32160
32167
  institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32168
+ deprecations: import_zod2.z.array(DeprecationNoticeSchema),
32161
32169
  peer_context: import_zod2.z.object({
32162
32170
  peer_count: import_zod2.z.number().int(),
32163
32171
  peer_definition: import_zod2.z.string(),
@@ -35943,6 +35951,15 @@ function computePeerStats(subjectValue, peerValues, options) {
35943
35951
  }
35944
35952
 
35945
35953
  // src/tools/peerHealth.ts
35954
+ var PEER_HEALTH_DEPRECATIONS = [
35955
+ {
35956
+ path: "peer_context.subject_percentiles",
35957
+ status: "deprecated",
35958
+ replacement: "metrics",
35959
+ removal_target: "future_major_release",
35960
+ note: "Use metrics[] for new subject-vs-peer UI bindings. The legacy camelCase percentile map remains for backward compatibility until a coordinated major release."
35961
+ }
35962
+ ];
35946
35963
  var PEER_METRICS = [
35947
35964
  // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35948
35965
  // New UI consumers should bind to the flat metrics[].name snake_case values instead.
@@ -36029,7 +36046,7 @@ Three usage modes:
36029
36046
 
36030
36047
  Optionally provide cert to highlight a subject institution's position in the ranking.
36031
36048
 
36032
- 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.
36049
+ 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.
36033
36050
 
36034
36051
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
36035
36052
  inputSchema: PeerHealthInputSchema,
@@ -36427,7 +36444,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36427
36444
  subject_rank: subjectRank,
36428
36445
  metrics: metricRows,
36429
36446
  institutions: returned,
36430
- peer_context: peerContext
36447
+ peer_context: peerContext,
36448
+ deprecations: PEER_HEALTH_DEPRECATIONS
36431
36449
  }
36432
36450
  };
36433
36451
  } catch (err) {
@@ -38732,8 +38750,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38732
38750
  );
38733
38751
  }
38734
38752
 
38735
- // src/tools/chatgptRetrieval.ts
38753
+ // src/tools/qbpLite.ts
38736
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");
38737
39289
 
38738
39290
  // src/tools/shared/chatgptUrls.ts
38739
39291
  var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
@@ -38753,11 +39305,11 @@ function getBranchCitationUrl() {
38753
39305
  }
38754
39306
 
38755
39307
  // src/tools/chatgptRetrieval.ts
38756
- var SearchInputSchema = import_zod21.z.object({
38757
- 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.")
38758
39310
  });
38759
- var FetchInputSchema = import_zod21.z.object({
38760
- id: import_zod21.z.string().min(1).describe(
39311
+ var FetchInputSchema = import_zod22.z.object({
39312
+ id: import_zod22.z.string().min(1).describe(
38761
39313
  "Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
38762
39314
  )
38763
39315
  });
@@ -39195,7 +39747,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
39195
39747
  }
39196
39748
 
39197
39749
  // src/tools/chatgptBankDeepDive.ts
39198
- var import_zod22 = require("zod");
39750
+ var import_zod23 = require("zod");
39199
39751
 
39200
39752
  // src/resources/chatgptAppResources.ts
39201
39753
  var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
@@ -39456,9 +40008,9 @@ function registerChatGptAppResources(server) {
39456
40008
  }
39457
40009
 
39458
40010
  // src/tools/chatgptBankDeepDive.ts
39459
- var BankDeepDiveInputSchema = import_zod22.z.object({
39460
- cert: import_zod22.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39461
- 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(
39462
40014
  "Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
39463
40015
  )
39464
40016
  });
@@ -39757,28 +40309,28 @@ function registerSchemaResources(server) {
39757
40309
  }
39758
40310
 
39759
40311
  // src/prompts/workflows.ts
39760
- var import_zod23 = require("zod");
40312
+ var import_zod24 = require("zod");
39761
40313
  var BankDeepDiveArgs = {
39762
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39763
- 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(
39764
40316
  "Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
39765
40317
  )
39766
40318
  };
39767
40319
  var FailureForensicsArgs = {
39768
- bank: import_zod23.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
39769
- 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(
39770
40322
  "Number of pre-failure quarters to reconstruct (default 12 if omitted)."
39771
40323
  )
39772
40324
  };
39773
40325
  var PortfolioSurveillanceArgs = {
39774
- scope: import_zod23.z.string().min(1).describe(
40326
+ scope: import_zod24.z.string().min(1).describe(
39775
40327
  "Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
39776
40328
  ),
39777
- 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.")
39778
40330
  };
39779
40331
  var ExaminerOverlayArgs = {
39780
- bank: import_zod23.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
39781
- 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(
39782
40334
  "Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
39783
40335
  )
39784
40336
  };
@@ -39951,6 +40503,7 @@ function createServer(options = {}) {
39951
40503
  registerFranchiseFootprintTools(server);
39952
40504
  registerHoldingCompanyProfileTools(server);
39953
40505
  registerRegionalContextTools(server);
40506
+ registerQbpLiteTools(server);
39954
40507
  }
39955
40508
  if (profile.chatgptCanonical || profile.chatgptAliases) {
39956
40509
  registerChatGptRetrievalTools(server, {