fdic-mcp-server 1.25.2 → 1.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +195 -13
  2. package/dist/server.js +195 -13
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
32
32
  var import_express2 = __toESM(require("express"));
33
33
 
34
34
  // src/constants.ts
35
- var VERSION = true ? "1.25.2" : process.env.npm_package_version ?? "0.0.0-dev";
35
+ var VERSION = true ? "1.27.0" : process.env.npm_package_version ?? "0.0.0-dev";
36
36
  var FDIC_API_BASE_URL = "https://banks.data.fdic.gov/api";
37
37
  var CHARACTER_LIMIT = 5e4;
38
38
  var DEFAULT_FDIC_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
@@ -32043,6 +32043,99 @@ var FdicBankDeepDiveOutputSchema = import_zod2.z.object({
32043
32043
  warnings: import_zod2.z.array(import_zod2.z.string()),
32044
32044
  sources: import_zod2.z.array(Source)
32045
32045
  });
32046
+ var PeerStatsSchema = import_zod2.z.object({
32047
+ peer_count: import_zod2.z.number().int(),
32048
+ peer_median: import_zod2.z.number(),
32049
+ peer_mean: import_zod2.z.number(),
32050
+ subject_value: import_zod2.z.number(),
32051
+ subject_percentile: import_zod2.z.number(),
32052
+ robust_z_score: import_zod2.z.number(),
32053
+ is_outlier: import_zod2.z.boolean(),
32054
+ outlier_direction: import_zod2.z.enum(["high", "low"]).optional()
32055
+ });
32056
+ var PeerHealthMetricRowSchema = import_zod2.z.object({
32057
+ name: import_zod2.z.string(),
32058
+ label: import_zod2.z.string(),
32059
+ subject: import_zod2.z.number().nullable(),
32060
+ peer_median: import_zod2.z.number().nullable(),
32061
+ peer_weighted_avg: import_zod2.z.number().nullable(),
32062
+ percentile: import_zod2.z.number().nullable(),
32063
+ higher_is_better: import_zod2.z.boolean(),
32064
+ is_outlier: import_zod2.z.boolean(),
32065
+ outlier_direction: import_zod2.z.enum(["high", "low"]).nullable()
32066
+ });
32067
+ var PeerHealthInstitutionSchema = import_zod2.z.object({
32068
+ cert: import_zod2.z.number().int(),
32069
+ name: import_zod2.z.string(),
32070
+ name_source: import_zod2.z.enum(["fdic_institution_profile", "cert_fallback"]),
32071
+ city: import_zod2.z.string().nullable(),
32072
+ state: import_zod2.z.string().nullable(),
32073
+ total_assets: import_zod2.z.number().nullable(),
32074
+ proxy_score: import_zod2.z.number(),
32075
+ proxy_band: import_zod2.z.string(),
32076
+ composite_rating: import_zod2.z.number(),
32077
+ composite_label: import_zod2.z.string(),
32078
+ component_ratings: import_zod2.z.record(import_zod2.z.number()),
32079
+ flags: import_zod2.z.array(import_zod2.z.string())
32080
+ });
32081
+ var PeerHealthProxySummarySchema = import_zod2.z.object({
32082
+ model: import_zod2.z.literal("public_camels_proxy_v1"),
32083
+ official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
32084
+ score: import_zod2.z.number(),
32085
+ band: import_zod2.z.string(),
32086
+ components: import_zod2.z.array(
32087
+ import_zod2.z.object({
32088
+ name: import_zod2.z.string(),
32089
+ label: import_zod2.z.string(),
32090
+ score: import_zod2.z.number(),
32091
+ legacy_rating: import_zod2.z.number(),
32092
+ legacy_label: import_zod2.z.string(),
32093
+ flags: import_zod2.z.array(import_zod2.z.string())
32094
+ })
32095
+ ),
32096
+ capital_classification: import_zod2.z.object({
32097
+ category: import_zod2.z.string(),
32098
+ label: import_zod2.z.string(),
32099
+ binding_constraint: import_zod2.z.string().nullable(),
32100
+ ratios_used: import_zod2.z.record(import_zod2.z.number().nullable())
32101
+ }),
32102
+ management_overlay: import_zod2.z.object({
32103
+ level: import_zod2.z.string(),
32104
+ caps_band: import_zod2.z.boolean(),
32105
+ reason_codes: import_zod2.z.array(import_zod2.z.string())
32106
+ }),
32107
+ risk_signal_count: import_zod2.z.number().int(),
32108
+ risk_signal_severities: import_zod2.z.record(import_zod2.z.number().int()),
32109
+ trend_count: import_zod2.z.number().int(),
32110
+ data_quality: import_zod2.z.object({
32111
+ report_date: import_zod2.z.string(),
32112
+ staleness: import_zod2.z.string(),
32113
+ gaps_count: import_zod2.z.number().int(),
32114
+ gaps: import_zod2.z.array(import_zod2.z.string())
32115
+ })
32116
+ });
32117
+ var FdicPeerHealthOutputSchema = import_zod2.z.object({
32118
+ model: import_zod2.z.literal("public_camels_proxy_v1"),
32119
+ official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
32120
+ proxy: import_zod2.z.unknown().nullable(),
32121
+ proxy_summary: PeerHealthProxySummarySchema.nullable(),
32122
+ report_date: import_zod2.z.string(),
32123
+ sort_by: import_zod2.z.string(),
32124
+ total_institutions: import_zod2.z.number().int(),
32125
+ returned_count: import_zod2.z.number().int(),
32126
+ subject_cert: import_zod2.z.number().int().nullable(),
32127
+ subject_rank: import_zod2.z.number().int().nullable(),
32128
+ metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32129
+ institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32130
+ peer_context: import_zod2.z.object({
32131
+ peer_count: import_zod2.z.number().int(),
32132
+ peer_definition: import_zod2.z.string(),
32133
+ broadening_steps: import_zod2.z.array(import_zod2.z.string()),
32134
+ subject_rank: import_zod2.z.number().int().nullable(),
32135
+ subject_percentiles: import_zod2.z.record(PeerStatsSchema),
32136
+ weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
32137
+ }).nullable()
32138
+ }).passthrough();
32046
32139
  var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
32047
32140
 
32048
32141
  // src/tools/institutions.ts
@@ -35819,6 +35912,58 @@ function computePeerStats(subjectValue, peerValues, options) {
35819
35912
  }
35820
35913
 
35821
35914
  // src/tools/peerHealth.ts
35915
+ var PEER_METRICS = [
35916
+ // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35917
+ // New UI consumers should bind to the flat metrics[].name snake_case values instead.
35918
+ { key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
35919
+ { key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
35920
+ { key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
35921
+ { key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
35922
+ { key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
35923
+ ];
35924
+ function buildProxySummary(proxy) {
35925
+ if (!proxy) return null;
35926
+ const componentEntries = [
35927
+ { name: "capital", assessment: proxy.component_assessment.capital },
35928
+ { name: "asset_quality", assessment: proxy.component_assessment.asset_quality },
35929
+ { name: "earnings", assessment: proxy.component_assessment.earnings },
35930
+ { name: "liquidity_funding", assessment: proxy.component_assessment.liquidity_funding },
35931
+ { name: "sensitivity_proxy", assessment: proxy.component_assessment.sensitivity_proxy }
35932
+ ];
35933
+ const riskSignalSeverities = {};
35934
+ for (const signal of proxy.risk_signals) {
35935
+ riskSignalSeverities[signal.severity] = (riskSignalSeverities[signal.severity] ?? 0) + 1;
35936
+ }
35937
+ return {
35938
+ model: proxy.model,
35939
+ official_status: proxy.official_status,
35940
+ score: proxy.overall.score,
35941
+ band: proxy.overall.band,
35942
+ components: componentEntries.map(({ name, assessment }) => ({
35943
+ name,
35944
+ label: assessment.label,
35945
+ score: assessment.score,
35946
+ legacy_rating: assessment.legacy_rating,
35947
+ legacy_label: assessment.legacy_label,
35948
+ flags: assessment.flags
35949
+ })),
35950
+ capital_classification: {
35951
+ category: proxy.capital_classification.category,
35952
+ label: proxy.capital_classification.label,
35953
+ binding_constraint: proxy.capital_classification.binding_constraint ?? null,
35954
+ ratios_used: proxy.capital_classification.ratios_used
35955
+ },
35956
+ management_overlay: {
35957
+ level: proxy.management_overlay.level,
35958
+ caps_band: proxy.management_overlay.caps_band,
35959
+ reason_codes: proxy.management_overlay.reason_codes
35960
+ },
35961
+ risk_signal_count: proxy.risk_signals.length,
35962
+ risk_signal_severities: riskSignalSeverities,
35963
+ trend_count: proxy.trend_insights.length,
35964
+ data_quality: proxy.data_quality
35965
+ };
35966
+ }
35822
35967
  var PeerHealthInputSchema = import_zod11.z.object({
35823
35968
  cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
35824
35969
  certs: import_zod11.z.array(import_zod11.z.number().int().positive()).max(50).optional().describe("Explicit list of CERTs to compare (max 50)."),
@@ -35853,11 +35998,11 @@ Three usage modes:
35853
35998
 
35854
35999
  Optionally provide cert to highlight a subject institution's position in the ranking.
35855
36000
 
35856
- Output: Ranked list with per-institution proxy_score (1-4 scale) and proxy_band, sorted by composite or any individual component. When a subject cert is provided, includes peer percentile context, asset-weighted peer averages, and the subject's full proxy assessment. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
36001
+ Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy_summary, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array and proxy_summary is a flattened subject proxy for UI binding while peer_context and proxy preserve the legacy detailed payloads. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
35857
36002
 
35858
36003
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
35859
36004
  inputSchema: PeerHealthInputSchema,
35860
- outputSchema: FdicAnalysisOutputSchema,
36005
+ outputSchema: FdicPeerHealthOutputSchema,
35861
36006
  annotations: {
35862
36007
  readOnlyHint: true,
35863
36008
  destructiveHint: false,
@@ -36031,6 +36176,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36031
36176
  const c = asNumber(r.CERT);
36032
36177
  if (c !== null) profileMap.set(c, r);
36033
36178
  }
36179
+ const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
36180
+ const missingProfileCerts = financialCerts.filter((cert) => {
36181
+ const profile = profileMap.get(cert);
36182
+ return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
36183
+ });
36184
+ if (missingProfileCerts.length > 0) {
36185
+ const missingProfileFilters = buildCertFilters(missingProfileCerts);
36186
+ const missingProfileResponses = await mapWithConcurrency(
36187
+ missingProfileFilters,
36188
+ MAX_CONCURRENCY,
36189
+ async (certFilter) => queryEndpoint(
36190
+ ENDPOINTS.INSTITUTIONS,
36191
+ {
36192
+ filters: certFilter,
36193
+ fields: "CERT,NAME,CITY,STALP",
36194
+ limit: 1e4,
36195
+ sort_by: "CERT",
36196
+ sort_order: "ASC"
36197
+ },
36198
+ { signal: controller.signal }
36199
+ )
36200
+ );
36201
+ for (const r of missingProfileResponses.flatMap(extractRecords)) {
36202
+ const c = asNumber(r.CERT);
36203
+ if (c !== null) profileMap.set(c, r);
36204
+ }
36205
+ }
36034
36206
  await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
36035
36207
  const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
36036
36208
  let subjectProxy = null;
@@ -36069,7 +36241,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36069
36241
  ];
36070
36242
  entries.push({
36071
36243
  cert,
36072
- name: String(profile?.NAME ?? `CERT ${cert}`),
36244
+ name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
36245
+ name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
36073
36246
  city: profile?.CITY ? String(profile.CITY) : null,
36074
36247
  state: profile?.STALP ? String(profile.STALP) : null,
36075
36248
  total_assets: asNumber(fin.ASSET),
@@ -36094,18 +36267,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36094
36267
  });
36095
36268
  const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
36096
36269
  let peerContext = null;
36270
+ let metricRows = [];
36097
36271
  if (params.cert) {
36098
36272
  const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
36099
36273
  if (subjectFin) {
36100
36274
  const subjectExtraction = extractCanonicalMetrics(subjectFin);
36101
36275
  const sm = subjectExtraction.metrics;
36102
- const PEER_METRICS = [
36103
- { key: "roaPct", label: "roaPct", higherIsBetter: true },
36104
- { key: "equityCapitalRatioPct", label: "equityCapitalRatioPct", higherIsBetter: true },
36105
- { key: "netInterestMarginPct", label: "netInterestMarginPct", higherIsBetter: true },
36106
- { key: "efficiencyRatioPct", label: "efficiencyRatioPct", higherIsBetter: false },
36107
- { key: "loanToDepositPct", label: "loanToDepositPct", higherIsBetter: false }
36108
- ];
36109
36276
  const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
36110
36277
  const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
36111
36278
  const subjectPercentiles = {};
@@ -36124,15 +36291,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36124
36291
  }
36125
36292
  }
36126
36293
  if (peerValues.length === 0) continue;
36294
+ let stats = null;
36127
36295
  if (subjectVal !== null) {
36128
- subjectPercentiles[pm.label] = computePeerStats(subjectVal, peerValues, {
36296
+ stats = computePeerStats(subjectVal, peerValues, {
36129
36297
  higherIsBetter: pm.higherIsBetter
36130
36298
  });
36299
+ subjectPercentiles[pm.legacyKey] = stats;
36131
36300
  }
36132
36301
  const weighted = computeWeightedAggregate(weightedEntries);
36133
36302
  if (weighted !== null) {
36134
- weightedPeerAverages[pm.label] = Math.round(weighted * 100) / 100;
36303
+ weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
36135
36304
  }
36305
+ metricRows.push({
36306
+ name: pm.name,
36307
+ label: pm.label,
36308
+ subject: subjectVal,
36309
+ peer_median: stats?.peer_median ?? null,
36310
+ peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
36311
+ percentile: stats?.subject_percentile ?? null,
36312
+ higher_is_better: pm.higherIsBetter,
36313
+ is_outlier: stats?.is_outlier ?? false,
36314
+ outlier_direction: stats?.outlier_direction ?? null
36315
+ });
36136
36316
  }
36137
36317
  let peerDef;
36138
36318
  if (params.certs) {
@@ -36207,12 +36387,14 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36207
36387
  model: "public_camels_proxy_v1",
36208
36388
  official_status: "public off-site proxy, not official CAMELS",
36209
36389
  proxy: subjectProxy,
36390
+ proxy_summary: buildProxySummary(subjectProxy),
36210
36391
  report_date: params.repdte,
36211
36392
  sort_by: params.sort_by,
36212
36393
  total_institutions: entries.length,
36213
36394
  returned_count: returned.length,
36214
36395
  subject_cert: params.cert ?? null,
36215
36396
  subject_rank: subjectRank,
36397
+ metrics: metricRows,
36216
36398
  institutions: returned,
36217
36399
  peer_context: peerContext
36218
36400
  }
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.25.2" : process.env.npm_package_version ?? "0.0.0-dev";
50
+ var VERSION = true ? "1.27.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;
@@ -32058,6 +32058,99 @@ var FdicBankDeepDiveOutputSchema = import_zod2.z.object({
32058
32058
  warnings: import_zod2.z.array(import_zod2.z.string()),
32059
32059
  sources: import_zod2.z.array(Source)
32060
32060
  });
32061
+ var PeerStatsSchema = import_zod2.z.object({
32062
+ peer_count: import_zod2.z.number().int(),
32063
+ peer_median: import_zod2.z.number(),
32064
+ peer_mean: import_zod2.z.number(),
32065
+ subject_value: import_zod2.z.number(),
32066
+ subject_percentile: import_zod2.z.number(),
32067
+ robust_z_score: import_zod2.z.number(),
32068
+ is_outlier: import_zod2.z.boolean(),
32069
+ outlier_direction: import_zod2.z.enum(["high", "low"]).optional()
32070
+ });
32071
+ var PeerHealthMetricRowSchema = import_zod2.z.object({
32072
+ name: import_zod2.z.string(),
32073
+ label: import_zod2.z.string(),
32074
+ subject: import_zod2.z.number().nullable(),
32075
+ peer_median: import_zod2.z.number().nullable(),
32076
+ peer_weighted_avg: import_zod2.z.number().nullable(),
32077
+ percentile: import_zod2.z.number().nullable(),
32078
+ higher_is_better: import_zod2.z.boolean(),
32079
+ is_outlier: import_zod2.z.boolean(),
32080
+ outlier_direction: import_zod2.z.enum(["high", "low"]).nullable()
32081
+ });
32082
+ var PeerHealthInstitutionSchema = import_zod2.z.object({
32083
+ cert: import_zod2.z.number().int(),
32084
+ name: import_zod2.z.string(),
32085
+ name_source: import_zod2.z.enum(["fdic_institution_profile", "cert_fallback"]),
32086
+ city: import_zod2.z.string().nullable(),
32087
+ state: import_zod2.z.string().nullable(),
32088
+ total_assets: import_zod2.z.number().nullable(),
32089
+ proxy_score: import_zod2.z.number(),
32090
+ proxy_band: import_zod2.z.string(),
32091
+ composite_rating: import_zod2.z.number(),
32092
+ composite_label: import_zod2.z.string(),
32093
+ component_ratings: import_zod2.z.record(import_zod2.z.number()),
32094
+ flags: import_zod2.z.array(import_zod2.z.string())
32095
+ });
32096
+ var PeerHealthProxySummarySchema = import_zod2.z.object({
32097
+ model: import_zod2.z.literal("public_camels_proxy_v1"),
32098
+ official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
32099
+ score: import_zod2.z.number(),
32100
+ band: import_zod2.z.string(),
32101
+ components: import_zod2.z.array(
32102
+ import_zod2.z.object({
32103
+ name: import_zod2.z.string(),
32104
+ label: import_zod2.z.string(),
32105
+ score: import_zod2.z.number(),
32106
+ legacy_rating: import_zod2.z.number(),
32107
+ legacy_label: import_zod2.z.string(),
32108
+ flags: import_zod2.z.array(import_zod2.z.string())
32109
+ })
32110
+ ),
32111
+ capital_classification: import_zod2.z.object({
32112
+ category: import_zod2.z.string(),
32113
+ label: import_zod2.z.string(),
32114
+ binding_constraint: import_zod2.z.string().nullable(),
32115
+ ratios_used: import_zod2.z.record(import_zod2.z.number().nullable())
32116
+ }),
32117
+ management_overlay: import_zod2.z.object({
32118
+ level: import_zod2.z.string(),
32119
+ caps_band: import_zod2.z.boolean(),
32120
+ reason_codes: import_zod2.z.array(import_zod2.z.string())
32121
+ }),
32122
+ risk_signal_count: import_zod2.z.number().int(),
32123
+ risk_signal_severities: import_zod2.z.record(import_zod2.z.number().int()),
32124
+ trend_count: import_zod2.z.number().int(),
32125
+ data_quality: import_zod2.z.object({
32126
+ report_date: import_zod2.z.string(),
32127
+ staleness: import_zod2.z.string(),
32128
+ gaps_count: import_zod2.z.number().int(),
32129
+ gaps: import_zod2.z.array(import_zod2.z.string())
32130
+ })
32131
+ });
32132
+ var FdicPeerHealthOutputSchema = import_zod2.z.object({
32133
+ model: import_zod2.z.literal("public_camels_proxy_v1"),
32134
+ official_status: import_zod2.z.literal("public off-site proxy, not official CAMELS"),
32135
+ proxy: import_zod2.z.unknown().nullable(),
32136
+ proxy_summary: PeerHealthProxySummarySchema.nullable(),
32137
+ report_date: import_zod2.z.string(),
32138
+ sort_by: import_zod2.z.string(),
32139
+ total_institutions: import_zod2.z.number().int(),
32140
+ returned_count: import_zod2.z.number().int(),
32141
+ subject_cert: import_zod2.z.number().int().nullable(),
32142
+ subject_rank: import_zod2.z.number().int().nullable(),
32143
+ metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32144
+ institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32145
+ peer_context: import_zod2.z.object({
32146
+ peer_count: import_zod2.z.number().int(),
32147
+ peer_definition: import_zod2.z.string(),
32148
+ broadening_steps: import_zod2.z.array(import_zod2.z.string()),
32149
+ subject_rank: import_zod2.z.number().int().nullable(),
32150
+ subject_percentiles: import_zod2.z.record(PeerStatsSchema),
32151
+ weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
32152
+ }).nullable()
32153
+ }).passthrough();
32061
32154
  var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
32062
32155
 
32063
32156
  // src/tools/institutions.ts
@@ -35834,6 +35927,58 @@ function computePeerStats(subjectValue, peerValues, options) {
35834
35927
  }
35835
35928
 
35836
35929
  // src/tools/peerHealth.ts
35930
+ var PEER_METRICS = [
35931
+ // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35932
+ // New UI consumers should bind to the flat metrics[].name snake_case values instead.
35933
+ { key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
35934
+ { key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
35935
+ { key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
35936
+ { key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
35937
+ { key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
35938
+ ];
35939
+ function buildProxySummary(proxy) {
35940
+ if (!proxy) return null;
35941
+ const componentEntries = [
35942
+ { name: "capital", assessment: proxy.component_assessment.capital },
35943
+ { name: "asset_quality", assessment: proxy.component_assessment.asset_quality },
35944
+ { name: "earnings", assessment: proxy.component_assessment.earnings },
35945
+ { name: "liquidity_funding", assessment: proxy.component_assessment.liquidity_funding },
35946
+ { name: "sensitivity_proxy", assessment: proxy.component_assessment.sensitivity_proxy }
35947
+ ];
35948
+ const riskSignalSeverities = {};
35949
+ for (const signal of proxy.risk_signals) {
35950
+ riskSignalSeverities[signal.severity] = (riskSignalSeverities[signal.severity] ?? 0) + 1;
35951
+ }
35952
+ return {
35953
+ model: proxy.model,
35954
+ official_status: proxy.official_status,
35955
+ score: proxy.overall.score,
35956
+ band: proxy.overall.band,
35957
+ components: componentEntries.map(({ name, assessment }) => ({
35958
+ name,
35959
+ label: assessment.label,
35960
+ score: assessment.score,
35961
+ legacy_rating: assessment.legacy_rating,
35962
+ legacy_label: assessment.legacy_label,
35963
+ flags: assessment.flags
35964
+ })),
35965
+ capital_classification: {
35966
+ category: proxy.capital_classification.category,
35967
+ label: proxy.capital_classification.label,
35968
+ binding_constraint: proxy.capital_classification.binding_constraint ?? null,
35969
+ ratios_used: proxy.capital_classification.ratios_used
35970
+ },
35971
+ management_overlay: {
35972
+ level: proxy.management_overlay.level,
35973
+ caps_band: proxy.management_overlay.caps_band,
35974
+ reason_codes: proxy.management_overlay.reason_codes
35975
+ },
35976
+ risk_signal_count: proxy.risk_signals.length,
35977
+ risk_signal_severities: riskSignalSeverities,
35978
+ trend_count: proxy.trend_insights.length,
35979
+ data_quality: proxy.data_quality
35980
+ };
35981
+ }
35837
35982
  var PeerHealthInputSchema = import_zod11.z.object({
35838
35983
  cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
35839
35984
  certs: import_zod11.z.array(import_zod11.z.number().int().positive()).max(50).optional().describe("Explicit list of CERTs to compare (max 50)."),
@@ -35868,11 +36013,11 @@ Three usage modes:
35868
36013
 
35869
36014
  Optionally provide cert to highlight a subject institution's position in the ranking.
35870
36015
 
35871
- Output: Ranked list with per-institution proxy_score (1-4 scale) and proxy_band, sorted by composite or any individual component. When a subject cert is provided, includes peer percentile context, asset-weighted peer averages, and the subject's full proxy assessment. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
36016
+ Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy_summary, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array and proxy_summary is a flattened subject proxy for UI binding while peer_context and proxy preserve the legacy detailed payloads. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
35872
36017
 
35873
36018
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
35874
36019
  inputSchema: PeerHealthInputSchema,
35875
- outputSchema: FdicAnalysisOutputSchema,
36020
+ outputSchema: FdicPeerHealthOutputSchema,
35876
36021
  annotations: {
35877
36022
  readOnlyHint: true,
35878
36023
  destructiveHint: false,
@@ -36046,6 +36191,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36046
36191
  const c = asNumber(r.CERT);
36047
36192
  if (c !== null) profileMap.set(c, r);
36048
36193
  }
36194
+ const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
36195
+ const missingProfileCerts = financialCerts.filter((cert) => {
36196
+ const profile = profileMap.get(cert);
36197
+ return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
36198
+ });
36199
+ if (missingProfileCerts.length > 0) {
36200
+ const missingProfileFilters = buildCertFilters(missingProfileCerts);
36201
+ const missingProfileResponses = await mapWithConcurrency(
36202
+ missingProfileFilters,
36203
+ MAX_CONCURRENCY,
36204
+ async (certFilter) => queryEndpoint(
36205
+ ENDPOINTS.INSTITUTIONS,
36206
+ {
36207
+ filters: certFilter,
36208
+ fields: "CERT,NAME,CITY,STALP",
36209
+ limit: 1e4,
36210
+ sort_by: "CERT",
36211
+ sort_order: "ASC"
36212
+ },
36213
+ { signal: controller.signal }
36214
+ )
36215
+ );
36216
+ for (const r of missingProfileResponses.flatMap(extractRecords)) {
36217
+ const c = asNumber(r.CERT);
36218
+ if (c !== null) profileMap.set(c, r);
36219
+ }
36220
+ }
36049
36221
  await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
36050
36222
  const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
36051
36223
  let subjectProxy = null;
@@ -36084,7 +36256,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36084
36256
  ];
36085
36257
  entries.push({
36086
36258
  cert,
36087
- name: String(profile?.NAME ?? `CERT ${cert}`),
36259
+ name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
36260
+ name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
36088
36261
  city: profile?.CITY ? String(profile.CITY) : null,
36089
36262
  state: profile?.STALP ? String(profile.STALP) : null,
36090
36263
  total_assets: asNumber(fin.ASSET),
@@ -36109,18 +36282,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36109
36282
  });
36110
36283
  const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
36111
36284
  let peerContext = null;
36285
+ let metricRows = [];
36112
36286
  if (params.cert) {
36113
36287
  const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
36114
36288
  if (subjectFin) {
36115
36289
  const subjectExtraction = extractCanonicalMetrics(subjectFin);
36116
36290
  const sm = subjectExtraction.metrics;
36117
- const PEER_METRICS = [
36118
- { key: "roaPct", label: "roaPct", higherIsBetter: true },
36119
- { key: "equityCapitalRatioPct", label: "equityCapitalRatioPct", higherIsBetter: true },
36120
- { key: "netInterestMarginPct", label: "netInterestMarginPct", higherIsBetter: true },
36121
- { key: "efficiencyRatioPct", label: "efficiencyRatioPct", higherIsBetter: false },
36122
- { key: "loanToDepositPct", label: "loanToDepositPct", higherIsBetter: false }
36123
- ];
36124
36291
  const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
36125
36292
  const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
36126
36293
  const subjectPercentiles = {};
@@ -36139,15 +36306,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36139
36306
  }
36140
36307
  }
36141
36308
  if (peerValues.length === 0) continue;
36309
+ let stats = null;
36142
36310
  if (subjectVal !== null) {
36143
- subjectPercentiles[pm.label] = computePeerStats(subjectVal, peerValues, {
36311
+ stats = computePeerStats(subjectVal, peerValues, {
36144
36312
  higherIsBetter: pm.higherIsBetter
36145
36313
  });
36314
+ subjectPercentiles[pm.legacyKey] = stats;
36146
36315
  }
36147
36316
  const weighted = computeWeightedAggregate(weightedEntries);
36148
36317
  if (weighted !== null) {
36149
- weightedPeerAverages[pm.label] = Math.round(weighted * 100) / 100;
36318
+ weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
36150
36319
  }
36320
+ metricRows.push({
36321
+ name: pm.name,
36322
+ label: pm.label,
36323
+ subject: subjectVal,
36324
+ peer_median: stats?.peer_median ?? null,
36325
+ peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
36326
+ percentile: stats?.subject_percentile ?? null,
36327
+ higher_is_better: pm.higherIsBetter,
36328
+ is_outlier: stats?.is_outlier ?? false,
36329
+ outlier_direction: stats?.outlier_direction ?? null
36330
+ });
36151
36331
  }
36152
36332
  let peerDef;
36153
36333
  if (params.certs) {
@@ -36222,12 +36402,14 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36222
36402
  model: "public_camels_proxy_v1",
36223
36403
  official_status: "public off-site proxy, not official CAMELS",
36224
36404
  proxy: subjectProxy,
36405
+ proxy_summary: buildProxySummary(subjectProxy),
36225
36406
  report_date: params.repdte,
36226
36407
  sort_by: params.sort_by,
36227
36408
  total_institutions: entries.length,
36228
36409
  returned_count: returned.length,
36229
36410
  subject_cert: params.cert ?? null,
36230
36411
  subject_rank: subjectRank,
36412
+ metrics: metricRows,
36231
36413
  institutions: returned,
36232
36414
  peer_context: peerContext
36233
36415
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdic-mcp-server",
3
- "version": "1.25.2",
3
+ "version": "1.27.0",
4
4
  "description": "MCP server for the FDIC BankFind Suite API",
5
5
  "mcpName": "io.github.jflamb/fdic-mcp-server",
6
6
  "main": "dist/server.js",