fdic-mcp-server 1.29.0 → 1.30.1
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 +664 -26
- package/dist/server.js +664 -26
- 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.1" : 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;
|
|
@@ -88,6 +88,42 @@ var RateLimiter = class {
|
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
+
var ConcurrentLimiter = class {
|
|
92
|
+
maxConcurrent;
|
|
93
|
+
active = /* @__PURE__ */ new Map();
|
|
94
|
+
constructor(maxConcurrent) {
|
|
95
|
+
this.maxConcurrent = maxConcurrent;
|
|
96
|
+
}
|
|
97
|
+
acquire(key) {
|
|
98
|
+
const current = this.active.get(key) ?? 0;
|
|
99
|
+
if (current >= this.maxConcurrent) {
|
|
100
|
+
return void 0;
|
|
101
|
+
}
|
|
102
|
+
this.active.set(key, current + 1);
|
|
103
|
+
let released = false;
|
|
104
|
+
return () => {
|
|
105
|
+
if (released) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
released = true;
|
|
109
|
+
const latest = this.active.get(key) ?? 0;
|
|
110
|
+
if (latest <= 1) {
|
|
111
|
+
this.active.delete(key);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.active.set(key, latest - 1);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/requestIdentity.ts
|
|
120
|
+
function getRequestIp(req) {
|
|
121
|
+
const forwarded = req.get("x-forwarded-for");
|
|
122
|
+
if (forwarded) {
|
|
123
|
+
return forwarded.split(",")[0]?.trim() || req.ip || "unknown";
|
|
124
|
+
}
|
|
125
|
+
return req.ip || "unknown";
|
|
126
|
+
}
|
|
91
127
|
|
|
92
128
|
// src/chat.ts
|
|
93
129
|
var CHAT_SYSTEM_PROMPT = `You are a demo assistant for the FDIC BankFind MCP Server. You help users
|
|
@@ -210,13 +246,6 @@ function mapMessagesToContents(messages) {
|
|
|
210
246
|
parts: [{ text: message.content }]
|
|
211
247
|
}));
|
|
212
248
|
}
|
|
213
|
-
function getRequestIp(req) {
|
|
214
|
-
const forwarded = req.get("x-forwarded-for");
|
|
215
|
-
if (forwarded) {
|
|
216
|
-
return forwarded.split(",")[0]?.trim() || req.ip || "unknown";
|
|
217
|
-
}
|
|
218
|
-
return req.ip || "unknown";
|
|
219
|
-
}
|
|
220
249
|
function getResponseParts(response) {
|
|
221
250
|
return response.candidates?.[0]?.content?.parts ?? [];
|
|
222
251
|
}
|
|
@@ -38750,8 +38779,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
|
|
|
38750
38779
|
);
|
|
38751
38780
|
}
|
|
38752
38781
|
|
|
38753
|
-
// src/tools/
|
|
38782
|
+
// src/tools/qbpLite.ts
|
|
38754
38783
|
var import_zod21 = require("zod");
|
|
38784
|
+
var QBP_LITE_FETCH_LIMIT = 1e4;
|
|
38785
|
+
var QBP_LITE_FIELDS = [
|
|
38786
|
+
"CERT",
|
|
38787
|
+
"NAME",
|
|
38788
|
+
"REPDTE",
|
|
38789
|
+
"CB",
|
|
38790
|
+
"SPECGRP",
|
|
38791
|
+
"SPECGRPDESC",
|
|
38792
|
+
"ASSET",
|
|
38793
|
+
"DEP",
|
|
38794
|
+
"DEPDOM",
|
|
38795
|
+
"NETINC",
|
|
38796
|
+
"ROA",
|
|
38797
|
+
"ROE",
|
|
38798
|
+
"NIMY",
|
|
38799
|
+
"ERNAST",
|
|
38800
|
+
"LNLSNET",
|
|
38801
|
+
"LNRE",
|
|
38802
|
+
"LNCI",
|
|
38803
|
+
"LNCON",
|
|
38804
|
+
"LNCRCD",
|
|
38805
|
+
"LNAG",
|
|
38806
|
+
"SC",
|
|
38807
|
+
"EQTOT",
|
|
38808
|
+
"NCLNLSR",
|
|
38809
|
+
"NTLNLSR",
|
|
38810
|
+
"RBC",
|
|
38811
|
+
"RBC1AAJ",
|
|
38812
|
+
"IDT1RWAJR",
|
|
38813
|
+
"RBCT1",
|
|
38814
|
+
"RBCT1C",
|
|
38815
|
+
"RBCT1CER",
|
|
38816
|
+
"RBCRWAJ",
|
|
38817
|
+
"RWAJ",
|
|
38818
|
+
"RWAJT",
|
|
38819
|
+
"P3RER",
|
|
38820
|
+
"P3CIR",
|
|
38821
|
+
"P3CONR",
|
|
38822
|
+
"P3CRCDR",
|
|
38823
|
+
"P3AGR",
|
|
38824
|
+
"NTRER",
|
|
38825
|
+
"NTCIR",
|
|
38826
|
+
"NTCONR",
|
|
38827
|
+
"NTCRCDR",
|
|
38828
|
+
"NTAGR"
|
|
38829
|
+
].join(",");
|
|
38830
|
+
var QbpLiteSchema = import_zod21.z.object({
|
|
38831
|
+
repdte: import_zod21.z.string().regex(/^\d{8}$/).optional().describe(
|
|
38832
|
+
"Quarter-end Report Date (REPDTE) in YYYYMMDD format. If omitted, the tool searches backward from the latest likely published quarter until data is found."
|
|
38833
|
+
),
|
|
38834
|
+
trend_quarters: import_zod21.z.number().int().min(8).max(40).default(20).describe(
|
|
38835
|
+
"Number of quarterly observations to return for trend charts, including the current quarter. Default 20 quarters."
|
|
38836
|
+
),
|
|
38837
|
+
include_community_banks: import_zod21.z.boolean().default(true).describe(
|
|
38838
|
+
"Include a compact community-bank-vs-industry comparison using the public community-bank flag."
|
|
38839
|
+
)
|
|
38840
|
+
});
|
|
38841
|
+
var EXECUTIVE_METRICS = [
|
|
38842
|
+
{
|
|
38843
|
+
id: "institution_count",
|
|
38844
|
+
label: "Number of institutions reporting",
|
|
38845
|
+
unit: "count"
|
|
38846
|
+
},
|
|
38847
|
+
{ id: "total_assets", label: "Total assets", unit: "$thousands" },
|
|
38848
|
+
{
|
|
38849
|
+
id: "total_loans_and_leases",
|
|
38850
|
+
label: "Total loans and leases",
|
|
38851
|
+
unit: "$thousands"
|
|
38852
|
+
},
|
|
38853
|
+
{ id: "domestic_deposits", label: "Domestic deposits", unit: "$thousands" },
|
|
38854
|
+
{ id: "net_income", label: "Net income", unit: "$thousands" },
|
|
38855
|
+
{ id: "roa_pct", label: "Return on assets", unit: "percent" },
|
|
38856
|
+
{ id: "roe_pct", label: "Return on equity", unit: "percent" },
|
|
38857
|
+
{ id: "net_interest_margin_pct", label: "Net interest margin", unit: "percent" },
|
|
38858
|
+
{ id: "noncurrent_loans_pct", label: "Noncurrent loans to loans", unit: "percent" },
|
|
38859
|
+
{ id: "net_chargeoffs_pct", label: "Net charge-offs to loans", unit: "percent" },
|
|
38860
|
+
{ id: "leverage_ratio_pct", label: "Core capital (leverage) ratio", unit: "percent" }
|
|
38861
|
+
];
|
|
38862
|
+
function sumField(records, field) {
|
|
38863
|
+
let total = 0;
|
|
38864
|
+
let seen = false;
|
|
38865
|
+
for (const record of records) {
|
|
38866
|
+
const value = asNumber(record[field]);
|
|
38867
|
+
if (value !== null) {
|
|
38868
|
+
total += value;
|
|
38869
|
+
seen = true;
|
|
38870
|
+
}
|
|
38871
|
+
}
|
|
38872
|
+
return seen ? total : null;
|
|
38873
|
+
}
|
|
38874
|
+
function weightedAverage(records, valueField, weightField) {
|
|
38875
|
+
let weightedSum = 0;
|
|
38876
|
+
let weightSum = 0;
|
|
38877
|
+
for (const record of records) {
|
|
38878
|
+
const value = asNumber(record[valueField]);
|
|
38879
|
+
const weight = asNumber(record[weightField]);
|
|
38880
|
+
if (value !== null && weight !== null && weight > 0) {
|
|
38881
|
+
weightedSum += value * weight;
|
|
38882
|
+
weightSum += weight;
|
|
38883
|
+
}
|
|
38884
|
+
}
|
|
38885
|
+
return weightSum > 0 ? weightedSum / weightSum : null;
|
|
38886
|
+
}
|
|
38887
|
+
function pctChange2(current, prior) {
|
|
38888
|
+
if (current === null || prior === null || prior === 0) return null;
|
|
38889
|
+
return (current - prior) / prior * 100;
|
|
38890
|
+
}
|
|
38891
|
+
function change2(current, prior) {
|
|
38892
|
+
if (current === null || prior === null) return null;
|
|
38893
|
+
return current - prior;
|
|
38894
|
+
}
|
|
38895
|
+
function dividePct(numerator, denominator) {
|
|
38896
|
+
if (numerator === null || denominator === null || denominator === 0) return null;
|
|
38897
|
+
return numerator / denominator * 100;
|
|
38898
|
+
}
|
|
38899
|
+
function sumRatioPct(records, numeratorField, denominatorField) {
|
|
38900
|
+
return dividePct(sumField(records, numeratorField), sumField(records, denominatorField));
|
|
38901
|
+
}
|
|
38902
|
+
function aggregateFinancialRecords(records) {
|
|
38903
|
+
const totalAssets = sumField(records, "ASSET");
|
|
38904
|
+
const totalLoans = sumField(records, "LNLSNET");
|
|
38905
|
+
const equityCapital = sumField(records, "EQTOT");
|
|
38906
|
+
return {
|
|
38907
|
+
institution_count: records.length,
|
|
38908
|
+
total_assets: totalAssets,
|
|
38909
|
+
total_loans_and_leases: totalLoans,
|
|
38910
|
+
domestic_deposits: sumField(records, "DEPDOM"),
|
|
38911
|
+
total_deposits: sumField(records, "DEP"),
|
|
38912
|
+
securities: sumField(records, "SC"),
|
|
38913
|
+
equity_capital: equityCapital,
|
|
38914
|
+
net_income: sumField(records, "NETINC"),
|
|
38915
|
+
roa_pct: weightedAverage(records, "ROA", "ASSET"),
|
|
38916
|
+
roe_pct: weightedAverage(records, "ROE", "EQTOT"),
|
|
38917
|
+
net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST"),
|
|
38918
|
+
noncurrent_loans_pct: weightedAverage(records, "NCLNLSR", "LNLSNET"),
|
|
38919
|
+
net_chargeoffs_pct: weightedAverage(records, "NTLNLSR", "LNLSNET"),
|
|
38920
|
+
leverage_ratio_pct: weightedAverage(records, "RBC1AAJ", "ASSET"),
|
|
38921
|
+
common_equity_tier1_ratio_pct: sumRatioPct(records, "RBCT1C", "RWAJ") ?? weightedAverage(records, "RBCT1CER", "RWAJ"),
|
|
38922
|
+
tier1_risk_based_ratio_pct: sumRatioPct(records, "RBCT1", "RWAJ") ?? weightedAverage(records, "IDT1RWAJR", "RWAJ"),
|
|
38923
|
+
total_risk_based_ratio_pct: sumRatioPct(records, "RBC", "RWAJT") ?? weightedAverage(records, "RBCRWAJ", "RWAJT")
|
|
38924
|
+
};
|
|
38925
|
+
}
|
|
38926
|
+
function buildExecutiveSnapshot(current, priorQuarter, yearAgo) {
|
|
38927
|
+
return EXECUTIVE_METRICS.map(({ id, label, unit }) => {
|
|
38928
|
+
const currentValue = current[id];
|
|
38929
|
+
const priorValue = priorQuarter?.[id] ?? null;
|
|
38930
|
+
const yearAgoValue = yearAgo?.[id] ?? null;
|
|
38931
|
+
return {
|
|
38932
|
+
id,
|
|
38933
|
+
label,
|
|
38934
|
+
unit,
|
|
38935
|
+
change_unit: unit === "percent" ? "percentage_points" : unit,
|
|
38936
|
+
current: currentValue,
|
|
38937
|
+
prior_quarter_change: change2(currentValue, priorValue),
|
|
38938
|
+
prior_quarter_change_pct: unit === "percent" ? null : pctChange2(currentValue, priorValue),
|
|
38939
|
+
year_over_year_change: change2(currentValue, yearAgoValue),
|
|
38940
|
+
year_over_year_change_pct: unit === "percent" ? null : pctChange2(currentValue, yearAgoValue)
|
|
38941
|
+
};
|
|
38942
|
+
});
|
|
38943
|
+
}
|
|
38944
|
+
function isCommunityBank(record) {
|
|
38945
|
+
const value = record.CB;
|
|
38946
|
+
return value === 1 || value === "1";
|
|
38947
|
+
}
|
|
38948
|
+
function assetSizeGroup(asset) {
|
|
38949
|
+
if (asset === null) return "Unknown";
|
|
38950
|
+
if (asset < 1e5) return "Assets < $100 Million";
|
|
38951
|
+
if (asset < 1e6) return "Assets $100 Million - $1 Billion";
|
|
38952
|
+
if (asset < 1e7) return "Assets $1 Billion - $10 Billion";
|
|
38953
|
+
if (asset < 25e7) return "Assets $10 Billion - $250 Billion";
|
|
38954
|
+
return "Assets > $250 Billion";
|
|
38955
|
+
}
|
|
38956
|
+
function buildSeriesPoint(repdte, records) {
|
|
38957
|
+
const metrics = aggregateFinancialRecords(records);
|
|
38958
|
+
return {
|
|
38959
|
+
repdte,
|
|
38960
|
+
...metrics
|
|
38961
|
+
};
|
|
38962
|
+
}
|
|
38963
|
+
function buildAssetSizeNimSeries(groupedRecords) {
|
|
38964
|
+
const rows = [];
|
|
38965
|
+
for (const [repdte, records] of groupedRecords) {
|
|
38966
|
+
const groups = /* @__PURE__ */ new Map();
|
|
38967
|
+
for (const record of records) {
|
|
38968
|
+
const group = assetSizeGroup(asNumber(record.ASSET));
|
|
38969
|
+
const groupRecords = groups.get(group);
|
|
38970
|
+
if (groupRecords) {
|
|
38971
|
+
groupRecords.push(record);
|
|
38972
|
+
} else {
|
|
38973
|
+
groups.set(group, [record]);
|
|
38974
|
+
}
|
|
38975
|
+
}
|
|
38976
|
+
rows.push({
|
|
38977
|
+
repdte,
|
|
38978
|
+
group: "Industry",
|
|
38979
|
+
institution_count: records.length,
|
|
38980
|
+
total_assets: sumField(records, "ASSET"),
|
|
38981
|
+
net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST")
|
|
38982
|
+
});
|
|
38983
|
+
for (const [group, groupRecords] of groups) {
|
|
38984
|
+
rows.push({
|
|
38985
|
+
repdte,
|
|
38986
|
+
group,
|
|
38987
|
+
institution_count: groupRecords.length,
|
|
38988
|
+
total_assets: sumField(groupRecords, "ASSET"),
|
|
38989
|
+
net_interest_margin_pct: weightedAverage(groupRecords, "NIMY", "ERNAST")
|
|
38990
|
+
});
|
|
38991
|
+
}
|
|
38992
|
+
}
|
|
38993
|
+
return rows;
|
|
38994
|
+
}
|
|
38995
|
+
function buildLoanAndDepositSeries(series) {
|
|
38996
|
+
const byDate = new Map(series.map((point) => [point.repdte, point]));
|
|
38997
|
+
return series.map((point) => {
|
|
38998
|
+
const priorQuarter = getPriorQuarterDates(point.repdte, 1)[0];
|
|
38999
|
+
const yearAgo = getReportDateOneYearPrior(point.repdte);
|
|
39000
|
+
const priorPoint = byDate.get(priorQuarter);
|
|
39001
|
+
const yearAgoPoint = byDate.get(yearAgo);
|
|
39002
|
+
return {
|
|
39003
|
+
repdte: point.repdte,
|
|
39004
|
+
total_loans_and_leases: point.total_loans_and_leases,
|
|
39005
|
+
quarterly_loan_change: change2(
|
|
39006
|
+
point.total_loans_and_leases,
|
|
39007
|
+
priorPoint?.total_loans_and_leases ?? null
|
|
39008
|
+
),
|
|
39009
|
+
loan_growth_12_month_pct: pctChange2(
|
|
39010
|
+
point.total_loans_and_leases,
|
|
39011
|
+
yearAgoPoint?.total_loans_and_leases ?? null
|
|
39012
|
+
),
|
|
39013
|
+
domestic_deposits: point.domestic_deposits,
|
|
39014
|
+
quarterly_domestic_deposit_change: change2(
|
|
39015
|
+
point.domestic_deposits,
|
|
39016
|
+
priorPoint?.domestic_deposits ?? null
|
|
39017
|
+
),
|
|
39018
|
+
domestic_deposit_growth_12_month_pct: pctChange2(
|
|
39019
|
+
point.domestic_deposits,
|
|
39020
|
+
yearAgoPoint?.domestic_deposits ?? null
|
|
39021
|
+
)
|
|
39022
|
+
};
|
|
39023
|
+
});
|
|
39024
|
+
}
|
|
39025
|
+
function buildPortfolioPerformance(records) {
|
|
39026
|
+
const portfolios = [
|
|
39027
|
+
{
|
|
39028
|
+
id: "real_estate",
|
|
39029
|
+
label: "Real estate loans",
|
|
39030
|
+
balanceField: "LNRE",
|
|
39031
|
+
noncurrentRateField: "P3RER",
|
|
39032
|
+
chargeoffRateField: "NTRER"
|
|
39033
|
+
},
|
|
39034
|
+
{
|
|
39035
|
+
id: "commercial_industrial",
|
|
39036
|
+
label: "Commercial and industrial loans",
|
|
39037
|
+
balanceField: "LNCI",
|
|
39038
|
+
noncurrentRateField: "P3CIR",
|
|
39039
|
+
chargeoffRateField: "NTCIR"
|
|
39040
|
+
},
|
|
39041
|
+
{
|
|
39042
|
+
id: "consumer",
|
|
39043
|
+
label: "Consumer loans",
|
|
39044
|
+
balanceField: "LNCON",
|
|
39045
|
+
noncurrentRateField: "P3CONR",
|
|
39046
|
+
chargeoffRateField: "NTCONR"
|
|
39047
|
+
},
|
|
39048
|
+
{
|
|
39049
|
+
id: "credit_card",
|
|
39050
|
+
label: "Credit card loans",
|
|
39051
|
+
balanceField: "LNCRCD",
|
|
39052
|
+
noncurrentRateField: "P3CRCDR",
|
|
39053
|
+
chargeoffRateField: "NTCRCDR"
|
|
39054
|
+
},
|
|
39055
|
+
{
|
|
39056
|
+
id: "farm",
|
|
39057
|
+
label: "Farm loans",
|
|
39058
|
+
balanceField: "LNAG",
|
|
39059
|
+
noncurrentRateField: "P3AGR",
|
|
39060
|
+
chargeoffRateField: "NTAGR"
|
|
39061
|
+
}
|
|
39062
|
+
];
|
|
39063
|
+
return portfolios.map((portfolio) => ({
|
|
39064
|
+
id: portfolio.id,
|
|
39065
|
+
label: portfolio.label,
|
|
39066
|
+
balance: sumField(records, portfolio.balanceField),
|
|
39067
|
+
balance_share_of_total_loans_pct: dividePct(
|
|
39068
|
+
sumField(records, portfolio.balanceField),
|
|
39069
|
+
sumField(records, "LNLSNET")
|
|
39070
|
+
),
|
|
39071
|
+
noncurrent_rate_pct: weightedAverage(
|
|
39072
|
+
records,
|
|
39073
|
+
portfolio.noncurrentRateField,
|
|
39074
|
+
portfolio.balanceField
|
|
39075
|
+
),
|
|
39076
|
+
net_chargeoff_rate_pct: weightedAverage(
|
|
39077
|
+
records,
|
|
39078
|
+
portfolio.chargeoffRateField,
|
|
39079
|
+
portfolio.balanceField
|
|
39080
|
+
)
|
|
39081
|
+
}));
|
|
39082
|
+
}
|
|
39083
|
+
function buildCommunityComparison(groupedRecords) {
|
|
39084
|
+
const rows = [];
|
|
39085
|
+
for (const [repdte, records] of groupedRecords) {
|
|
39086
|
+
for (const [group, groupRecords] of [
|
|
39087
|
+
["Industry", records],
|
|
39088
|
+
["Community Banks", records.filter(isCommunityBank)]
|
|
39089
|
+
]) {
|
|
39090
|
+
const metrics = aggregateFinancialRecords(groupRecords);
|
|
39091
|
+
rows.push({
|
|
39092
|
+
repdte,
|
|
39093
|
+
group,
|
|
39094
|
+
institution_count: metrics.institution_count,
|
|
39095
|
+
total_assets: metrics.total_assets,
|
|
39096
|
+
total_loans_and_leases: metrics.total_loans_and_leases,
|
|
39097
|
+
domestic_deposits: metrics.domestic_deposits,
|
|
39098
|
+
net_income: metrics.net_income,
|
|
39099
|
+
roa_pct: metrics.roa_pct,
|
|
39100
|
+
net_interest_margin_pct: metrics.net_interest_margin_pct,
|
|
39101
|
+
noncurrent_loans_pct: metrics.noncurrent_loans_pct,
|
|
39102
|
+
net_chargeoffs_pct: metrics.net_chargeoffs_pct
|
|
39103
|
+
});
|
|
39104
|
+
}
|
|
39105
|
+
}
|
|
39106
|
+
return rows;
|
|
39107
|
+
}
|
|
39108
|
+
function buildTruncationWarning2(repdte, returnedCount, totalCount, limit = QBP_LITE_FETCH_LIMIT) {
|
|
39109
|
+
if (returnedCount >= limit && totalCount > limit) {
|
|
39110
|
+
return `REPDTE ${repdte}: results truncated at ${limit.toLocaleString()} of ${totalCount.toLocaleString()} institutions.`;
|
|
39111
|
+
}
|
|
39112
|
+
return null;
|
|
39113
|
+
}
|
|
39114
|
+
async function fetchRecordsForRepdte(repdte) {
|
|
39115
|
+
const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
|
|
39116
|
+
filters: `REPDTE:${repdte}`,
|
|
39117
|
+
fields: QBP_LITE_FIELDS,
|
|
39118
|
+
limit: QBP_LITE_FETCH_LIMIT,
|
|
39119
|
+
sort_by: "CERT",
|
|
39120
|
+
sort_order: "ASC"
|
|
39121
|
+
});
|
|
39122
|
+
const records = extractRecords(response);
|
|
39123
|
+
return {
|
|
39124
|
+
records,
|
|
39125
|
+
total: response.meta.total,
|
|
39126
|
+
truncationWarning: buildTruncationWarning2(
|
|
39127
|
+
repdte,
|
|
39128
|
+
records.length,
|
|
39129
|
+
response.meta.total
|
|
39130
|
+
)
|
|
39131
|
+
};
|
|
39132
|
+
}
|
|
39133
|
+
async function hasFinancialData(repdte) {
|
|
39134
|
+
const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
|
|
39135
|
+
filters: `REPDTE:${repdte}`,
|
|
39136
|
+
fields: "CERT,REPDTE",
|
|
39137
|
+
limit: 1
|
|
39138
|
+
});
|
|
39139
|
+
return response.meta.total > 0;
|
|
39140
|
+
}
|
|
39141
|
+
async function resolveReportDate(params) {
|
|
39142
|
+
if (params.repdte) {
|
|
39143
|
+
const validationError = validateQuarterEndDate(params.repdte, "repdte");
|
|
39144
|
+
if (validationError) {
|
|
39145
|
+
throw new Error(validationError);
|
|
39146
|
+
}
|
|
39147
|
+
return params.repdte;
|
|
39148
|
+
}
|
|
39149
|
+
const candidates = [getDefaultReportDate(), ...getPriorQuarterDates(getDefaultReportDate(), 7)];
|
|
39150
|
+
for (const candidate of candidates) {
|
|
39151
|
+
if (await hasFinancialData(candidate)) {
|
|
39152
|
+
return candidate;
|
|
39153
|
+
}
|
|
39154
|
+
}
|
|
39155
|
+
throw new Error("No BankFind financial data found in the latest eight expected reporting quarters.");
|
|
39156
|
+
}
|
|
39157
|
+
async function buildQbpLiteData(params) {
|
|
39158
|
+
const trendQuarters = params.trend_quarters ?? 20;
|
|
39159
|
+
const includeCommunityBanks = params.include_community_banks ?? true;
|
|
39160
|
+
const repdte = await resolveReportDate(params);
|
|
39161
|
+
const priorQuarterRepdte = getPriorQuarterDates(repdte, 1)[0];
|
|
39162
|
+
const yearAgoRepdte = getReportDateOneYearPrior(repdte);
|
|
39163
|
+
const trendDates = [...getPriorQuarterDates(repdte, trendQuarters - 1)].reverse();
|
|
39164
|
+
const requiredDates = [.../* @__PURE__ */ new Set([...trendDates, repdte, priorQuarterRepdte, yearAgoRepdte])].sort();
|
|
39165
|
+
const recordsByDate = /* @__PURE__ */ new Map();
|
|
39166
|
+
const warnings = [];
|
|
39167
|
+
await mapWithConcurrency(requiredDates, 4, async (date) => {
|
|
39168
|
+
const { records, truncationWarning } = await fetchRecordsForRepdte(date);
|
|
39169
|
+
recordsByDate.set(date, records);
|
|
39170
|
+
if (records.length === 0) {
|
|
39171
|
+
warnings.push(`No financial records found for REPDTE ${date}.`);
|
|
39172
|
+
}
|
|
39173
|
+
if (truncationWarning) {
|
|
39174
|
+
warnings.push(truncationWarning);
|
|
39175
|
+
}
|
|
39176
|
+
});
|
|
39177
|
+
const currentRecords = recordsByDate.get(repdte) ?? [];
|
|
39178
|
+
if (currentRecords.length === 0) {
|
|
39179
|
+
throw new Error(`No financial records found for current REPDTE ${repdte}.`);
|
|
39180
|
+
}
|
|
39181
|
+
const trendRecords = new Map(
|
|
39182
|
+
[...trendDates, repdte].filter((date) => (recordsByDate.get(date)?.length ?? 0) > 0).map((date) => [date, recordsByDate.get(date) ?? []])
|
|
39183
|
+
);
|
|
39184
|
+
const trendSeries = [...trendRecords.entries()].map(
|
|
39185
|
+
([date, records]) => buildSeriesPoint(date, records)
|
|
39186
|
+
);
|
|
39187
|
+
const current = aggregateFinancialRecords(currentRecords);
|
|
39188
|
+
const priorQuarter = recordsByDate.has(priorQuarterRepdte) ? aggregateFinancialRecords(recordsByDate.get(priorQuarterRepdte) ?? []) : null;
|
|
39189
|
+
const yearAgo = recordsByDate.has(yearAgoRepdte) ? aggregateFinancialRecords(recordsByDate.get(yearAgoRepdte) ?? []) : null;
|
|
39190
|
+
return {
|
|
39191
|
+
report: {
|
|
39192
|
+
title: "QBP Lite: FDIC-Insured Institutions",
|
|
39193
|
+
repdte,
|
|
39194
|
+
prior_quarter_repdte: priorQuarterRepdte,
|
|
39195
|
+
year_ago_repdte: yearAgoRepdte,
|
|
39196
|
+
trend_start_repdte: trendSeries[0]?.repdte ?? repdte,
|
|
39197
|
+
trend_end_repdte: repdte
|
|
39198
|
+
},
|
|
39199
|
+
executive_snapshot: buildExecutiveSnapshot(current, priorQuarter, yearAgo),
|
|
39200
|
+
charts: {
|
|
39201
|
+
quarterly_net_income_and_roa: trendSeries.map((point) => ({
|
|
39202
|
+
repdte: point.repdte,
|
|
39203
|
+
net_income: point.net_income,
|
|
39204
|
+
roa_pct: point.roa_pct
|
|
39205
|
+
})),
|
|
39206
|
+
net_interest_margin_by_asset_size: buildAssetSizeNimSeries(trendRecords),
|
|
39207
|
+
loans_and_deposits: buildLoanAndDepositSeries(trendSeries),
|
|
39208
|
+
credit_quality: trendSeries.map((point) => ({
|
|
39209
|
+
repdte: point.repdte,
|
|
39210
|
+
noncurrent_loans_pct: point.noncurrent_loans_pct,
|
|
39211
|
+
net_chargeoffs_pct: point.net_chargeoffs_pct
|
|
39212
|
+
})),
|
|
39213
|
+
loan_performance_by_portfolio: buildPortfolioPerformance(currentRecords),
|
|
39214
|
+
capital_ratios: trendSeries.map((point) => ({
|
|
39215
|
+
repdte: point.repdte,
|
|
39216
|
+
leverage_ratio_pct: point.leverage_ratio_pct,
|
|
39217
|
+
common_equity_tier1_ratio_pct: point.common_equity_tier1_ratio_pct,
|
|
39218
|
+
tier1_risk_based_ratio_pct: point.tier1_risk_based_ratio_pct,
|
|
39219
|
+
total_risk_based_ratio_pct: point.total_risk_based_ratio_pct
|
|
39220
|
+
})),
|
|
39221
|
+
community_banks_vs_industry: includeCommunityBanks ? buildCommunityComparison(trendRecords) : []
|
|
39222
|
+
},
|
|
39223
|
+
data_notes: {
|
|
39224
|
+
source_dataset: "FDIC BankFind financials endpoint",
|
|
39225
|
+
date_field: "REPDTE",
|
|
39226
|
+
dollar_units: "Thousands of dollars unless converted by the client.",
|
|
39227
|
+
aggregation_notes: [
|
|
39228
|
+
"Dollar fields are summed across reporting institutions.",
|
|
39229
|
+
"ROA, ROE, credit-quality ratios, and leverage ratio are weighted using the closest available public denominator field.",
|
|
39230
|
+
"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.",
|
|
39231
|
+
"Net interest margin is weighted by earning assets.",
|
|
39232
|
+
"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.",
|
|
39233
|
+
"The report uses public quarterly Call Report-derived BankFind data; it is not an official FDIC QBP publication."
|
|
39234
|
+
],
|
|
39235
|
+
known_exclusions: [
|
|
39236
|
+
"Official Problem Bank List counts are excluded because they depend on confidential CAMELS ratings.",
|
|
39237
|
+
"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.",
|
|
39238
|
+
"Assessment-rate distribution data is excluded because public BankFind financials do not provide institution assessment-rate ranges.",
|
|
39239
|
+
"Community-bank merger-adjusted prior-period series are excluded; community-bank comparisons use the public CB flag available in BankFind records."
|
|
39240
|
+
],
|
|
39241
|
+
warnings
|
|
39242
|
+
}
|
|
39243
|
+
};
|
|
39244
|
+
}
|
|
39245
|
+
function formatMetricValue2(value, unit) {
|
|
39246
|
+
if (value === null) return "n/a";
|
|
39247
|
+
if (unit === "percent") return `${value.toFixed(2)}%`;
|
|
39248
|
+
return Math.round(value).toLocaleString();
|
|
39249
|
+
}
|
|
39250
|
+
function formatChangeValue(value, unit) {
|
|
39251
|
+
if (value === null) return "n/a";
|
|
39252
|
+
if (unit === "percentage_points") return `${value.toFixed(2)} ppts`;
|
|
39253
|
+
return Math.round(value).toLocaleString();
|
|
39254
|
+
}
|
|
39255
|
+
function formatQbpLiteText(data) {
|
|
39256
|
+
const lines = [
|
|
39257
|
+
`${data.report.title}`,
|
|
39258
|
+
`Quarter ended: ${data.report.repdte}`,
|
|
39259
|
+
`Prior quarter: ${data.report.prior_quarter_repdte} | Year ago: ${data.report.year_ago_repdte}`,
|
|
39260
|
+
"",
|
|
39261
|
+
"Executive Snapshot"
|
|
39262
|
+
];
|
|
39263
|
+
for (const metric of data.executive_snapshot) {
|
|
39264
|
+
const current = formatMetricValue2(metric.current, metric.unit);
|
|
39265
|
+
const qoq = formatChangeValue(
|
|
39266
|
+
metric.prior_quarter_change,
|
|
39267
|
+
metric.change_unit
|
|
39268
|
+
);
|
|
39269
|
+
const yoy = formatChangeValue(
|
|
39270
|
+
metric.year_over_year_change,
|
|
39271
|
+
metric.change_unit
|
|
39272
|
+
);
|
|
39273
|
+
lines.push(`- ${metric.label}: ${current}; QoQ change ${qoq}; YoY change ${yoy}`);
|
|
39274
|
+
}
|
|
39275
|
+
lines.push(
|
|
39276
|
+
"",
|
|
39277
|
+
"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.",
|
|
39278
|
+
"",
|
|
39279
|
+
"Known exclusions: official Problem Bank List counts, DIF accounting, assessment-rate distributions, and merger-adjusted community-bank prior-period series."
|
|
39280
|
+
);
|
|
39281
|
+
return truncateIfNeeded(
|
|
39282
|
+
lines.join("\n"),
|
|
39283
|
+
CHARACTER_LIMIT,
|
|
39284
|
+
"Use structuredContent for the complete QBP Lite dataset."
|
|
39285
|
+
);
|
|
39286
|
+
}
|
|
39287
|
+
function registerQbpLiteTools(server) {
|
|
39288
|
+
server.registerTool(
|
|
39289
|
+
"fdic_qbp_lite_data",
|
|
39290
|
+
{
|
|
39291
|
+
title: "Generate QBP Lite Data Bundle",
|
|
39292
|
+
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.",
|
|
39293
|
+
inputSchema: QbpLiteSchema,
|
|
39294
|
+
outputSchema: FdicAnalysisOutputSchema,
|
|
39295
|
+
annotations: {
|
|
39296
|
+
readOnlyHint: true,
|
|
39297
|
+
destructiveHint: false,
|
|
39298
|
+
idempotentHint: true,
|
|
39299
|
+
openWorldHint: true
|
|
39300
|
+
}
|
|
39301
|
+
},
|
|
39302
|
+
async (params) => {
|
|
39303
|
+
try {
|
|
39304
|
+
const data = await buildQbpLiteData(params);
|
|
39305
|
+
return {
|
|
39306
|
+
content: [{ type: "text", text: formatQbpLiteText(data) }],
|
|
39307
|
+
structuredContent: data
|
|
39308
|
+
};
|
|
39309
|
+
} catch (err) {
|
|
39310
|
+
return formatToolError(err);
|
|
39311
|
+
}
|
|
39312
|
+
}
|
|
39313
|
+
);
|
|
39314
|
+
}
|
|
39315
|
+
|
|
39316
|
+
// src/tools/chatgptRetrieval.ts
|
|
39317
|
+
var import_zod22 = require("zod");
|
|
38755
39318
|
|
|
38756
39319
|
// src/tools/shared/chatgptUrls.ts
|
|
38757
39320
|
var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
|
|
@@ -38771,11 +39334,11 @@ function getBranchCitationUrl() {
|
|
|
38771
39334
|
}
|
|
38772
39335
|
|
|
38773
39336
|
// src/tools/chatgptRetrieval.ts
|
|
38774
|
-
var SearchInputSchema =
|
|
38775
|
-
query:
|
|
39337
|
+
var SearchInputSchema = import_zod22.z.object({
|
|
39338
|
+
query: import_zod22.z.string().min(1).describe("Natural-language search query.")
|
|
38776
39339
|
});
|
|
38777
|
-
var FetchInputSchema =
|
|
38778
|
-
id:
|
|
39340
|
+
var FetchInputSchema = import_zod22.z.object({
|
|
39341
|
+
id: import_zod22.z.string().min(1).describe(
|
|
38779
39342
|
"Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
|
|
38780
39343
|
)
|
|
38781
39344
|
});
|
|
@@ -39213,7 +39776,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
|
|
|
39213
39776
|
}
|
|
39214
39777
|
|
|
39215
39778
|
// src/tools/chatgptBankDeepDive.ts
|
|
39216
|
-
var
|
|
39779
|
+
var import_zod23 = require("zod");
|
|
39217
39780
|
|
|
39218
39781
|
// src/resources/chatgptAppResources.ts
|
|
39219
39782
|
var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
|
|
@@ -39474,9 +40037,9 @@ function registerChatGptAppResources(server) {
|
|
|
39474
40037
|
}
|
|
39475
40038
|
|
|
39476
40039
|
// src/tools/chatgptBankDeepDive.ts
|
|
39477
|
-
var BankDeepDiveInputSchema =
|
|
39478
|
-
cert:
|
|
39479
|
-
repdte:
|
|
40040
|
+
var BankDeepDiveInputSchema = import_zod23.z.object({
|
|
40041
|
+
cert: import_zod23.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
|
|
40042
|
+
repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
|
|
39480
40043
|
"Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
|
|
39481
40044
|
)
|
|
39482
40045
|
});
|
|
@@ -39775,28 +40338,28 @@ function registerSchemaResources(server) {
|
|
|
39775
40338
|
}
|
|
39776
40339
|
|
|
39777
40340
|
// src/prompts/workflows.ts
|
|
39778
|
-
var
|
|
40341
|
+
var import_zod24 = require("zod");
|
|
39779
40342
|
var BankDeepDiveArgs = {
|
|
39780
|
-
bank:
|
|
39781
|
-
repdte:
|
|
40343
|
+
bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
|
|
40344
|
+
repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe(
|
|
39782
40345
|
"Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
|
|
39783
40346
|
)
|
|
39784
40347
|
};
|
|
39785
40348
|
var FailureForensicsArgs = {
|
|
39786
|
-
bank:
|
|
39787
|
-
lookback_quarters:
|
|
40349
|
+
bank: import_zod24.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
|
|
40350
|
+
lookback_quarters: import_zod24.z.string().regex(/^\d+$/).optional().describe(
|
|
39788
40351
|
"Number of pre-failure quarters to reconstruct (default 12 if omitted)."
|
|
39789
40352
|
)
|
|
39790
40353
|
};
|
|
39791
40354
|
var PortfolioSurveillanceArgs = {
|
|
39792
|
-
scope:
|
|
40355
|
+
scope: import_zod24.z.string().min(1).describe(
|
|
39793
40356
|
"Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
|
|
39794
40357
|
),
|
|
39795
|
-
repdte:
|
|
40358
|
+
repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
|
|
39796
40359
|
};
|
|
39797
40360
|
var ExaminerOverlayArgs = {
|
|
39798
|
-
bank:
|
|
39799
|
-
qualitative_notes:
|
|
40361
|
+
bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
|
|
40362
|
+
qualitative_notes: import_zod24.z.string().optional().describe(
|
|
39800
40363
|
"Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
|
|
39801
40364
|
)
|
|
39802
40365
|
};
|
|
@@ -39969,6 +40532,7 @@ function createServer(options = {}) {
|
|
|
39969
40532
|
registerFranchiseFootprintTools(server);
|
|
39970
40533
|
registerHoldingCompanyProfileTools(server);
|
|
39971
40534
|
registerRegionalContextTools(server);
|
|
40535
|
+
registerQbpLiteTools(server);
|
|
39972
40536
|
}
|
|
39973
40537
|
if (profile.chatgptCanonical || profile.chatgptAliases) {
|
|
39974
40538
|
registerChatGptRetrievalTools(server, {
|
|
@@ -40020,6 +40584,21 @@ function parseAllowedOrigins(rawOrigins, port) {
|
|
|
40020
40584
|
}
|
|
40021
40585
|
var DEFAULT_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
40022
40586
|
var DEFAULT_SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
40587
|
+
var DEFAULT_MCP_RATE_LIMIT_MAX_REQUESTS = 120;
|
|
40588
|
+
var DEFAULT_MCP_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
40589
|
+
var DEFAULT_MCP_STREAM_RATE_LIMIT_MAX_REQUESTS = 2;
|
|
40590
|
+
var DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1e3;
|
|
40591
|
+
var DEFAULT_MCP_MAX_CONCURRENT_STREAMS_PER_IP = 1;
|
|
40592
|
+
function parsePositiveInteger(rawValue, fallback, name) {
|
|
40593
|
+
if (rawValue === void 0 || rawValue.trim().length === 0) {
|
|
40594
|
+
return fallback;
|
|
40595
|
+
}
|
|
40596
|
+
const value = Number.parseInt(rawValue, 10);
|
|
40597
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
40598
|
+
throw new Error(`${name} must be a positive integer. Received: ${rawValue}`);
|
|
40599
|
+
}
|
|
40600
|
+
return value;
|
|
40601
|
+
}
|
|
40023
40602
|
async function closeSession(sessions, sessionId) {
|
|
40024
40603
|
const session = sessions.get(sessionId);
|
|
40025
40604
|
if (!session) {
|
|
@@ -40055,6 +40634,17 @@ function sendInvalidSessionResponse(res) {
|
|
|
40055
40634
|
id: null
|
|
40056
40635
|
});
|
|
40057
40636
|
}
|
|
40637
|
+
function sendMcpRateLimitResponse(res, retryAfterSeconds) {
|
|
40638
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
40639
|
+
res.status(429).json({
|
|
40640
|
+
jsonrpc: "2.0",
|
|
40641
|
+
error: {
|
|
40642
|
+
code: -32e3,
|
|
40643
|
+
message: "Rate limit exceeded. Try again shortly."
|
|
40644
|
+
},
|
|
40645
|
+
id: null
|
|
40646
|
+
});
|
|
40647
|
+
}
|
|
40058
40648
|
function createApp(options = {}) {
|
|
40059
40649
|
const app = (0, import_express2.default)();
|
|
40060
40650
|
const serverFactory = options.serverFactory ?? (() => createServer());
|
|
@@ -40065,6 +40655,29 @@ function createApp(options = {}) {
|
|
|
40065
40655
|
const sessionIdleTimeoutMs = options.sessionIdleTimeoutMs ?? DEFAULT_SESSION_IDLE_TIMEOUT_MS;
|
|
40066
40656
|
const sessionSweepIntervalMs = options.sessionSweepIntervalMs ?? DEFAULT_SESSION_SWEEP_INTERVAL_MS;
|
|
40067
40657
|
const stateless = options.stateless ?? process.env.FDIC_MCP_STATELESS_HTTP === "true";
|
|
40658
|
+
const mcpRateLimiter = options.mcpRateLimiter ?? new RateLimiter({
|
|
40659
|
+
maxRequests: parsePositiveInteger(
|
|
40660
|
+
process.env.MCP_RATE_LIMIT_MAX_REQUESTS_PER_MINUTE,
|
|
40661
|
+
DEFAULT_MCP_RATE_LIMIT_MAX_REQUESTS,
|
|
40662
|
+
"MCP_RATE_LIMIT_MAX_REQUESTS_PER_MINUTE"
|
|
40663
|
+
),
|
|
40664
|
+
windowMs: DEFAULT_MCP_RATE_LIMIT_WINDOW_MS
|
|
40665
|
+
});
|
|
40666
|
+
const mcpStreamRateLimiter = options.mcpStreamRateLimiter ?? new RateLimiter({
|
|
40667
|
+
maxRequests: parsePositiveInteger(
|
|
40668
|
+
process.env.MCP_STREAM_RATE_LIMIT_MAX_REQUESTS_PER_HOUR,
|
|
40669
|
+
DEFAULT_MCP_STREAM_RATE_LIMIT_MAX_REQUESTS,
|
|
40670
|
+
"MCP_STREAM_RATE_LIMIT_MAX_REQUESTS_PER_HOUR"
|
|
40671
|
+
),
|
|
40672
|
+
windowMs: DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS
|
|
40673
|
+
});
|
|
40674
|
+
const mcpStreamConcurrentLimiter = options.mcpStreamConcurrentLimiter ?? new ConcurrentLimiter(
|
|
40675
|
+
parsePositiveInteger(
|
|
40676
|
+
process.env.MCP_MAX_CONCURRENT_STREAMS_PER_IP,
|
|
40677
|
+
DEFAULT_MCP_MAX_CONCURRENT_STREAMS_PER_IP,
|
|
40678
|
+
"MCP_MAX_CONCURRENT_STREAMS_PER_IP"
|
|
40679
|
+
)
|
|
40680
|
+
);
|
|
40068
40681
|
app.use(import_express2.default.json());
|
|
40069
40682
|
if (!stateless) {
|
|
40070
40683
|
const sessionSweepTimer = setInterval(() => {
|
|
@@ -40077,6 +40690,31 @@ function createApp(options = {}) {
|
|
|
40077
40690
|
res.json({ status: "ok", server: "fdic-mcp-server", version: VERSION });
|
|
40078
40691
|
});
|
|
40079
40692
|
app.all("/mcp", async (req, res) => {
|
|
40693
|
+
const requestIp = getRequestIp(req);
|
|
40694
|
+
if (!mcpRateLimiter.check(requestIp)) {
|
|
40695
|
+
sendMcpRateLimitResponse(
|
|
40696
|
+
res,
|
|
40697
|
+
Math.ceil(DEFAULT_MCP_RATE_LIMIT_WINDOW_MS / 1e3)
|
|
40698
|
+
);
|
|
40699
|
+
return;
|
|
40700
|
+
}
|
|
40701
|
+
if (req.method === "GET") {
|
|
40702
|
+
const releaseStream = mcpStreamConcurrentLimiter.acquire(requestIp);
|
|
40703
|
+
if (!releaseStream) {
|
|
40704
|
+
sendMcpRateLimitResponse(res, 60);
|
|
40705
|
+
return;
|
|
40706
|
+
}
|
|
40707
|
+
if (!mcpStreamRateLimiter.check(requestIp)) {
|
|
40708
|
+
releaseStream();
|
|
40709
|
+
sendMcpRateLimitResponse(
|
|
40710
|
+
res,
|
|
40711
|
+
Math.ceil(DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS / 1e3)
|
|
40712
|
+
);
|
|
40713
|
+
return;
|
|
40714
|
+
}
|
|
40715
|
+
res.once("close", releaseStream);
|
|
40716
|
+
res.once("finish", releaseStream);
|
|
40717
|
+
}
|
|
40080
40718
|
if (stateless) {
|
|
40081
40719
|
let server;
|
|
40082
40720
|
let transport;
|