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/index.js
CHANGED
|
@@ -32,7 +32,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
32
32
|
var import_express2 = __toESM(require("express"));
|
|
33
33
|
|
|
34
34
|
// src/constants.ts
|
|
35
|
-
var VERSION = true ? "1.
|
|
35
|
+
var VERSION = true ? "1.30.1" : process.env.npm_package_version ?? "0.0.0-dev";
|
|
36
36
|
var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
|
|
37
37
|
var CHARACTER_LIMIT = 5e4;
|
|
38
38
|
var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
@@ -73,6 +73,42 @@ var RateLimiter = class {
|
|
|
73
73
|
return true;
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
|
+
var ConcurrentLimiter = class {
|
|
77
|
+
maxConcurrent;
|
|
78
|
+
active = /* @__PURE__ */ new Map();
|
|
79
|
+
constructor(maxConcurrent) {
|
|
80
|
+
this.maxConcurrent = maxConcurrent;
|
|
81
|
+
}
|
|
82
|
+
acquire(key) {
|
|
83
|
+
const current = this.active.get(key) ?? 0;
|
|
84
|
+
if (current >= this.maxConcurrent) {
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
this.active.set(key, current + 1);
|
|
88
|
+
let released = false;
|
|
89
|
+
return () => {
|
|
90
|
+
if (released) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
released = true;
|
|
94
|
+
const latest = this.active.get(key) ?? 0;
|
|
95
|
+
if (latest <= 1) {
|
|
96
|
+
this.active.delete(key);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.active.set(key, latest - 1);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/requestIdentity.ts
|
|
105
|
+
function getRequestIp(req) {
|
|
106
|
+
const forwarded = req.get("x-forwarded-for");
|
|
107
|
+
if (forwarded) {
|
|
108
|
+
return forwarded.split(",")[0]?.trim() || req.ip || "unknown";
|
|
109
|
+
}
|
|
110
|
+
return req.ip || "unknown";
|
|
111
|
+
}
|
|
76
112
|
|
|
77
113
|
// src/chat.ts
|
|
78
114
|
var CHAT_SYSTEM_PROMPT = `You are a demo assistant for the FDIC BankFind MCP Server. You help users
|
|
@@ -195,13 +231,6 @@ function mapMessagesToContents(messages) {
|
|
|
195
231
|
parts: [{ text: message.content }]
|
|
196
232
|
}));
|
|
197
233
|
}
|
|
198
|
-
function getRequestIp(req) {
|
|
199
|
-
const forwarded = req.get("x-forwarded-for");
|
|
200
|
-
if (forwarded) {
|
|
201
|
-
return forwarded.split(",")[0]?.trim() || req.ip || "unknown";
|
|
202
|
-
}
|
|
203
|
-
return req.ip || "unknown";
|
|
204
|
-
}
|
|
205
234
|
function getResponseParts(response) {
|
|
206
235
|
return response.candidates?.[0]?.content?.parts ?? [];
|
|
207
236
|
}
|
|
@@ -38735,8 +38764,542 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
|
|
|
38735
38764
|
);
|
|
38736
38765
|
}
|
|
38737
38766
|
|
|
38738
|
-
// src/tools/
|
|
38767
|
+
// src/tools/qbpLite.ts
|
|
38739
38768
|
var import_zod21 = require("zod");
|
|
38769
|
+
var QBP_LITE_FETCH_LIMIT = 1e4;
|
|
38770
|
+
var QBP_LITE_FIELDS = [
|
|
38771
|
+
"CERT",
|
|
38772
|
+
"NAME",
|
|
38773
|
+
"REPDTE",
|
|
38774
|
+
"CB",
|
|
38775
|
+
"SPECGRP",
|
|
38776
|
+
"SPECGRPDESC",
|
|
38777
|
+
"ASSET",
|
|
38778
|
+
"DEP",
|
|
38779
|
+
"DEPDOM",
|
|
38780
|
+
"NETINC",
|
|
38781
|
+
"ROA",
|
|
38782
|
+
"ROE",
|
|
38783
|
+
"NIMY",
|
|
38784
|
+
"ERNAST",
|
|
38785
|
+
"LNLSNET",
|
|
38786
|
+
"LNRE",
|
|
38787
|
+
"LNCI",
|
|
38788
|
+
"LNCON",
|
|
38789
|
+
"LNCRCD",
|
|
38790
|
+
"LNAG",
|
|
38791
|
+
"SC",
|
|
38792
|
+
"EQTOT",
|
|
38793
|
+
"NCLNLSR",
|
|
38794
|
+
"NTLNLSR",
|
|
38795
|
+
"RBC",
|
|
38796
|
+
"RBC1AAJ",
|
|
38797
|
+
"IDT1RWAJR",
|
|
38798
|
+
"RBCT1",
|
|
38799
|
+
"RBCT1C",
|
|
38800
|
+
"RBCT1CER",
|
|
38801
|
+
"RBCRWAJ",
|
|
38802
|
+
"RWAJ",
|
|
38803
|
+
"RWAJT",
|
|
38804
|
+
"P3RER",
|
|
38805
|
+
"P3CIR",
|
|
38806
|
+
"P3CONR",
|
|
38807
|
+
"P3CRCDR",
|
|
38808
|
+
"P3AGR",
|
|
38809
|
+
"NTRER",
|
|
38810
|
+
"NTCIR",
|
|
38811
|
+
"NTCONR",
|
|
38812
|
+
"NTCRCDR",
|
|
38813
|
+
"NTAGR"
|
|
38814
|
+
].join(",");
|
|
38815
|
+
var QbpLiteSchema = import_zod21.z.object({
|
|
38816
|
+
repdte: import_zod21.z.string().regex(/^\d{8}$/).optional().describe(
|
|
38817
|
+
"Quarter-end Report Date (REPDTE) in YYYYMMDD format. If omitted, the tool searches backward from the latest likely published quarter until data is found."
|
|
38818
|
+
),
|
|
38819
|
+
trend_quarters: import_zod21.z.number().int().min(8).max(40).default(20).describe(
|
|
38820
|
+
"Number of quarterly observations to return for trend charts, including the current quarter. Default 20 quarters."
|
|
38821
|
+
),
|
|
38822
|
+
include_community_banks: import_zod21.z.boolean().default(true).describe(
|
|
38823
|
+
"Include a compact community-bank-vs-industry comparison using the public community-bank flag."
|
|
38824
|
+
)
|
|
38825
|
+
});
|
|
38826
|
+
var EXECUTIVE_METRICS = [
|
|
38827
|
+
{
|
|
38828
|
+
id: "institution_count",
|
|
38829
|
+
label: "Number of institutions reporting",
|
|
38830
|
+
unit: "count"
|
|
38831
|
+
},
|
|
38832
|
+
{ id: "total_assets", label: "Total assets", unit: "$thousands" },
|
|
38833
|
+
{
|
|
38834
|
+
id: "total_loans_and_leases",
|
|
38835
|
+
label: "Total loans and leases",
|
|
38836
|
+
unit: "$thousands"
|
|
38837
|
+
},
|
|
38838
|
+
{ id: "domestic_deposits", label: "Domestic deposits", unit: "$thousands" },
|
|
38839
|
+
{ id: "net_income", label: "Net income", unit: "$thousands" },
|
|
38840
|
+
{ id: "roa_pct", label: "Return on assets", unit: "percent" },
|
|
38841
|
+
{ id: "roe_pct", label: "Return on equity", unit: "percent" },
|
|
38842
|
+
{ id: "net_interest_margin_pct", label: "Net interest margin", unit: "percent" },
|
|
38843
|
+
{ id: "noncurrent_loans_pct", label: "Noncurrent loans to loans", unit: "percent" },
|
|
38844
|
+
{ id: "net_chargeoffs_pct", label: "Net charge-offs to loans", unit: "percent" },
|
|
38845
|
+
{ id: "leverage_ratio_pct", label: "Core capital (leverage) ratio", unit: "percent" }
|
|
38846
|
+
];
|
|
38847
|
+
function sumField(records, field) {
|
|
38848
|
+
let total = 0;
|
|
38849
|
+
let seen = false;
|
|
38850
|
+
for (const record of records) {
|
|
38851
|
+
const value = asNumber(record[field]);
|
|
38852
|
+
if (value !== null) {
|
|
38853
|
+
total += value;
|
|
38854
|
+
seen = true;
|
|
38855
|
+
}
|
|
38856
|
+
}
|
|
38857
|
+
return seen ? total : null;
|
|
38858
|
+
}
|
|
38859
|
+
function weightedAverage(records, valueField, weightField) {
|
|
38860
|
+
let weightedSum = 0;
|
|
38861
|
+
let weightSum = 0;
|
|
38862
|
+
for (const record of records) {
|
|
38863
|
+
const value = asNumber(record[valueField]);
|
|
38864
|
+
const weight = asNumber(record[weightField]);
|
|
38865
|
+
if (value !== null && weight !== null && weight > 0) {
|
|
38866
|
+
weightedSum += value * weight;
|
|
38867
|
+
weightSum += weight;
|
|
38868
|
+
}
|
|
38869
|
+
}
|
|
38870
|
+
return weightSum > 0 ? weightedSum / weightSum : null;
|
|
38871
|
+
}
|
|
38872
|
+
function pctChange2(current, prior) {
|
|
38873
|
+
if (current === null || prior === null || prior === 0) return null;
|
|
38874
|
+
return (current - prior) / prior * 100;
|
|
38875
|
+
}
|
|
38876
|
+
function change2(current, prior) {
|
|
38877
|
+
if (current === null || prior === null) return null;
|
|
38878
|
+
return current - prior;
|
|
38879
|
+
}
|
|
38880
|
+
function dividePct(numerator, denominator) {
|
|
38881
|
+
if (numerator === null || denominator === null || denominator === 0) return null;
|
|
38882
|
+
return numerator / denominator * 100;
|
|
38883
|
+
}
|
|
38884
|
+
function sumRatioPct(records, numeratorField, denominatorField) {
|
|
38885
|
+
return dividePct(sumField(records, numeratorField), sumField(records, denominatorField));
|
|
38886
|
+
}
|
|
38887
|
+
function aggregateFinancialRecords(records) {
|
|
38888
|
+
const totalAssets = sumField(records, "ASSET");
|
|
38889
|
+
const totalLoans = sumField(records, "LNLSNET");
|
|
38890
|
+
const equityCapital = sumField(records, "EQTOT");
|
|
38891
|
+
return {
|
|
38892
|
+
institution_count: records.length,
|
|
38893
|
+
total_assets: totalAssets,
|
|
38894
|
+
total_loans_and_leases: totalLoans,
|
|
38895
|
+
domestic_deposits: sumField(records, "DEPDOM"),
|
|
38896
|
+
total_deposits: sumField(records, "DEP"),
|
|
38897
|
+
securities: sumField(records, "SC"),
|
|
38898
|
+
equity_capital: equityCapital,
|
|
38899
|
+
net_income: sumField(records, "NETINC"),
|
|
38900
|
+
roa_pct: weightedAverage(records, "ROA", "ASSET"),
|
|
38901
|
+
roe_pct: weightedAverage(records, "ROE", "EQTOT"),
|
|
38902
|
+
net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST"),
|
|
38903
|
+
noncurrent_loans_pct: weightedAverage(records, "NCLNLSR", "LNLSNET"),
|
|
38904
|
+
net_chargeoffs_pct: weightedAverage(records, "NTLNLSR", "LNLSNET"),
|
|
38905
|
+
leverage_ratio_pct: weightedAverage(records, "RBC1AAJ", "ASSET"),
|
|
38906
|
+
common_equity_tier1_ratio_pct: sumRatioPct(records, "RBCT1C", "RWAJ") ?? weightedAverage(records, "RBCT1CER", "RWAJ"),
|
|
38907
|
+
tier1_risk_based_ratio_pct: sumRatioPct(records, "RBCT1", "RWAJ") ?? weightedAverage(records, "IDT1RWAJR", "RWAJ"),
|
|
38908
|
+
total_risk_based_ratio_pct: sumRatioPct(records, "RBC", "RWAJT") ?? weightedAverage(records, "RBCRWAJ", "RWAJT")
|
|
38909
|
+
};
|
|
38910
|
+
}
|
|
38911
|
+
function buildExecutiveSnapshot(current, priorQuarter, yearAgo) {
|
|
38912
|
+
return EXECUTIVE_METRICS.map(({ id, label, unit }) => {
|
|
38913
|
+
const currentValue = current[id];
|
|
38914
|
+
const priorValue = priorQuarter?.[id] ?? null;
|
|
38915
|
+
const yearAgoValue = yearAgo?.[id] ?? null;
|
|
38916
|
+
return {
|
|
38917
|
+
id,
|
|
38918
|
+
label,
|
|
38919
|
+
unit,
|
|
38920
|
+
change_unit: unit === "percent" ? "percentage_points" : unit,
|
|
38921
|
+
current: currentValue,
|
|
38922
|
+
prior_quarter_change: change2(currentValue, priorValue),
|
|
38923
|
+
prior_quarter_change_pct: unit === "percent" ? null : pctChange2(currentValue, priorValue),
|
|
38924
|
+
year_over_year_change: change2(currentValue, yearAgoValue),
|
|
38925
|
+
year_over_year_change_pct: unit === "percent" ? null : pctChange2(currentValue, yearAgoValue)
|
|
38926
|
+
};
|
|
38927
|
+
});
|
|
38928
|
+
}
|
|
38929
|
+
function isCommunityBank(record) {
|
|
38930
|
+
const value = record.CB;
|
|
38931
|
+
return value === 1 || value === "1";
|
|
38932
|
+
}
|
|
38933
|
+
function assetSizeGroup(asset) {
|
|
38934
|
+
if (asset === null) return "Unknown";
|
|
38935
|
+
if (asset < 1e5) return "Assets < $100 Million";
|
|
38936
|
+
if (asset < 1e6) return "Assets $100 Million - $1 Billion";
|
|
38937
|
+
if (asset < 1e7) return "Assets $1 Billion - $10 Billion";
|
|
38938
|
+
if (asset < 25e7) return "Assets $10 Billion - $250 Billion";
|
|
38939
|
+
return "Assets > $250 Billion";
|
|
38940
|
+
}
|
|
38941
|
+
function buildSeriesPoint(repdte, records) {
|
|
38942
|
+
const metrics = aggregateFinancialRecords(records);
|
|
38943
|
+
return {
|
|
38944
|
+
repdte,
|
|
38945
|
+
...metrics
|
|
38946
|
+
};
|
|
38947
|
+
}
|
|
38948
|
+
function buildAssetSizeNimSeries(groupedRecords) {
|
|
38949
|
+
const rows = [];
|
|
38950
|
+
for (const [repdte, records] of groupedRecords) {
|
|
38951
|
+
const groups = /* @__PURE__ */ new Map();
|
|
38952
|
+
for (const record of records) {
|
|
38953
|
+
const group = assetSizeGroup(asNumber(record.ASSET));
|
|
38954
|
+
const groupRecords = groups.get(group);
|
|
38955
|
+
if (groupRecords) {
|
|
38956
|
+
groupRecords.push(record);
|
|
38957
|
+
} else {
|
|
38958
|
+
groups.set(group, [record]);
|
|
38959
|
+
}
|
|
38960
|
+
}
|
|
38961
|
+
rows.push({
|
|
38962
|
+
repdte,
|
|
38963
|
+
group: "Industry",
|
|
38964
|
+
institution_count: records.length,
|
|
38965
|
+
total_assets: sumField(records, "ASSET"),
|
|
38966
|
+
net_interest_margin_pct: weightedAverage(records, "NIMY", "ERNAST")
|
|
38967
|
+
});
|
|
38968
|
+
for (const [group, groupRecords] of groups) {
|
|
38969
|
+
rows.push({
|
|
38970
|
+
repdte,
|
|
38971
|
+
group,
|
|
38972
|
+
institution_count: groupRecords.length,
|
|
38973
|
+
total_assets: sumField(groupRecords, "ASSET"),
|
|
38974
|
+
net_interest_margin_pct: weightedAverage(groupRecords, "NIMY", "ERNAST")
|
|
38975
|
+
});
|
|
38976
|
+
}
|
|
38977
|
+
}
|
|
38978
|
+
return rows;
|
|
38979
|
+
}
|
|
38980
|
+
function buildLoanAndDepositSeries(series) {
|
|
38981
|
+
const byDate = new Map(series.map((point) => [point.repdte, point]));
|
|
38982
|
+
return series.map((point) => {
|
|
38983
|
+
const priorQuarter = getPriorQuarterDates(point.repdte, 1)[0];
|
|
38984
|
+
const yearAgo = getReportDateOneYearPrior(point.repdte);
|
|
38985
|
+
const priorPoint = byDate.get(priorQuarter);
|
|
38986
|
+
const yearAgoPoint = byDate.get(yearAgo);
|
|
38987
|
+
return {
|
|
38988
|
+
repdte: point.repdte,
|
|
38989
|
+
total_loans_and_leases: point.total_loans_and_leases,
|
|
38990
|
+
quarterly_loan_change: change2(
|
|
38991
|
+
point.total_loans_and_leases,
|
|
38992
|
+
priorPoint?.total_loans_and_leases ?? null
|
|
38993
|
+
),
|
|
38994
|
+
loan_growth_12_month_pct: pctChange2(
|
|
38995
|
+
point.total_loans_and_leases,
|
|
38996
|
+
yearAgoPoint?.total_loans_and_leases ?? null
|
|
38997
|
+
),
|
|
38998
|
+
domestic_deposits: point.domestic_deposits,
|
|
38999
|
+
quarterly_domestic_deposit_change: change2(
|
|
39000
|
+
point.domestic_deposits,
|
|
39001
|
+
priorPoint?.domestic_deposits ?? null
|
|
39002
|
+
),
|
|
39003
|
+
domestic_deposit_growth_12_month_pct: pctChange2(
|
|
39004
|
+
point.domestic_deposits,
|
|
39005
|
+
yearAgoPoint?.domestic_deposits ?? null
|
|
39006
|
+
)
|
|
39007
|
+
};
|
|
39008
|
+
});
|
|
39009
|
+
}
|
|
39010
|
+
function buildPortfolioPerformance(records) {
|
|
39011
|
+
const portfolios = [
|
|
39012
|
+
{
|
|
39013
|
+
id: "real_estate",
|
|
39014
|
+
label: "Real estate loans",
|
|
39015
|
+
balanceField: "LNRE",
|
|
39016
|
+
noncurrentRateField: "P3RER",
|
|
39017
|
+
chargeoffRateField: "NTRER"
|
|
39018
|
+
},
|
|
39019
|
+
{
|
|
39020
|
+
id: "commercial_industrial",
|
|
39021
|
+
label: "Commercial and industrial loans",
|
|
39022
|
+
balanceField: "LNCI",
|
|
39023
|
+
noncurrentRateField: "P3CIR",
|
|
39024
|
+
chargeoffRateField: "NTCIR"
|
|
39025
|
+
},
|
|
39026
|
+
{
|
|
39027
|
+
id: "consumer",
|
|
39028
|
+
label: "Consumer loans",
|
|
39029
|
+
balanceField: "LNCON",
|
|
39030
|
+
noncurrentRateField: "P3CONR",
|
|
39031
|
+
chargeoffRateField: "NTCONR"
|
|
39032
|
+
},
|
|
39033
|
+
{
|
|
39034
|
+
id: "credit_card",
|
|
39035
|
+
label: "Credit card loans",
|
|
39036
|
+
balanceField: "LNCRCD",
|
|
39037
|
+
noncurrentRateField: "P3CRCDR",
|
|
39038
|
+
chargeoffRateField: "NTCRCDR"
|
|
39039
|
+
},
|
|
39040
|
+
{
|
|
39041
|
+
id: "farm",
|
|
39042
|
+
label: "Farm loans",
|
|
39043
|
+
balanceField: "LNAG",
|
|
39044
|
+
noncurrentRateField: "P3AGR",
|
|
39045
|
+
chargeoffRateField: "NTAGR"
|
|
39046
|
+
}
|
|
39047
|
+
];
|
|
39048
|
+
return portfolios.map((portfolio) => ({
|
|
39049
|
+
id: portfolio.id,
|
|
39050
|
+
label: portfolio.label,
|
|
39051
|
+
balance: sumField(records, portfolio.balanceField),
|
|
39052
|
+
balance_share_of_total_loans_pct: dividePct(
|
|
39053
|
+
sumField(records, portfolio.balanceField),
|
|
39054
|
+
sumField(records, "LNLSNET")
|
|
39055
|
+
),
|
|
39056
|
+
noncurrent_rate_pct: weightedAverage(
|
|
39057
|
+
records,
|
|
39058
|
+
portfolio.noncurrentRateField,
|
|
39059
|
+
portfolio.balanceField
|
|
39060
|
+
),
|
|
39061
|
+
net_chargeoff_rate_pct: weightedAverage(
|
|
39062
|
+
records,
|
|
39063
|
+
portfolio.chargeoffRateField,
|
|
39064
|
+
portfolio.balanceField
|
|
39065
|
+
)
|
|
39066
|
+
}));
|
|
39067
|
+
}
|
|
39068
|
+
function buildCommunityComparison(groupedRecords) {
|
|
39069
|
+
const rows = [];
|
|
39070
|
+
for (const [repdte, records] of groupedRecords) {
|
|
39071
|
+
for (const [group, groupRecords] of [
|
|
39072
|
+
["Industry", records],
|
|
39073
|
+
["Community Banks", records.filter(isCommunityBank)]
|
|
39074
|
+
]) {
|
|
39075
|
+
const metrics = aggregateFinancialRecords(groupRecords);
|
|
39076
|
+
rows.push({
|
|
39077
|
+
repdte,
|
|
39078
|
+
group,
|
|
39079
|
+
institution_count: metrics.institution_count,
|
|
39080
|
+
total_assets: metrics.total_assets,
|
|
39081
|
+
total_loans_and_leases: metrics.total_loans_and_leases,
|
|
39082
|
+
domestic_deposits: metrics.domestic_deposits,
|
|
39083
|
+
net_income: metrics.net_income,
|
|
39084
|
+
roa_pct: metrics.roa_pct,
|
|
39085
|
+
net_interest_margin_pct: metrics.net_interest_margin_pct,
|
|
39086
|
+
noncurrent_loans_pct: metrics.noncurrent_loans_pct,
|
|
39087
|
+
net_chargeoffs_pct: metrics.net_chargeoffs_pct
|
|
39088
|
+
});
|
|
39089
|
+
}
|
|
39090
|
+
}
|
|
39091
|
+
return rows;
|
|
39092
|
+
}
|
|
39093
|
+
function buildTruncationWarning2(repdte, returnedCount, totalCount, limit = QBP_LITE_FETCH_LIMIT) {
|
|
39094
|
+
if (returnedCount >= limit && totalCount > limit) {
|
|
39095
|
+
return `REPDTE ${repdte}: results truncated at ${limit.toLocaleString()} of ${totalCount.toLocaleString()} institutions.`;
|
|
39096
|
+
}
|
|
39097
|
+
return null;
|
|
39098
|
+
}
|
|
39099
|
+
async function fetchRecordsForRepdte(repdte) {
|
|
39100
|
+
const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
|
|
39101
|
+
filters: `REPDTE:${repdte}`,
|
|
39102
|
+
fields: QBP_LITE_FIELDS,
|
|
39103
|
+
limit: QBP_LITE_FETCH_LIMIT,
|
|
39104
|
+
sort_by: "CERT",
|
|
39105
|
+
sort_order: "ASC"
|
|
39106
|
+
});
|
|
39107
|
+
const records = extractRecords(response);
|
|
39108
|
+
return {
|
|
39109
|
+
records,
|
|
39110
|
+
total: response.meta.total,
|
|
39111
|
+
truncationWarning: buildTruncationWarning2(
|
|
39112
|
+
repdte,
|
|
39113
|
+
records.length,
|
|
39114
|
+
response.meta.total
|
|
39115
|
+
)
|
|
39116
|
+
};
|
|
39117
|
+
}
|
|
39118
|
+
async function hasFinancialData(repdte) {
|
|
39119
|
+
const response = await queryEndpoint(ENDPOINTS.FINANCIALS, {
|
|
39120
|
+
filters: `REPDTE:${repdte}`,
|
|
39121
|
+
fields: "CERT,REPDTE",
|
|
39122
|
+
limit: 1
|
|
39123
|
+
});
|
|
39124
|
+
return response.meta.total > 0;
|
|
39125
|
+
}
|
|
39126
|
+
async function resolveReportDate(params) {
|
|
39127
|
+
if (params.repdte) {
|
|
39128
|
+
const validationError = validateQuarterEndDate(params.repdte, "repdte");
|
|
39129
|
+
if (validationError) {
|
|
39130
|
+
throw new Error(validationError);
|
|
39131
|
+
}
|
|
39132
|
+
return params.repdte;
|
|
39133
|
+
}
|
|
39134
|
+
const candidates = [getDefaultReportDate(), ...getPriorQuarterDates(getDefaultReportDate(), 7)];
|
|
39135
|
+
for (const candidate of candidates) {
|
|
39136
|
+
if (await hasFinancialData(candidate)) {
|
|
39137
|
+
return candidate;
|
|
39138
|
+
}
|
|
39139
|
+
}
|
|
39140
|
+
throw new Error("No BankFind financial data found in the latest eight expected reporting quarters.");
|
|
39141
|
+
}
|
|
39142
|
+
async function buildQbpLiteData(params) {
|
|
39143
|
+
const trendQuarters = params.trend_quarters ?? 20;
|
|
39144
|
+
const includeCommunityBanks = params.include_community_banks ?? true;
|
|
39145
|
+
const repdte = await resolveReportDate(params);
|
|
39146
|
+
const priorQuarterRepdte = getPriorQuarterDates(repdte, 1)[0];
|
|
39147
|
+
const yearAgoRepdte = getReportDateOneYearPrior(repdte);
|
|
39148
|
+
const trendDates = [...getPriorQuarterDates(repdte, trendQuarters - 1)].reverse();
|
|
39149
|
+
const requiredDates = [.../* @__PURE__ */ new Set([...trendDates, repdte, priorQuarterRepdte, yearAgoRepdte])].sort();
|
|
39150
|
+
const recordsByDate = /* @__PURE__ */ new Map();
|
|
39151
|
+
const warnings = [];
|
|
39152
|
+
await mapWithConcurrency(requiredDates, 4, async (date) => {
|
|
39153
|
+
const { records, truncationWarning } = await fetchRecordsForRepdte(date);
|
|
39154
|
+
recordsByDate.set(date, records);
|
|
39155
|
+
if (records.length === 0) {
|
|
39156
|
+
warnings.push(`No financial records found for REPDTE ${date}.`);
|
|
39157
|
+
}
|
|
39158
|
+
if (truncationWarning) {
|
|
39159
|
+
warnings.push(truncationWarning);
|
|
39160
|
+
}
|
|
39161
|
+
});
|
|
39162
|
+
const currentRecords = recordsByDate.get(repdte) ?? [];
|
|
39163
|
+
if (currentRecords.length === 0) {
|
|
39164
|
+
throw new Error(`No financial records found for current REPDTE ${repdte}.`);
|
|
39165
|
+
}
|
|
39166
|
+
const trendRecords = new Map(
|
|
39167
|
+
[...trendDates, repdte].filter((date) => (recordsByDate.get(date)?.length ?? 0) > 0).map((date) => [date, recordsByDate.get(date) ?? []])
|
|
39168
|
+
);
|
|
39169
|
+
const trendSeries = [...trendRecords.entries()].map(
|
|
39170
|
+
([date, records]) => buildSeriesPoint(date, records)
|
|
39171
|
+
);
|
|
39172
|
+
const current = aggregateFinancialRecords(currentRecords);
|
|
39173
|
+
const priorQuarter = recordsByDate.has(priorQuarterRepdte) ? aggregateFinancialRecords(recordsByDate.get(priorQuarterRepdte) ?? []) : null;
|
|
39174
|
+
const yearAgo = recordsByDate.has(yearAgoRepdte) ? aggregateFinancialRecords(recordsByDate.get(yearAgoRepdte) ?? []) : null;
|
|
39175
|
+
return {
|
|
39176
|
+
report: {
|
|
39177
|
+
title: "QBP Lite: FDIC-Insured Institutions",
|
|
39178
|
+
repdte,
|
|
39179
|
+
prior_quarter_repdte: priorQuarterRepdte,
|
|
39180
|
+
year_ago_repdte: yearAgoRepdte,
|
|
39181
|
+
trend_start_repdte: trendSeries[0]?.repdte ?? repdte,
|
|
39182
|
+
trend_end_repdte: repdte
|
|
39183
|
+
},
|
|
39184
|
+
executive_snapshot: buildExecutiveSnapshot(current, priorQuarter, yearAgo),
|
|
39185
|
+
charts: {
|
|
39186
|
+
quarterly_net_income_and_roa: trendSeries.map((point) => ({
|
|
39187
|
+
repdte: point.repdte,
|
|
39188
|
+
net_income: point.net_income,
|
|
39189
|
+
roa_pct: point.roa_pct
|
|
39190
|
+
})),
|
|
39191
|
+
net_interest_margin_by_asset_size: buildAssetSizeNimSeries(trendRecords),
|
|
39192
|
+
loans_and_deposits: buildLoanAndDepositSeries(trendSeries),
|
|
39193
|
+
credit_quality: trendSeries.map((point) => ({
|
|
39194
|
+
repdte: point.repdte,
|
|
39195
|
+
noncurrent_loans_pct: point.noncurrent_loans_pct,
|
|
39196
|
+
net_chargeoffs_pct: point.net_chargeoffs_pct
|
|
39197
|
+
})),
|
|
39198
|
+
loan_performance_by_portfolio: buildPortfolioPerformance(currentRecords),
|
|
39199
|
+
capital_ratios: trendSeries.map((point) => ({
|
|
39200
|
+
repdte: point.repdte,
|
|
39201
|
+
leverage_ratio_pct: point.leverage_ratio_pct,
|
|
39202
|
+
common_equity_tier1_ratio_pct: point.common_equity_tier1_ratio_pct,
|
|
39203
|
+
tier1_risk_based_ratio_pct: point.tier1_risk_based_ratio_pct,
|
|
39204
|
+
total_risk_based_ratio_pct: point.total_risk_based_ratio_pct
|
|
39205
|
+
})),
|
|
39206
|
+
community_banks_vs_industry: includeCommunityBanks ? buildCommunityComparison(trendRecords) : []
|
|
39207
|
+
},
|
|
39208
|
+
data_notes: {
|
|
39209
|
+
source_dataset: "FDIC BankFind financials endpoint",
|
|
39210
|
+
date_field: "REPDTE",
|
|
39211
|
+
dollar_units: "Thousands of dollars unless converted by the client.",
|
|
39212
|
+
aggregation_notes: [
|
|
39213
|
+
"Dollar fields are summed across reporting institutions.",
|
|
39214
|
+
"ROA, ROE, credit-quality ratios, and leverage ratio are weighted using the closest available public denominator field.",
|
|
39215
|
+
"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.",
|
|
39216
|
+
"Net interest margin is weighted by earning assets.",
|
|
39217
|
+
"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.",
|
|
39218
|
+
"The report uses public quarterly Call Report-derived BankFind data; it is not an official FDIC QBP publication."
|
|
39219
|
+
],
|
|
39220
|
+
known_exclusions: [
|
|
39221
|
+
"Official Problem Bank List counts are excluded because they depend on confidential CAMELS ratings.",
|
|
39222
|
+
"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.",
|
|
39223
|
+
"Assessment-rate distribution data is excluded because public BankFind financials do not provide institution assessment-rate ranges.",
|
|
39224
|
+
"Community-bank merger-adjusted prior-period series are excluded; community-bank comparisons use the public CB flag available in BankFind records."
|
|
39225
|
+
],
|
|
39226
|
+
warnings
|
|
39227
|
+
}
|
|
39228
|
+
};
|
|
39229
|
+
}
|
|
39230
|
+
function formatMetricValue2(value, unit) {
|
|
39231
|
+
if (value === null) return "n/a";
|
|
39232
|
+
if (unit === "percent") return `${value.toFixed(2)}%`;
|
|
39233
|
+
return Math.round(value).toLocaleString();
|
|
39234
|
+
}
|
|
39235
|
+
function formatChangeValue(value, unit) {
|
|
39236
|
+
if (value === null) return "n/a";
|
|
39237
|
+
if (unit === "percentage_points") return `${value.toFixed(2)} ppts`;
|
|
39238
|
+
return Math.round(value).toLocaleString();
|
|
39239
|
+
}
|
|
39240
|
+
function formatQbpLiteText(data) {
|
|
39241
|
+
const lines = [
|
|
39242
|
+
`${data.report.title}`,
|
|
39243
|
+
`Quarter ended: ${data.report.repdte}`,
|
|
39244
|
+
`Prior quarter: ${data.report.prior_quarter_repdte} | Year ago: ${data.report.year_ago_repdte}`,
|
|
39245
|
+
"",
|
|
39246
|
+
"Executive Snapshot"
|
|
39247
|
+
];
|
|
39248
|
+
for (const metric of data.executive_snapshot) {
|
|
39249
|
+
const current = formatMetricValue2(metric.current, metric.unit);
|
|
39250
|
+
const qoq = formatChangeValue(
|
|
39251
|
+
metric.prior_quarter_change,
|
|
39252
|
+
metric.change_unit
|
|
39253
|
+
);
|
|
39254
|
+
const yoy = formatChangeValue(
|
|
39255
|
+
metric.year_over_year_change,
|
|
39256
|
+
metric.change_unit
|
|
39257
|
+
);
|
|
39258
|
+
lines.push(`- ${metric.label}: ${current}; QoQ change ${qoq}; YoY change ${yoy}`);
|
|
39259
|
+
}
|
|
39260
|
+
lines.push(
|
|
39261
|
+
"",
|
|
39262
|
+
"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.",
|
|
39263
|
+
"",
|
|
39264
|
+
"Known exclusions: official Problem Bank List counts, DIF accounting, assessment-rate distributions, and merger-adjusted community-bank prior-period series."
|
|
39265
|
+
);
|
|
39266
|
+
return truncateIfNeeded(
|
|
39267
|
+
lines.join("\n"),
|
|
39268
|
+
CHARACTER_LIMIT,
|
|
39269
|
+
"Use structuredContent for the complete QBP Lite dataset."
|
|
39270
|
+
);
|
|
39271
|
+
}
|
|
39272
|
+
function registerQbpLiteTools(server) {
|
|
39273
|
+
server.registerTool(
|
|
39274
|
+
"fdic_qbp_lite_data",
|
|
39275
|
+
{
|
|
39276
|
+
title: "Generate QBP Lite Data Bundle",
|
|
39277
|
+
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.",
|
|
39278
|
+
inputSchema: QbpLiteSchema,
|
|
39279
|
+
outputSchema: FdicAnalysisOutputSchema,
|
|
39280
|
+
annotations: {
|
|
39281
|
+
readOnlyHint: true,
|
|
39282
|
+
destructiveHint: false,
|
|
39283
|
+
idempotentHint: true,
|
|
39284
|
+
openWorldHint: true
|
|
39285
|
+
}
|
|
39286
|
+
},
|
|
39287
|
+
async (params) => {
|
|
39288
|
+
try {
|
|
39289
|
+
const data = await buildQbpLiteData(params);
|
|
39290
|
+
return {
|
|
39291
|
+
content: [{ type: "text", text: formatQbpLiteText(data) }],
|
|
39292
|
+
structuredContent: data
|
|
39293
|
+
};
|
|
39294
|
+
} catch (err) {
|
|
39295
|
+
return formatToolError(err);
|
|
39296
|
+
}
|
|
39297
|
+
}
|
|
39298
|
+
);
|
|
39299
|
+
}
|
|
39300
|
+
|
|
39301
|
+
// src/tools/chatgptRetrieval.ts
|
|
39302
|
+
var import_zod22 = require("zod");
|
|
38740
39303
|
|
|
38741
39304
|
// src/tools/shared/chatgptUrls.ts
|
|
38742
39305
|
var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
|
|
@@ -38756,11 +39319,11 @@ function getBranchCitationUrl() {
|
|
|
38756
39319
|
}
|
|
38757
39320
|
|
|
38758
39321
|
// src/tools/chatgptRetrieval.ts
|
|
38759
|
-
var SearchInputSchema =
|
|
38760
|
-
query:
|
|
39322
|
+
var SearchInputSchema = import_zod22.z.object({
|
|
39323
|
+
query: import_zod22.z.string().min(1).describe("Natural-language search query.")
|
|
38761
39324
|
});
|
|
38762
|
-
var FetchInputSchema =
|
|
38763
|
-
id:
|
|
39325
|
+
var FetchInputSchema = import_zod22.z.object({
|
|
39326
|
+
id: import_zod22.z.string().min(1).describe(
|
|
38764
39327
|
"Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
|
|
38765
39328
|
)
|
|
38766
39329
|
});
|
|
@@ -39198,7 +39761,7 @@ function registerChatGptRetrievalTools(server, options = {}) {
|
|
|
39198
39761
|
}
|
|
39199
39762
|
|
|
39200
39763
|
// src/tools/chatgptBankDeepDive.ts
|
|
39201
|
-
var
|
|
39764
|
+
var import_zod23 = require("zod");
|
|
39202
39765
|
|
|
39203
39766
|
// src/resources/chatgptAppResources.ts
|
|
39204
39767
|
var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
|
|
@@ -39459,9 +40022,9 @@ function registerChatGptAppResources(server) {
|
|
|
39459
40022
|
}
|
|
39460
40023
|
|
|
39461
40024
|
// src/tools/chatgptBankDeepDive.ts
|
|
39462
|
-
var BankDeepDiveInputSchema =
|
|
39463
|
-
cert:
|
|
39464
|
-
repdte:
|
|
40025
|
+
var BankDeepDiveInputSchema = import_zod23.z.object({
|
|
40026
|
+
cert: import_zod23.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
|
|
40027
|
+
repdte: import_zod23.z.string().regex(/^\d{8}$/).optional().describe(
|
|
39465
40028
|
"Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
|
|
39466
40029
|
)
|
|
39467
40030
|
});
|
|
@@ -39760,28 +40323,28 @@ function registerSchemaResources(server) {
|
|
|
39760
40323
|
}
|
|
39761
40324
|
|
|
39762
40325
|
// src/prompts/workflows.ts
|
|
39763
|
-
var
|
|
40326
|
+
var import_zod24 = require("zod");
|
|
39764
40327
|
var BankDeepDiveArgs = {
|
|
39765
|
-
bank:
|
|
39766
|
-
repdte:
|
|
40328
|
+
bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
|
|
40329
|
+
repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe(
|
|
39767
40330
|
"Optional quarter-end report date in YYYYMMDD format (0331, 0630, 0930, or 1231)."
|
|
39768
40331
|
)
|
|
39769
40332
|
};
|
|
39770
40333
|
var FailureForensicsArgs = {
|
|
39771
|
-
bank:
|
|
39772
|
-
lookback_quarters:
|
|
40334
|
+
bank: import_zod24.z.string().min(1).describe("Failed bank name or FDIC Certificate Number (CERT)."),
|
|
40335
|
+
lookback_quarters: import_zod24.z.string().regex(/^\d+$/).optional().describe(
|
|
39773
40336
|
"Number of pre-failure quarters to reconstruct (default 12 if omitted)."
|
|
39774
40337
|
)
|
|
39775
40338
|
};
|
|
39776
40339
|
var PortfolioSurveillanceArgs = {
|
|
39777
|
-
scope:
|
|
40340
|
+
scope: import_zod24.z.string().min(1).describe(
|
|
39778
40341
|
"Universe to screen \u2014 e.g., 'state:NC', 'asset_min:1000000,asset_max:10000000', or a comma-separated CERT list ('certs:3511,29846,...')."
|
|
39779
40342
|
),
|
|
39780
|
-
repdte:
|
|
40343
|
+
repdte: import_zod24.z.string().regex(/^\d{8}$/).optional().describe("Optional quarter-end report date in YYYYMMDD format.")
|
|
39781
40344
|
};
|
|
39782
40345
|
var ExaminerOverlayArgs = {
|
|
39783
|
-
bank:
|
|
39784
|
-
qualitative_notes:
|
|
40346
|
+
bank: import_zod24.z.string().min(1).describe("Bank name or FDIC Certificate Number (CERT)."),
|
|
40347
|
+
qualitative_notes: import_zod24.z.string().optional().describe(
|
|
39785
40348
|
"Optional qualitative analyst inputs (management quality, governance, exam findings) to overlay onto the public proxy assessment."
|
|
39786
40349
|
)
|
|
39787
40350
|
};
|
|
@@ -39954,6 +40517,7 @@ function createServer(options = {}) {
|
|
|
39954
40517
|
registerFranchiseFootprintTools(server);
|
|
39955
40518
|
registerHoldingCompanyProfileTools(server);
|
|
39956
40519
|
registerRegionalContextTools(server);
|
|
40520
|
+
registerQbpLiteTools(server);
|
|
39957
40521
|
}
|
|
39958
40522
|
if (profile.chatgptCanonical || profile.chatgptAliases) {
|
|
39959
40523
|
registerChatGptRetrievalTools(server, {
|
|
@@ -40005,6 +40569,21 @@ function parseAllowedOrigins(rawOrigins, port) {
|
|
|
40005
40569
|
}
|
|
40006
40570
|
var DEFAULT_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
40007
40571
|
var DEFAULT_SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
40572
|
+
var DEFAULT_MCP_RATE_LIMIT_MAX_REQUESTS = 120;
|
|
40573
|
+
var DEFAULT_MCP_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
40574
|
+
var DEFAULT_MCP_STREAM_RATE_LIMIT_MAX_REQUESTS = 2;
|
|
40575
|
+
var DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS = 60 * 60 * 1e3;
|
|
40576
|
+
var DEFAULT_MCP_MAX_CONCURRENT_STREAMS_PER_IP = 1;
|
|
40577
|
+
function parsePositiveInteger(rawValue, fallback, name) {
|
|
40578
|
+
if (rawValue === void 0 || rawValue.trim().length === 0) {
|
|
40579
|
+
return fallback;
|
|
40580
|
+
}
|
|
40581
|
+
const value = Number.parseInt(rawValue, 10);
|
|
40582
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
40583
|
+
throw new Error(`${name} must be a positive integer. Received: ${rawValue}`);
|
|
40584
|
+
}
|
|
40585
|
+
return value;
|
|
40586
|
+
}
|
|
40008
40587
|
async function closeSession(sessions, sessionId) {
|
|
40009
40588
|
const session = sessions.get(sessionId);
|
|
40010
40589
|
if (!session) {
|
|
@@ -40040,6 +40619,17 @@ function sendInvalidSessionResponse(res) {
|
|
|
40040
40619
|
id: null
|
|
40041
40620
|
});
|
|
40042
40621
|
}
|
|
40622
|
+
function sendMcpRateLimitResponse(res, retryAfterSeconds) {
|
|
40623
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
40624
|
+
res.status(429).json({
|
|
40625
|
+
jsonrpc: "2.0",
|
|
40626
|
+
error: {
|
|
40627
|
+
code: -32e3,
|
|
40628
|
+
message: "Rate limit exceeded. Try again shortly."
|
|
40629
|
+
},
|
|
40630
|
+
id: null
|
|
40631
|
+
});
|
|
40632
|
+
}
|
|
40043
40633
|
function createApp(options = {}) {
|
|
40044
40634
|
const app = (0, import_express2.default)();
|
|
40045
40635
|
const serverFactory = options.serverFactory ?? (() => createServer());
|
|
@@ -40050,6 +40640,29 @@ function createApp(options = {}) {
|
|
|
40050
40640
|
const sessionIdleTimeoutMs = options.sessionIdleTimeoutMs ?? DEFAULT_SESSION_IDLE_TIMEOUT_MS;
|
|
40051
40641
|
const sessionSweepIntervalMs = options.sessionSweepIntervalMs ?? DEFAULT_SESSION_SWEEP_INTERVAL_MS;
|
|
40052
40642
|
const stateless = options.stateless ?? process.env.FDIC_MCP_STATELESS_HTTP === "true";
|
|
40643
|
+
const mcpRateLimiter = options.mcpRateLimiter ?? new RateLimiter({
|
|
40644
|
+
maxRequests: parsePositiveInteger(
|
|
40645
|
+
process.env.MCP_RATE_LIMIT_MAX_REQUESTS_PER_MINUTE,
|
|
40646
|
+
DEFAULT_MCP_RATE_LIMIT_MAX_REQUESTS,
|
|
40647
|
+
"MCP_RATE_LIMIT_MAX_REQUESTS_PER_MINUTE"
|
|
40648
|
+
),
|
|
40649
|
+
windowMs: DEFAULT_MCP_RATE_LIMIT_WINDOW_MS
|
|
40650
|
+
});
|
|
40651
|
+
const mcpStreamRateLimiter = options.mcpStreamRateLimiter ?? new RateLimiter({
|
|
40652
|
+
maxRequests: parsePositiveInteger(
|
|
40653
|
+
process.env.MCP_STREAM_RATE_LIMIT_MAX_REQUESTS_PER_HOUR,
|
|
40654
|
+
DEFAULT_MCP_STREAM_RATE_LIMIT_MAX_REQUESTS,
|
|
40655
|
+
"MCP_STREAM_RATE_LIMIT_MAX_REQUESTS_PER_HOUR"
|
|
40656
|
+
),
|
|
40657
|
+
windowMs: DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS
|
|
40658
|
+
});
|
|
40659
|
+
const mcpStreamConcurrentLimiter = options.mcpStreamConcurrentLimiter ?? new ConcurrentLimiter(
|
|
40660
|
+
parsePositiveInteger(
|
|
40661
|
+
process.env.MCP_MAX_CONCURRENT_STREAMS_PER_IP,
|
|
40662
|
+
DEFAULT_MCP_MAX_CONCURRENT_STREAMS_PER_IP,
|
|
40663
|
+
"MCP_MAX_CONCURRENT_STREAMS_PER_IP"
|
|
40664
|
+
)
|
|
40665
|
+
);
|
|
40053
40666
|
app.use(import_express2.default.json());
|
|
40054
40667
|
if (!stateless) {
|
|
40055
40668
|
const sessionSweepTimer = setInterval(() => {
|
|
@@ -40062,6 +40675,31 @@ function createApp(options = {}) {
|
|
|
40062
40675
|
res.json({ status: "ok", server: "fdic-mcp-server", version: VERSION });
|
|
40063
40676
|
});
|
|
40064
40677
|
app.all("/mcp", async (req, res) => {
|
|
40678
|
+
const requestIp = getRequestIp(req);
|
|
40679
|
+
if (!mcpRateLimiter.check(requestIp)) {
|
|
40680
|
+
sendMcpRateLimitResponse(
|
|
40681
|
+
res,
|
|
40682
|
+
Math.ceil(DEFAULT_MCP_RATE_LIMIT_WINDOW_MS / 1e3)
|
|
40683
|
+
);
|
|
40684
|
+
return;
|
|
40685
|
+
}
|
|
40686
|
+
if (req.method === "GET") {
|
|
40687
|
+
const releaseStream = mcpStreamConcurrentLimiter.acquire(requestIp);
|
|
40688
|
+
if (!releaseStream) {
|
|
40689
|
+
sendMcpRateLimitResponse(res, 60);
|
|
40690
|
+
return;
|
|
40691
|
+
}
|
|
40692
|
+
if (!mcpStreamRateLimiter.check(requestIp)) {
|
|
40693
|
+
releaseStream();
|
|
40694
|
+
sendMcpRateLimitResponse(
|
|
40695
|
+
res,
|
|
40696
|
+
Math.ceil(DEFAULT_MCP_STREAM_RATE_LIMIT_WINDOW_MS / 1e3)
|
|
40697
|
+
);
|
|
40698
|
+
return;
|
|
40699
|
+
}
|
|
40700
|
+
res.once("close", releaseStream);
|
|
40701
|
+
res.once("finish", releaseStream);
|
|
40702
|
+
}
|
|
40065
40703
|
if (stateless) {
|
|
40066
40704
|
let server;
|
|
40067
40705
|
let transport;
|