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.
- package/dist/index.js +574 -21
- package/dist/server.js +574 -21
- 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.
|
|
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
|
|
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/
|
|
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 =
|
|
38757
|
-
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 =
|
|
38760
|
-
id:
|
|
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
|
|
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 =
|
|
39460
|
-
cert:
|
|
39461
|
-
repdte:
|
|
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
|
|
40312
|
+
var import_zod24 = require("zod");
|
|
39761
40313
|
var BankDeepDiveArgs = {
|
|
39762
|
-
bank:
|
|
39763
|
-
repdte:
|
|
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:
|
|
39769
|
-
lookback_quarters:
|
|
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:
|
|
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:
|
|
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:
|
|
39781
|
-
qualitative_notes:
|
|
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, {
|