fdic-mcp-server 1.25.1 → 1.26.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 +114 -13
  2. package/dist/server.js +114 -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.1" : process.env.npm_package_version ?? "0.0.0-dev";
35
+ var VERSION = true ? "1.26.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,62 @@ 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 FdicPeerHealthOutputSchema = 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
+ proxy: import_zod2.z.unknown().nullable(),
32085
+ report_date: import_zod2.z.string(),
32086
+ sort_by: import_zod2.z.string(),
32087
+ total_institutions: import_zod2.z.number().int(),
32088
+ returned_count: import_zod2.z.number().int(),
32089
+ subject_cert: import_zod2.z.number().int().nullable(),
32090
+ subject_rank: import_zod2.z.number().int().nullable(),
32091
+ metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32092
+ institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32093
+ peer_context: import_zod2.z.object({
32094
+ peer_count: import_zod2.z.number().int(),
32095
+ peer_definition: import_zod2.z.string(),
32096
+ broadening_steps: import_zod2.z.array(import_zod2.z.string()),
32097
+ subject_rank: import_zod2.z.number().int().nullable(),
32098
+ subject_percentiles: import_zod2.z.record(PeerStatsSchema),
32099
+ weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
32100
+ }).nullable()
32101
+ }).passthrough();
32046
32102
  var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
32047
32103
 
32048
32104
  // src/tools/institutions.ts
@@ -35819,6 +35875,15 @@ function computePeerStats(subjectValue, peerValues, options) {
35819
35875
  }
35820
35876
 
35821
35877
  // src/tools/peerHealth.ts
35878
+ var PEER_METRICS = [
35879
+ // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35880
+ // New UI consumers should bind to the flat metrics[].name snake_case values instead.
35881
+ { key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
35882
+ { key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
35883
+ { key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
35884
+ { key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
35885
+ { key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
35886
+ ];
35822
35887
  var PeerHealthInputSchema = import_zod11.z.object({
35823
35888
  cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
35824
35889
  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 +35918,11 @@ Three usage modes:
35853
35918
 
35854
35919
  Optionally provide cert to highlight a subject institution's position in the ranking.
35855
35920
 
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.
35921
+ Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array for UI binding while peer_context preserves the legacy nested percentiles and weighted averages. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
35857
35922
 
35858
35923
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
35859
35924
  inputSchema: PeerHealthInputSchema,
35860
- outputSchema: FdicAnalysisOutputSchema,
35925
+ outputSchema: FdicPeerHealthOutputSchema,
35861
35926
  annotations: {
35862
35927
  readOnlyHint: true,
35863
35928
  destructiveHint: false,
@@ -36031,6 +36096,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36031
36096
  const c = asNumber(r.CERT);
36032
36097
  if (c !== null) profileMap.set(c, r);
36033
36098
  }
36099
+ const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
36100
+ const missingProfileCerts = financialCerts.filter((cert) => {
36101
+ const profile = profileMap.get(cert);
36102
+ return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
36103
+ });
36104
+ if (missingProfileCerts.length > 0) {
36105
+ const missingProfileFilters = buildCertFilters(missingProfileCerts);
36106
+ const missingProfileResponses = await mapWithConcurrency(
36107
+ missingProfileFilters,
36108
+ MAX_CONCURRENCY,
36109
+ async (certFilter) => queryEndpoint(
36110
+ ENDPOINTS.INSTITUTIONS,
36111
+ {
36112
+ filters: certFilter,
36113
+ fields: "CERT,NAME,CITY,STALP",
36114
+ limit: 1e4,
36115
+ sort_by: "CERT",
36116
+ sort_order: "ASC"
36117
+ },
36118
+ { signal: controller.signal }
36119
+ )
36120
+ );
36121
+ for (const r of missingProfileResponses.flatMap(extractRecords)) {
36122
+ const c = asNumber(r.CERT);
36123
+ if (c !== null) profileMap.set(c, r);
36124
+ }
36125
+ }
36034
36126
  await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
36035
36127
  const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
36036
36128
  let subjectProxy = null;
@@ -36069,7 +36161,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36069
36161
  ];
36070
36162
  entries.push({
36071
36163
  cert,
36072
- name: String(profile?.NAME ?? `CERT ${cert}`),
36164
+ name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
36165
+ name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
36073
36166
  city: profile?.CITY ? String(profile.CITY) : null,
36074
36167
  state: profile?.STALP ? String(profile.STALP) : null,
36075
36168
  total_assets: asNumber(fin.ASSET),
@@ -36094,18 +36187,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36094
36187
  });
36095
36188
  const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
36096
36189
  let peerContext = null;
36190
+ let metricRows = [];
36097
36191
  if (params.cert) {
36098
36192
  const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
36099
36193
  if (subjectFin) {
36100
36194
  const subjectExtraction = extractCanonicalMetrics(subjectFin);
36101
36195
  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
36196
  const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
36110
36197
  const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
36111
36198
  const subjectPercentiles = {};
@@ -36124,15 +36211,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36124
36211
  }
36125
36212
  }
36126
36213
  if (peerValues.length === 0) continue;
36214
+ let stats = null;
36127
36215
  if (subjectVal !== null) {
36128
- subjectPercentiles[pm.label] = computePeerStats(subjectVal, peerValues, {
36216
+ stats = computePeerStats(subjectVal, peerValues, {
36129
36217
  higherIsBetter: pm.higherIsBetter
36130
36218
  });
36219
+ subjectPercentiles[pm.legacyKey] = stats;
36131
36220
  }
36132
36221
  const weighted = computeWeightedAggregate(weightedEntries);
36133
36222
  if (weighted !== null) {
36134
- weightedPeerAverages[pm.label] = Math.round(weighted * 100) / 100;
36223
+ weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
36135
36224
  }
36225
+ metricRows.push({
36226
+ name: pm.name,
36227
+ label: pm.label,
36228
+ subject: subjectVal,
36229
+ peer_median: stats?.peer_median ?? null,
36230
+ peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
36231
+ percentile: stats?.subject_percentile ?? null,
36232
+ higher_is_better: pm.higherIsBetter,
36233
+ is_outlier: stats?.is_outlier ?? false,
36234
+ outlier_direction: stats?.outlier_direction ?? null
36235
+ });
36136
36236
  }
36137
36237
  let peerDef;
36138
36238
  if (params.certs) {
@@ -36213,6 +36313,7 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36213
36313
  returned_count: returned.length,
36214
36314
  subject_cert: params.cert ?? null,
36215
36315
  subject_rank: subjectRank,
36316
+ metrics: metricRows,
36216
36317
  institutions: returned,
36217
36318
  peer_context: peerContext
36218
36319
  }
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.1" : process.env.npm_package_version ?? "0.0.0-dev";
50
+ var VERSION = true ? "1.26.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,62 @@ 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 FdicPeerHealthOutputSchema = 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
+ proxy: import_zod2.z.unknown().nullable(),
32100
+ report_date: import_zod2.z.string(),
32101
+ sort_by: import_zod2.z.string(),
32102
+ total_institutions: import_zod2.z.number().int(),
32103
+ returned_count: import_zod2.z.number().int(),
32104
+ subject_cert: import_zod2.z.number().int().nullable(),
32105
+ subject_rank: import_zod2.z.number().int().nullable(),
32106
+ metrics: import_zod2.z.array(PeerHealthMetricRowSchema),
32107
+ institutions: import_zod2.z.array(PeerHealthInstitutionSchema),
32108
+ peer_context: import_zod2.z.object({
32109
+ peer_count: import_zod2.z.number().int(),
32110
+ peer_definition: import_zod2.z.string(),
32111
+ broadening_steps: import_zod2.z.array(import_zod2.z.string()),
32112
+ subject_rank: import_zod2.z.number().int().nullable(),
32113
+ subject_percentiles: import_zod2.z.record(PeerStatsSchema),
32114
+ weighted_peer_averages: import_zod2.z.record(import_zod2.z.number())
32115
+ }).nullable()
32116
+ }).passthrough();
32061
32117
  var FdicAnalysisOutputSchema = import_zod2.z.object({}).passthrough();
32062
32118
 
32063
32119
  // src/tools/institutions.ts
@@ -35834,6 +35890,15 @@ function computePeerStats(subjectValue, peerValues, options) {
35834
35890
  }
35835
35891
 
35836
35892
  // src/tools/peerHealth.ts
35893
+ var PEER_METRICS = [
35894
+ // legacyKey preserves the original camelCase peer_context map keys for backward compatibility.
35895
+ // New UI consumers should bind to the flat metrics[].name snake_case values instead.
35896
+ { key: "roaPct", legacyKey: "roaPct", name: "roa_pct", label: "Return on assets", higherIsBetter: true },
35897
+ { key: "equityCapitalRatioPct", legacyKey: "equityCapitalRatioPct", name: "equity_capital_ratio_pct", label: "Equity capital ratio", higherIsBetter: true },
35898
+ { key: "netInterestMarginPct", legacyKey: "netInterestMarginPct", name: "net_interest_margin_pct", label: "Net interest margin", higherIsBetter: true },
35899
+ { key: "efficiencyRatioPct", legacyKey: "efficiencyRatioPct", name: "efficiency_ratio_pct", label: "Efficiency ratio", higherIsBetter: false },
35900
+ { key: "loanToDepositPct", legacyKey: "loanToDepositPct", name: "loan_to_deposit_pct", label: "Loan-to-deposit ratio", higherIsBetter: false }
35901
+ ];
35837
35902
  var PeerHealthInputSchema = import_zod11.z.object({
35838
35903
  cert: import_zod11.z.number().int().positive().optional().describe("Subject institution CERT to highlight in the ranking. Optional."),
35839
35904
  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 +35933,11 @@ Three usage modes:
35868
35933
 
35869
35934
  Optionally provide cert to highlight a subject institution's position in the ranking.
35870
35935
 
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.
35936
+ Output: structuredContent includes {model, official_status, report_date, institutions, metrics, peer_context, proxy}. Institutions include proxy scores and name_source. When a subject cert is provided, metrics is a flat subject-vs-peer array for UI binding while peer_context preserves the legacy nested percentiles and weighted averages. Auto-peer selection derives asset bands from report-date financials and broadens the cohort if fewer than 10 peers match.
35872
35937
 
35873
35938
  NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`,
35874
35939
  inputSchema: PeerHealthInputSchema,
35875
- outputSchema: FdicAnalysisOutputSchema,
35940
+ outputSchema: FdicPeerHealthOutputSchema,
35876
35941
  annotations: {
35877
35942
  readOnlyHint: true,
35878
35943
  destructiveHint: false,
@@ -36046,6 +36111,33 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36046
36111
  const c = asNumber(r.CERT);
36047
36112
  if (c !== null) profileMap.set(c, r);
36048
36113
  }
36114
+ const financialCerts = allFinancials.map((r) => asNumber(r.CERT)).filter((cert) => cert !== null);
36115
+ const missingProfileCerts = financialCerts.filter((cert) => {
36116
+ const profile = profileMap.get(cert);
36117
+ return !profile || typeof profile.NAME !== "string" || profile.NAME.length === 0;
36118
+ });
36119
+ if (missingProfileCerts.length > 0) {
36120
+ const missingProfileFilters = buildCertFilters(missingProfileCerts);
36121
+ const missingProfileResponses = await mapWithConcurrency(
36122
+ missingProfileFilters,
36123
+ MAX_CONCURRENCY,
36124
+ async (certFilter) => queryEndpoint(
36125
+ ENDPOINTS.INSTITUTIONS,
36126
+ {
36127
+ filters: certFilter,
36128
+ fields: "CERT,NAME,CITY,STALP",
36129
+ limit: 1e4,
36130
+ sort_by: "CERT",
36131
+ sort_order: "ASC"
36132
+ },
36133
+ { signal: controller.signal }
36134
+ )
36135
+ );
36136
+ for (const r of missingProfileResponses.flatMap(extractRecords)) {
36137
+ const c = asNumber(r.CERT);
36138
+ if (c !== null) profileMap.set(c, r);
36139
+ }
36140
+ }
36049
36141
  await sendProgressNotification(server.server, progressToken, 0.7, "Computing proxy assessments");
36050
36142
  const subjectHistory = params.cert ? await fetchHistoryEvents(params.cert, { signal: controller.signal, repdte: params.repdte }) : [];
36051
36143
  let subjectProxy = null;
@@ -36084,7 +36176,8 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36084
36176
  ];
36085
36177
  entries.push({
36086
36178
  cert,
36087
- name: String(profile?.NAME ?? `CERT ${cert}`),
36179
+ name: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? profile.NAME : `CERT ${cert}`,
36180
+ name_source: typeof profile?.NAME === "string" && profile.NAME.length > 0 ? "fdic_institution_profile" : "cert_fallback",
36088
36181
  city: profile?.CITY ? String(profile.CITY) : null,
36089
36182
  state: profile?.STALP ? String(profile.STALP) : null,
36090
36183
  total_assets: asNumber(fin.ASSET),
@@ -36109,18 +36202,12 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36109
36202
  });
36110
36203
  const subjectRank = params.cert ? entries.findIndex((e) => e.cert === params.cert) + 1 : null;
36111
36204
  let peerContext = null;
36205
+ let metricRows = [];
36112
36206
  if (params.cert) {
36113
36207
  const subjectFin = allFinancials.find((f) => asNumber(f.CERT) === params.cert);
36114
36208
  if (subjectFin) {
36115
36209
  const subjectExtraction = extractCanonicalMetrics(subjectFin);
36116
36210
  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
36211
  const peerFinancials = allFinancials.filter((f) => asNumber(f.CERT) !== params.cert);
36125
36212
  const peerExtractions = peerFinancials.map((f) => extractCanonicalMetrics(f));
36126
36213
  const subjectPercentiles = {};
@@ -36139,15 +36226,28 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36139
36226
  }
36140
36227
  }
36141
36228
  if (peerValues.length === 0) continue;
36229
+ let stats = null;
36142
36230
  if (subjectVal !== null) {
36143
- subjectPercentiles[pm.label] = computePeerStats(subjectVal, peerValues, {
36231
+ stats = computePeerStats(subjectVal, peerValues, {
36144
36232
  higherIsBetter: pm.higherIsBetter
36145
36233
  });
36234
+ subjectPercentiles[pm.legacyKey] = stats;
36146
36235
  }
36147
36236
  const weighted = computeWeightedAggregate(weightedEntries);
36148
36237
  if (weighted !== null) {
36149
- weightedPeerAverages[pm.label] = Math.round(weighted * 100) / 100;
36238
+ weightedPeerAverages[pm.legacyKey] = Math.round(weighted * 100) / 100;
36150
36239
  }
36240
+ metricRows.push({
36241
+ name: pm.name,
36242
+ label: pm.label,
36243
+ subject: subjectVal,
36244
+ peer_median: stats?.peer_median ?? null,
36245
+ peer_weighted_avg: weighted !== null ? Math.round(weighted * 100) / 100 : null,
36246
+ percentile: stats?.subject_percentile ?? null,
36247
+ higher_is_better: pm.higherIsBetter,
36248
+ is_outlier: stats?.is_outlier ?? false,
36249
+ outlier_direction: stats?.outlier_direction ?? null
36250
+ });
36151
36251
  }
36152
36252
  let peerDef;
36153
36253
  if (params.certs) {
@@ -36228,6 +36328,7 @@ NOTE: Public off-site analytical proxy \u2014 not official supervisory ratings.`
36228
36328
  returned_count: returned.length,
36229
36329
  subject_cert: params.cert ?? null,
36230
36330
  subject_rank: subjectRank,
36331
+ metrics: metricRows,
36231
36332
  institutions: returned,
36232
36333
  peer_context: peerContext
36233
36334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fdic-mcp-server",
3
- "version": "1.25.1",
3
+ "version": "1.26.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",