fdic-mcp-server 1.23.2 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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.23.2" : process.env.npm_package_version ?? "0.0.0-dev";
35
+ var VERSION = true ? "1.24.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;
@@ -31883,50 +31883,7 @@ function registerInstitutionTools(server) {
31883
31883
  "fdic_search_institutions",
31884
31884
  {
31885
31885
  title: "Search FDIC Institutions",
31886
- description: `Search for FDIC-insured financial institutions (banks and savings institutions) using flexible filters.
31887
-
31888
- Returns institution profile data including name, location, charter class, asset size, deposit totals, profitability metrics, and regulatory status.
31889
-
31890
- Common filter examples:
31891
- - By state: STNAME:"California"
31892
- - Active banks only: ACTIVE:1
31893
- - Large banks: ASSET:[10000000 TO *] (assets in $thousands)
31894
- - By bank class: BKCLASS:N (national bank), BKCLASS:SM (state member bank), BKCLASS:NM (state non-member)
31895
- - By name: NAME:"Wells Fargo"
31896
- - Commercial banks: CB:1
31897
- - Savings institutions: MUTUAL:1
31898
- - Recently established: ESTYMD:[2010-01-01 TO *]
31899
-
31900
- Charter class codes (BKCLASS):
31901
- N = National commercial bank (OCC-supervised)
31902
- SM = State-chartered, Federal Reserve member
31903
- NM = State-chartered, non-member (FDIC-supervised)
31904
- SB = Federal savings bank (OCC-supervised)
31905
- SA = State savings association
31906
- OI = Insured branch of foreign bank
31907
-
31908
- Key returned fields:
31909
- - CERT: FDIC Certificate Number (unique ID)
31910
- - NAME: Institution name
31911
- - CITY, STALP (two-letter state code), STNAME (full state name): Location
31912
- - ASSET: Total assets ($thousands)
31913
- - DEP: Total deposits ($thousands)
31914
- - BKCLASS: Charter class code (see above)
31915
- - ACTIVE: 1 if currently active, 0 if inactive
31916
- - ROA, ROE: Profitability ratios
31917
- - OFFICES: Number of branch offices
31918
- - ESTYMD: Establishment date (YYYY-MM-DD)
31919
- - REGAGNT: Primary federal regulator (OCC, FRS, FDIC)
31920
-
31921
- Args:
31922
- - filters (string, optional): ElasticSearch query filter
31923
- - fields (string, optional): Comma-separated field names
31924
- - limit (number): Records to return, 1-10000 (default: 20)
31925
- - offset (number): Pagination offset (default: 0)
31926
- - sort_by (string, optional): Field to sort by
31927
- - sort_order ('ASC'|'DESC'): Sort direction (default: 'ASC')
31928
-
31929
- Prefer concise human-readable summaries or tables when answering users. Structured fields are available for totals, pagination, and institution records.`,
31886
+ description: "Use this when the user needs FDIC-insured institution search results by name, state, CERT, asset size, charter class, or regulatory status. Returns institution profile rows with pagination; use fdic://schemas/institutions for the full field catalog.",
31930
31887
  inputSchema: CommonQuerySchema,
31931
31888
  annotations: {
31932
31889
  readOnlyHint: true,
@@ -31970,15 +31927,7 @@ Prefer concise human-readable summaries or tables when answering users. Structur
31970
31927
  "fdic_get_institution",
31971
31928
  {
31972
31929
  title: "Get Institution by Certificate Number",
31973
- description: `Retrieve detailed information for a specific FDIC-insured institution using its FDIC Certificate Number (CERT).
31974
-
31975
- Use this when you know the exact CERT number for an institution. To find a CERT number, use fdic_search_institutions first.
31976
-
31977
- Args:
31978
- - cert (number): FDIC Certificate Number (e.g., 3511 for Bank of America)
31979
- - fields (string, optional): Comma-separated list of fields to return
31980
-
31981
- Returns a detailed institution profile suitable for concise summaries, with structured fields available for exact values when needed.`,
31930
+ description: "Use this when the user knows an exact FDIC Certificate Number and needs one institution profile. To discover a CERT first, call fdic_search_institutions or the ChatGPT-compatible search tool.",
31982
31931
  inputSchema: CertSchema,
31983
31932
  annotations: {
31984
31933
  readOnlyHint: true,
@@ -38625,6 +38574,884 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38625
38574
  );
38626
38575
  }
38627
38576
 
38577
+ // src/tools/chatgptRetrieval.ts
38578
+ var import_zod20 = require("zod");
38579
+
38580
+ // src/tools/shared/chatgptUrls.ts
38581
+ var FDIC_BANKFIND_BASE_URL = "https://banks.data.fdic.gov/bankfind-suite";
38582
+ var FDIC_FAILED_BANK_LIST_URL = "https://www.fdic.gov/bank-failures/failed-bank-list";
38583
+ var PROJECT_TOOL_REFERENCE_URL = "https://jflamb.github.io/fdic-mcp-server/tool-reference/";
38584
+ function getInstitutionUrl(cert) {
38585
+ return `${FDIC_BANKFIND_BASE_URL}/bankfind/details/${cert}`;
38586
+ }
38587
+ function getFailedBankListUrl() {
38588
+ return FDIC_FAILED_BANK_LIST_URL;
38589
+ }
38590
+ function getSchemaDocsUrl(endpoint) {
38591
+ return `${PROJECT_TOOL_REFERENCE_URL}#${encodeURIComponent(endpoint)}`;
38592
+ }
38593
+ function getBranchCitationUrl() {
38594
+ return `${PROJECT_TOOL_REFERENCE_URL}#fdic_search_locations`;
38595
+ }
38596
+
38597
+ // src/tools/chatgptRetrieval.ts
38598
+ var SearchInputSchema = import_zod20.z.object({
38599
+ query: import_zod20.z.string().min(1).describe("Natural-language search query.")
38600
+ });
38601
+ var FetchInputSchema = import_zod20.z.object({
38602
+ id: import_zod20.z.string().min(1).describe(
38603
+ "Retrieval item id, such as institution:<CERT>, failure:<CERT>, branch:<UNINUM>, or schema:<endpoint>."
38604
+ )
38605
+ });
38606
+ var MAX_SEARCH_RESULTS = 8;
38607
+ function asString(value) {
38608
+ if (value === void 0 || value === null) {
38609
+ return "";
38610
+ }
38611
+ return String(value);
38612
+ }
38613
+ function asNumber2(value) {
38614
+ if (typeof value === "number" && Number.isFinite(value)) {
38615
+ return value;
38616
+ }
38617
+ if (typeof value === "string" && value.trim() !== "") {
38618
+ const parsed = Number(value);
38619
+ return Number.isFinite(parsed) ? parsed : void 0;
38620
+ }
38621
+ return void 0;
38622
+ }
38623
+ function jsonText(payload) {
38624
+ return JSON.stringify(payload);
38625
+ }
38626
+ function escapeFilterValue(value) {
38627
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').trim();
38628
+ }
38629
+ function normalizeQuery(query) {
38630
+ return query.replace(/\s+/g, " ").trim();
38631
+ }
38632
+ function extractCertLikeNumber(query) {
38633
+ const match = query.match(/\b(?:cert(?:ificate)?\s*#?\s*)?(\d{1,7})\b/i);
38634
+ if (!match) {
38635
+ return void 0;
38636
+ }
38637
+ return Number.parseInt(match[1], 10);
38638
+ }
38639
+ function shouldSearchFailures(query) {
38640
+ return /\b(fail|failed|failure|closed|resolution|receivership)\b/i.test(
38641
+ query
38642
+ );
38643
+ }
38644
+ function shouldSearchBranches(query) {
38645
+ return /\b(branch|branches|office|offices|location|locations|address|city|county|zip|market|msa)\b/i.test(
38646
+ query
38647
+ );
38648
+ }
38649
+ function shouldSearchSchemas(query) {
38650
+ return /\b(schema|field|fields|column|columns|endpoint|call report|financials?|sod|summary of deposits|demographics|metadata)\b/i.test(
38651
+ query
38652
+ );
38653
+ }
38654
+ function getRecordTitle(record, fallback) {
38655
+ return asString(record.NAME) || asString(record.UNINAME) || asString(record.NAMEFULL) || asString(record.OFFNAME) || fallback;
38656
+ }
38657
+ function formatLocation(record) {
38658
+ return [record.CITY, record.STALP].map(asString).filter(Boolean).join(", ");
38659
+ }
38660
+ function buildBranchId(record) {
38661
+ const uninum = asString(record.UNINUM);
38662
+ if (uninum) {
38663
+ return `branch:${encodeURIComponent(uninum)}`;
38664
+ }
38665
+ const cert = asString(record.CERT);
38666
+ const brnum = asString(record.BRNUM);
38667
+ const zip = asString(record.ZIP);
38668
+ if (!cert || !brnum) {
38669
+ return void 0;
38670
+ }
38671
+ return `branch:${encodeURIComponent([cert, brnum, zip].join("~"))}`;
38672
+ }
38673
+ function institutionSearchResult(record) {
38674
+ const cert = asNumber2(record.CERT);
38675
+ if (cert === void 0) {
38676
+ return void 0;
38677
+ }
38678
+ const title = getRecordTitle(record, `FDIC institution ${cert}`);
38679
+ const location = formatLocation(record);
38680
+ return {
38681
+ id: `institution:${cert}`,
38682
+ title: location ? `${title} (${location})` : title,
38683
+ url: getInstitutionUrl(cert)
38684
+ };
38685
+ }
38686
+ function failureSearchResult(record) {
38687
+ const cert = asNumber2(record.CERT);
38688
+ if (cert === void 0) {
38689
+ return void 0;
38690
+ }
38691
+ const title = getRecordTitle(record, `Failed bank ${cert}`);
38692
+ const failDate = asString(record.FAILDATE);
38693
+ return {
38694
+ id: `failure:${cert}`,
38695
+ title: failDate ? `${title} failed ${failDate}` : `${title} failure record`,
38696
+ url: getFailedBankListUrl()
38697
+ };
38698
+ }
38699
+ function branchSearchResult(record) {
38700
+ const id = buildBranchId(record);
38701
+ if (!id) {
38702
+ return void 0;
38703
+ }
38704
+ const name = getRecordTitle(record, "FDIC branch location");
38705
+ const address = [record.ADDRESS, record.CITY, record.STALP, record.ZIP].map(asString).filter(Boolean).join(", ");
38706
+ return {
38707
+ id,
38708
+ title: address ? `${name} - ${address}` : name,
38709
+ url: getBranchCitationUrl()
38710
+ };
38711
+ }
38712
+ function schemaSearchResults(query) {
38713
+ const normalized = normalizeQuery(query).toLowerCase();
38714
+ const metadata = listEndpointMetadata();
38715
+ return metadata.filter((endpoint) => {
38716
+ if (normalized.includes(endpoint.endpoint.toLowerCase())) {
38717
+ return true;
38718
+ }
38719
+ if (endpoint.title.toLowerCase().includes(normalized)) {
38720
+ return true;
38721
+ }
38722
+ return Object.keys(endpoint.fields).some(
38723
+ (field) => normalized.includes(field.toLowerCase())
38724
+ );
38725
+ }).slice(0, 2).map((endpoint) => ({
38726
+ id: `schema:${endpoint.endpoint}`,
38727
+ title: `${endpoint.title} schema`,
38728
+ url: getSchemaDocsUrl(endpoint.endpoint)
38729
+ }));
38730
+ }
38731
+ async function searchInstitutions(query) {
38732
+ const cert = extractCertLikeNumber(query);
38733
+ const filters = cert !== void 0 ? `CERT:${cert}` : `NAME:"${escapeFilterValue(query)}"`;
38734
+ const response = await queryEndpoint(ENDPOINTS.INSTITUTIONS, {
38735
+ filters,
38736
+ fields: "CERT,NAME,CITY,STALP,ACTIVE",
38737
+ limit: 3,
38738
+ sort_by: "ACTIVE",
38739
+ sort_order: "DESC"
38740
+ });
38741
+ return extractRecords(response).map(institutionSearchResult).filter((result) => result !== void 0);
38742
+ }
38743
+ async function searchFailures(query) {
38744
+ const cert = extractCertLikeNumber(query);
38745
+ const filters = cert !== void 0 ? `CERT:${cert}` : `NAME:"${escapeFilterValue(query)}"`;
38746
+ const response = await queryEndpoint(ENDPOINTS.FAILURES, {
38747
+ filters,
38748
+ fields: "CERT,NAME,CITY,STALP,FAILDATE,RESTYPE",
38749
+ limit: 2,
38750
+ sort_by: "FAILDATE",
38751
+ sort_order: "DESC"
38752
+ });
38753
+ return extractRecords(response).map(failureSearchResult).filter((result) => result !== void 0);
38754
+ }
38755
+ async function searchBranches(query) {
38756
+ const cert = extractCertLikeNumber(query);
38757
+ const normalized = normalizeQuery(query);
38758
+ const zip = normalized.match(/\b\d{5}\b/)?.[0];
38759
+ const filters = cert !== void 0 ? `CERT:${cert}` : zip !== void 0 ? `ZIP:${zip}` : `CITY:"${escapeFilterValue(normalized)}"`;
38760
+ const response = await queryEndpoint(ENDPOINTS.LOCATIONS, {
38761
+ filters,
38762
+ fields: "UNINUM,CERT,NAME,OFFNAME,ADDRESS,CITY,STALP,ZIP",
38763
+ limit: 3,
38764
+ sort_by: "UNINUM",
38765
+ sort_order: "ASC"
38766
+ });
38767
+ return extractRecords(response).map(branchSearchResult).filter((result) => result !== void 0);
38768
+ }
38769
+ async function safeSearch(searcher) {
38770
+ try {
38771
+ return await searcher();
38772
+ } catch {
38773
+ return [];
38774
+ }
38775
+ }
38776
+ function dedupeResults(results) {
38777
+ const seen = /* @__PURE__ */ new Set();
38778
+ const deduped = [];
38779
+ for (const result of results) {
38780
+ if (seen.has(result.id)) {
38781
+ continue;
38782
+ }
38783
+ seen.add(result.id);
38784
+ deduped.push(result);
38785
+ }
38786
+ return deduped.slice(0, MAX_SEARCH_RESULTS);
38787
+ }
38788
+ function recordText(record, fields) {
38789
+ return fields.map((field) => [field, asString(record[field])]).filter(([, value]) => value !== "").map(([field, value]) => `${field}: ${value}`).join("\n");
38790
+ }
38791
+ async function fetchInstitution(cert) {
38792
+ const response = await queryEndpoint(ENDPOINTS.INSTITUTIONS, {
38793
+ filters: `CERT:${cert}`,
38794
+ limit: 1
38795
+ });
38796
+ const record = extractRecords(response)[0];
38797
+ if (!record) {
38798
+ throw new Error(`No institution found for CERT ${cert}.`);
38799
+ }
38800
+ const title = getRecordTitle(record, `FDIC institution ${cert}`);
38801
+ return {
38802
+ id: `institution:${cert}`,
38803
+ title,
38804
+ text: recordText(record, [
38805
+ "CERT",
38806
+ "NAME",
38807
+ "CITY",
38808
+ "STALP",
38809
+ "STNAME",
38810
+ "ACTIVE",
38811
+ "ASSET",
38812
+ "DEP",
38813
+ "OFFICES",
38814
+ "BKCLASS",
38815
+ "REGAGNT",
38816
+ "ESTYMD"
38817
+ ]),
38818
+ url: getInstitutionUrl(cert),
38819
+ metadata: { type: "institution", cert, source: "FDIC BankFind Suite" }
38820
+ };
38821
+ }
38822
+ async function fetchFailure(cert) {
38823
+ const response = await queryEndpoint(ENDPOINTS.FAILURES, {
38824
+ filters: `CERT:${cert}`,
38825
+ limit: 1
38826
+ });
38827
+ const record = extractRecords(response)[0];
38828
+ if (!record) {
38829
+ throw new Error(`No failure record found for CERT ${cert}.`);
38830
+ }
38831
+ const title = getRecordTitle(record, `Failed bank ${cert}`);
38832
+ return {
38833
+ id: `failure:${cert}`,
38834
+ title,
38835
+ text: recordText(record, [
38836
+ "CERT",
38837
+ "NAME",
38838
+ "CITY",
38839
+ "STALP",
38840
+ "FAILDATE",
38841
+ "RESTYPE",
38842
+ "QBFASSET",
38843
+ "COST"
38844
+ ]),
38845
+ url: getFailedBankListUrl(),
38846
+ metadata: { type: "failure", cert, source: "FDIC Failed Bank List" }
38847
+ };
38848
+ }
38849
+ async function fetchBranch(rawId) {
38850
+ const decoded = decodeURIComponent(rawId);
38851
+ const [cert, brnum, zip] = decoded.split("~");
38852
+ const filters = cert && brnum ? [`CERT:${cert}`, `BRNUM:${brnum}`, zip ? `ZIP:${zip}` : void 0].filter(Boolean).join(" AND ") : `UNINUM:${decoded}`;
38853
+ const response = await queryEndpoint(ENDPOINTS.LOCATIONS, {
38854
+ filters,
38855
+ limit: 1
38856
+ });
38857
+ const record = extractRecords(response)[0];
38858
+ if (!record) {
38859
+ throw new Error(`No branch/location found for id ${rawId}.`);
38860
+ }
38861
+ const title = getRecordTitle(record, `FDIC branch ${rawId}`);
38862
+ const id = buildBranchId(record) ?? `branch:${rawId}`;
38863
+ return {
38864
+ id,
38865
+ title,
38866
+ text: recordText(record, [
38867
+ "UNINUM",
38868
+ "CERT",
38869
+ "UNINAME",
38870
+ "NAMEFULL",
38871
+ "ADDRESS",
38872
+ "CITY",
38873
+ "STALP",
38874
+ "ZIP",
38875
+ "COUNTY",
38876
+ "BRNUM",
38877
+ "BRSERTYP",
38878
+ "ESTYMD",
38879
+ "ENDEFYMD"
38880
+ ]),
38881
+ url: getBranchCitationUrl(),
38882
+ metadata: {
38883
+ type: "branch",
38884
+ cert: record.CERT,
38885
+ uninum: record.UNINUM,
38886
+ source: "FDIC BankFind Suite locations"
38887
+ }
38888
+ };
38889
+ }
38890
+ function fetchSchema(endpoint) {
38891
+ const metadata = getEndpointMetadata(endpoint);
38892
+ if (!metadata) {
38893
+ throw new Error(`No schema metadata found for endpoint ${endpoint}.`);
38894
+ }
38895
+ const fields = Object.values(metadata.fields).slice(0, 200).map((field) => {
38896
+ const title = field.title ? ` - ${field.title}` : "";
38897
+ return `${field.name}${title}`;
38898
+ }).join("\n");
38899
+ return {
38900
+ id: `schema:${endpoint}`,
38901
+ title: `${metadata.title} schema`,
38902
+ text: [
38903
+ metadata.description ?? metadata.title,
38904
+ "",
38905
+ `Endpoint: ${metadata.endpoint}`,
38906
+ `Source: ${metadata.source.docsBaseUrl}`,
38907
+ "",
38908
+ "Fields:",
38909
+ fields
38910
+ ].join("\n"),
38911
+ url: getSchemaDocsUrl(endpoint),
38912
+ metadata: {
38913
+ type: "schema",
38914
+ endpoint,
38915
+ field_count: Object.keys(metadata.fields).length,
38916
+ source: metadata.source.docsBaseUrl
38917
+ }
38918
+ };
38919
+ }
38920
+ async function fetchById(id) {
38921
+ const [kind, rawValue] = id.split(":", 2);
38922
+ if (!kind || !rawValue) {
38923
+ throw new Error(
38924
+ "Invalid fetch id. Expected institution:<CERT>, failure:<CERT>, branch:<id>, or schema:<endpoint>."
38925
+ );
38926
+ }
38927
+ if (kind === "institution") {
38928
+ const cert = Number.parseInt(rawValue, 10);
38929
+ if (!Number.isInteger(cert) || cert <= 0) {
38930
+ throw new Error(`Invalid institution CERT in id ${id}.`);
38931
+ }
38932
+ return fetchInstitution(cert);
38933
+ }
38934
+ if (kind === "failure") {
38935
+ const cert = Number.parseInt(rawValue, 10);
38936
+ if (!Number.isInteger(cert) || cert <= 0) {
38937
+ throw new Error(`Invalid failure CERT in id ${id}.`);
38938
+ }
38939
+ return fetchFailure(cert);
38940
+ }
38941
+ if (kind === "branch") {
38942
+ return fetchBranch(rawValue);
38943
+ }
38944
+ if (kind === "schema") {
38945
+ return fetchSchema(rawValue);
38946
+ }
38947
+ throw new Error(`Unsupported fetch id kind: ${kind}.`);
38948
+ }
38949
+ function registerChatGptRetrievalTools(server) {
38950
+ server.registerTool(
38951
+ "search",
38952
+ {
38953
+ title: "Search FDIC BankFind",
38954
+ description: "Use this when ChatGPT needs citation-friendly FDIC BankFind search results for institutions, failed banks, branches, or schema documentation.",
38955
+ inputSchema: SearchInputSchema,
38956
+ annotations: {
38957
+ readOnlyHint: true,
38958
+ destructiveHint: false,
38959
+ idempotentHint: true,
38960
+ openWorldHint: true
38961
+ }
38962
+ },
38963
+ async ({ query }) => {
38964
+ const normalized = normalizeQuery(query);
38965
+ const shouldIncludeFailures = shouldSearchFailures(normalized);
38966
+ const shouldIncludeBranches = shouldSearchBranches(normalized);
38967
+ const shouldIncludeSchemas = shouldSearchSchemas(normalized);
38968
+ const [institutions, failures, branches] = await Promise.all([
38969
+ safeSearch(() => searchInstitutions(normalized)),
38970
+ shouldIncludeFailures ? safeSearch(() => searchFailures(normalized)) : Promise.resolve([]),
38971
+ shouldIncludeBranches ? safeSearch(() => searchBranches(normalized)) : Promise.resolve([])
38972
+ ]);
38973
+ const fallbackFailures = failures.length === 0 && institutions.length === 0 ? await safeSearch(() => searchFailures(normalized)) : [];
38974
+ const fallbackBranches = branches.length === 0 && institutions.length === 0 ? await safeSearch(() => searchBranches(normalized)) : [];
38975
+ const schemas = shouldIncludeSchemas ? schemaSearchResults(normalized) : [];
38976
+ const results = dedupeResults([
38977
+ ...institutions,
38978
+ ...failures,
38979
+ ...branches,
38980
+ ...fallbackFailures,
38981
+ ...fallbackBranches,
38982
+ ...schemas
38983
+ ]);
38984
+ return {
38985
+ content: [{ type: "text", text: jsonText({ results }) }]
38986
+ };
38987
+ }
38988
+ );
38989
+ server.registerTool(
38990
+ "fetch",
38991
+ {
38992
+ title: "Fetch FDIC BankFind Result",
38993
+ description: "Use this when ChatGPT needs the full citation text for a result returned by search.",
38994
+ inputSchema: FetchInputSchema,
38995
+ annotations: {
38996
+ readOnlyHint: true,
38997
+ destructiveHint: false,
38998
+ idempotentHint: true,
38999
+ openWorldHint: true
39000
+ }
39001
+ },
39002
+ async ({ id }) => {
39003
+ try {
39004
+ const result = await fetchById(id);
39005
+ return {
39006
+ content: [{ type: "text", text: jsonText(result) }]
39007
+ };
39008
+ } catch (err) {
39009
+ return formatToolError(err);
39010
+ }
39011
+ }
39012
+ );
39013
+ }
39014
+
39015
+ // src/tools/chatgptBankDeepDive.ts
39016
+ var import_zod21 = require("zod");
39017
+
39018
+ // src/resources/chatgptAppResources.ts
39019
+ var BANK_DEEP_DIVE_WIDGET_URI = "ui://widget/fdic-bank-deep-dive-v1.html";
39020
+ var MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
39021
+ var BANK_DEEP_DIVE_WIDGET_HTML = String.raw`
39022
+ <div id="root" class="fdic-app">
39023
+ <section class="empty">Loading bank dashboard...</section>
39024
+ </div>
39025
+ <style>
39026
+ :root {
39027
+ color-scheme: light dark;
39028
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
39029
+ }
39030
+
39031
+ body {
39032
+ margin: 0;
39033
+ background: Canvas;
39034
+ color: CanvasText;
39035
+ }
39036
+
39037
+ .fdic-app {
39038
+ box-sizing: border-box;
39039
+ min-height: 100%;
39040
+ padding: 16px;
39041
+ }
39042
+
39043
+ .empty,
39044
+ .panel {
39045
+ border: 1px solid color-mix(in srgb, CanvasText 14%, transparent);
39046
+ border-radius: 8px;
39047
+ padding: 14px;
39048
+ }
39049
+
39050
+ .header {
39051
+ display: grid;
39052
+ gap: 6px;
39053
+ margin-bottom: 14px;
39054
+ }
39055
+
39056
+ .eyebrow {
39057
+ color: color-mix(in srgb, CanvasText 58%, transparent);
39058
+ font-size: 12px;
39059
+ font-weight: 650;
39060
+ text-transform: uppercase;
39061
+ }
39062
+
39063
+ h1,
39064
+ h2,
39065
+ p {
39066
+ margin: 0;
39067
+ }
39068
+
39069
+ h1 {
39070
+ font-size: 20px;
39071
+ line-height: 1.25;
39072
+ letter-spacing: 0;
39073
+ }
39074
+
39075
+ h2 {
39076
+ font-size: 14px;
39077
+ line-height: 1.35;
39078
+ letter-spacing: 0;
39079
+ }
39080
+
39081
+ .subtle {
39082
+ color: color-mix(in srgb, CanvasText 64%, transparent);
39083
+ font-size: 13px;
39084
+ }
39085
+
39086
+ .grid {
39087
+ display: grid;
39088
+ grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
39089
+ gap: 8px;
39090
+ margin: 12px 0;
39091
+ }
39092
+
39093
+ .metric {
39094
+ border: 1px solid color-mix(in srgb, CanvasText 12%, transparent);
39095
+ border-radius: 8px;
39096
+ padding: 10px;
39097
+ }
39098
+
39099
+ .metric dt {
39100
+ color: color-mix(in srgb, CanvasText 62%, transparent);
39101
+ font-size: 12px;
39102
+ margin: 0 0 6px;
39103
+ }
39104
+
39105
+ .metric dd {
39106
+ font-size: 16px;
39107
+ font-weight: 680;
39108
+ margin: 0;
39109
+ }
39110
+
39111
+ .section {
39112
+ margin-top: 14px;
39113
+ }
39114
+
39115
+ .list {
39116
+ display: grid;
39117
+ gap: 6px;
39118
+ margin: 8px 0 0;
39119
+ padding: 0;
39120
+ list-style: none;
39121
+ }
39122
+
39123
+ .list li {
39124
+ border-left: 3px solid color-mix(in srgb, CanvasText 24%, transparent);
39125
+ padding: 4px 0 4px 8px;
39126
+ font-size: 13px;
39127
+ }
39128
+
39129
+ .actions {
39130
+ display: flex;
39131
+ flex-wrap: wrap;
39132
+ gap: 8px;
39133
+ margin-top: 14px;
39134
+ }
39135
+
39136
+ button {
39137
+ border: 1px solid color-mix(in srgb, CanvasText 18%, transparent);
39138
+ border-radius: 8px;
39139
+ background: Canvas;
39140
+ color: CanvasText;
39141
+ cursor: pointer;
39142
+ font: inherit;
39143
+ font-size: 13px;
39144
+ padding: 8px 10px;
39145
+ }
39146
+ </style>
39147
+ <script type="module">
39148
+ const root = document.getElementById("root");
39149
+
39150
+ function formatMoney(value) {
39151
+ if (typeof value !== "number") return "n/a";
39152
+ if (Math.abs(value) >= 1000000) return "$" + (value / 1000000).toFixed(1) + "B";
39153
+ if (Math.abs(value) >= 1000) return "$" + (value / 1000).toFixed(1) + "M";
39154
+ return "$" + value.toLocaleString() + "k";
39155
+ }
39156
+
39157
+ function escapeHtml(value) {
39158
+ return String(value ?? "").replace(/[&<>"']/g, (char) => ({
39159
+ "&": "&amp;",
39160
+ "<": "&lt;",
39161
+ ">": "&gt;",
39162
+ '"': "&quot;",
39163
+ "'": "&#39;",
39164
+ }[char]));
39165
+ }
39166
+
39167
+ function render(data) {
39168
+ const institution = data?.institution ?? {};
39169
+ const assessment = data?.assessment ?? {};
39170
+ const metrics = data?.metrics ?? {};
39171
+ const signals = data?.risk_signals ?? [];
39172
+ const warnings = data?.warnings ?? [];
39173
+ const sources = data?.sources ?? [];
39174
+ const title = institution.name || "FDIC bank dashboard";
39175
+
39176
+ const signalItems = signals.length
39177
+ ? signals.map((signal) => "<li>" + escapeHtml(signal) + "</li>").join("")
39178
+ : "<li>No risk signals returned for this dashboard.</li>";
39179
+ const warningSection = warnings.length
39180
+ ? '<section class="section"><h2>Warnings</h2><ul class="list">' +
39181
+ warnings.map((warning) => "<li>" + escapeHtml(warning) + "</li>").join("") +
39182
+ "</ul></section>"
39183
+ : "";
39184
+ const sourceItems = sources
39185
+ .map((source) => '<li><a href="' + escapeHtml(source.url) + '" target="_blank" rel="noreferrer">' + escapeHtml(source.title) + "</a></li>")
39186
+ .join("");
39187
+
39188
+ root.innerHTML = [
39189
+ '<article class="panel">',
39190
+ '<header class="header">',
39191
+ '<p class="eyebrow">FDIC BankFind</p>',
39192
+ "<h1>" + escapeHtml(title) + "</h1>",
39193
+ '<p class="subtle">' + escapeHtml(institution.city) + ", " + escapeHtml(institution.state) + " · CERT " + escapeHtml(institution.cert) + " · " + escapeHtml(institution.active ? "Active" : "Inactive or unknown") + "</p>",
39194
+ '<p class="subtle">Report date: ' + escapeHtml(institution.report_date ?? "latest available") + " · Public analytical proxy, not an official CAMELS rating.</p>",
39195
+ "</header>",
39196
+ '<dl class="grid">',
39197
+ '<div class="metric"><dt>Assets</dt><dd>' + formatMoney(institution.asset_thousands) + "</dd></div>",
39198
+ '<div class="metric"><dt>Deposits</dt><dd>' + formatMoney(institution.deposit_thousands) + "</dd></div>",
39199
+ '<div class="metric"><dt>Offices</dt><dd>' + escapeHtml(institution.offices ?? "n/a") + "</dd></div>",
39200
+ '<div class="metric"><dt>Proxy band</dt><dd>' + escapeHtml(assessment.proxy_band ?? "n/a") + "</dd></div>",
39201
+ '<div class="metric"><dt>ROA</dt><dd>' + escapeHtml(metrics.roa ?? "n/a") + "</dd></div>",
39202
+ '<div class="metric"><dt>Tier 1 leverage</dt><dd>' + escapeHtml(metrics.tier1_leverage ?? "n/a") + "</dd></div>",
39203
+ "</dl>",
39204
+ '<section class="section"><h2>Risk Signals</h2><ul class="list">' + signalItems + "</ul></section>",
39205
+ warningSection,
39206
+ '<section class="section"><h2>Sources</h2><ul class="list">' + sourceItems + "</ul></section>",
39207
+ '<div class="actions">',
39208
+ '<button type="button" data-message="Compare CERT ' + escapeHtml(institution.cert) + ' with peers.">Compare peers</button>',
39209
+ '<button type="button" data-message="Show the branch footprint for CERT ' + escapeHtml(institution.cert) + '.">Branch footprint</button>',
39210
+ '<button type="button" data-message="Analyze the funding profile for CERT ' + escapeHtml(institution.cert) + '.">Funding profile</button>',
39211
+ "</div>",
39212
+ "</article>",
39213
+ ].join("");
39214
+ }
39215
+
39216
+ window.addEventListener("message", (event) => {
39217
+ const message = event.data;
39218
+ if (message?.method === "ui/notifications/tool-result") {
39219
+ render(message.params?.structuredContent);
39220
+ }
39221
+ });
39222
+
39223
+ root.addEventListener("click", (event) => {
39224
+ const button = event.target.closest("button[data-message]");
39225
+ if (!button) return;
39226
+ const text = button.getAttribute("data-message");
39227
+ window.parent.postMessage({
39228
+ jsonrpc: "2.0",
39229
+ method: "ui/message",
39230
+ params: { role: "user", content: [{ type: "text", text }] },
39231
+ }, "*");
39232
+ });
39233
+
39234
+ const initial = window.openai?.toolOutput ?? window.openai?.toolResponse?.structuredContent;
39235
+ if (initial) {
39236
+ render(initial);
39237
+ }
39238
+ </script>
39239
+ `.trim();
39240
+ function registerChatGptAppResources(server) {
39241
+ server.registerResource(
39242
+ "fdic-bank-deep-dive-widget",
39243
+ BANK_DEEP_DIVE_WIDGET_URI,
39244
+ {
39245
+ title: "FDIC Bank Deep Dive Widget",
39246
+ description: "Interactive ChatGPT widget for a public FDIC bank deep-dive dashboard.",
39247
+ mimeType: MCP_APP_MIME_TYPE
39248
+ },
39249
+ async () => ({
39250
+ contents: [
39251
+ {
39252
+ uri: BANK_DEEP_DIVE_WIDGET_URI,
39253
+ mimeType: MCP_APP_MIME_TYPE,
39254
+ text: BANK_DEEP_DIVE_WIDGET_HTML,
39255
+ _meta: {
39256
+ ui: {
39257
+ prefersBorder: true,
39258
+ csp: {
39259
+ connectDomains: [],
39260
+ resourceDomains: []
39261
+ }
39262
+ },
39263
+ "openai/widgetDescription": "Renders an FDIC bank deep-dive dashboard from public BankFind data.",
39264
+ "openai/widgetPrefersBorder": true,
39265
+ "openai/widgetCSP": {
39266
+ connect_domains: [],
39267
+ resource_domains: []
39268
+ }
39269
+ }
39270
+ }
39271
+ ]
39272
+ })
39273
+ );
39274
+ }
39275
+
39276
+ // src/tools/chatgptBankDeepDive.ts
39277
+ var BankDeepDiveInputSchema = import_zod21.z.object({
39278
+ cert: import_zod21.z.number().int().positive().describe("FDIC Certificate Number of the institution to render."),
39279
+ repdte: import_zod21.z.string().regex(/^\d{8}$/).optional().describe(
39280
+ "Quarter-end report date in YYYYMMDD format. Defaults to the most recent likely published quarter."
39281
+ )
39282
+ });
39283
+ function asNumber3(value) {
39284
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
39285
+ }
39286
+ function asString2(value) {
39287
+ return value === void 0 || value === null ? "" : String(value);
39288
+ }
39289
+ function formatRatio(value) {
39290
+ return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(2)}%` : void 0;
39291
+ }
39292
+ function collectDashboardRiskSignals(financials) {
39293
+ if (!financials) {
39294
+ return [];
39295
+ }
39296
+ const signals = [];
39297
+ const tier1Leverage = asNumber3(financials.IDT1CER);
39298
+ const roa = asNumber3(financials.ROA);
39299
+ const noncurrentLoans = asNumber3(financials.NCLNLSR);
39300
+ const brokeredDeposits = asNumber3(financials.BRO);
39301
+ if (tier1Leverage !== void 0 && tier1Leverage < 5) {
39302
+ signals.push(
39303
+ `Tier 1 leverage ratio is ${tier1Leverage.toFixed(2)}%, below the 5% well-capitalized threshold.`
39304
+ );
39305
+ }
39306
+ if (roa !== void 0 && roa < 0) {
39307
+ signals.push(`Return on assets is negative at ${roa.toFixed(2)}%.`);
39308
+ }
39309
+ if (noncurrentLoans !== void 0 && noncurrentLoans > 3) {
39310
+ signals.push(
39311
+ `Noncurrent loans ratio is elevated at ${noncurrentLoans.toFixed(2)}%.`
39312
+ );
39313
+ }
39314
+ if (brokeredDeposits !== void 0 && brokeredDeposits > 0) {
39315
+ signals.push(
39316
+ "Brokered deposit fields are present; review funding-profile analysis for reliance context."
39317
+ );
39318
+ }
39319
+ return signals;
39320
+ }
39321
+ function buildDashboardText(institution, repdte, riskSignals, warnings) {
39322
+ const lines = [
39323
+ `FDIC Bank Deep Dive: ${asString2(institution.NAME)} (CERT ${asString2(institution.CERT)})`,
39324
+ `${asString2(institution.CITY)}, ${asString2(institution.STALP)} | Report date: ${repdte}`,
39325
+ "This dashboard uses public FDIC BankFind data and is not an official CAMELS rating or supervisory conclusion.",
39326
+ "",
39327
+ `Assets: ${asString2(institution.ASSET) || "n/a"} ($thousands)`,
39328
+ `Deposits: ${asString2(institution.DEP) || "n/a"} ($thousands)`,
39329
+ `Offices: ${asString2(institution.OFFICES) || "n/a"}`
39330
+ ];
39331
+ if (riskSignals.length > 0) {
39332
+ lines.push("", "Risk signals:", ...riskSignals.map((signal) => `- ${signal}`));
39333
+ }
39334
+ if (warnings.length > 0) {
39335
+ lines.push("", "Warnings:", ...warnings.map((warning) => `- ${warning}`));
39336
+ }
39337
+ return lines.join("\n");
39338
+ }
39339
+ function registerChatGptBankDeepDiveTool(server) {
39340
+ server.registerTool(
39341
+ "fdic_show_bank_deep_dive",
39342
+ {
39343
+ title: "Show Bank Deep Dive Dashboard",
39344
+ description: "Use this when the user wants a scannable ChatGPT dashboard for one FDIC-insured institution, including identity, public financial metrics, risk signals, and source links.",
39345
+ inputSchema: BankDeepDiveInputSchema,
39346
+ annotations: {
39347
+ readOnlyHint: true,
39348
+ destructiveHint: false,
39349
+ idempotentHint: true,
39350
+ openWorldHint: true
39351
+ },
39352
+ _meta: {
39353
+ ui: { resourceUri: BANK_DEEP_DIVE_WIDGET_URI },
39354
+ "openai/outputTemplate": BANK_DEEP_DIVE_WIDGET_URI,
39355
+ "openai/toolInvocation/invoking": "Building bank dashboard...",
39356
+ "openai/toolInvocation/invoked": "Bank dashboard ready"
39357
+ }
39358
+ },
39359
+ async (rawParams) => {
39360
+ const repdte = rawParams.repdte ?? getDefaultReportDate();
39361
+ const dateError = validateQuarterEndDate(repdte, "repdte");
39362
+ if (dateError) {
39363
+ return formatToolError(new Error(dateError));
39364
+ }
39365
+ try {
39366
+ const [institutionResponse, financialsResponse] = await Promise.all([
39367
+ queryEndpoint(ENDPOINTS.INSTITUTIONS, {
39368
+ filters: `CERT:${rawParams.cert}`,
39369
+ fields: "CERT,NAME,CITY,STALP,STNAME,ACTIVE,ASSET,DEP,OFFICES,BKCLASS,REGAGNT,ESTYMD",
39370
+ limit: 1
39371
+ }),
39372
+ queryEndpoint(ENDPOINTS.FINANCIALS, {
39373
+ filters: `CERT:${rawParams.cert} AND REPDTE:${repdte}`,
39374
+ fields: "CERT,REPDTE,ASSET,DEP,ROA,ROE,IDT1CER,NCLNLSR,LNLSDEPR,NIMY,EEFFR",
39375
+ limit: 1
39376
+ })
39377
+ ]);
39378
+ const institution = extractRecords(institutionResponse)[0];
39379
+ if (!institution) {
39380
+ return formatToolError(
39381
+ new Error(`No institution found with CERT number ${rawParams.cert}.`)
39382
+ );
39383
+ }
39384
+ const financials = extractRecords(financialsResponse)[0];
39385
+ const warnings = financials ? [] : [
39386
+ `No financial record found for CERT ${rawParams.cert} at ${repdte}. Try an earlier quarter-end date.`
39387
+ ];
39388
+ const riskSignals = collectDashboardRiskSignals(financials);
39389
+ const structuredContent = {
39390
+ institution: {
39391
+ cert: rawParams.cert,
39392
+ name: asString2(institution.NAME),
39393
+ city: asString2(institution.CITY),
39394
+ state: asString2(institution.STALP),
39395
+ active: institution.ACTIVE === 1 || institution.ACTIVE === "1",
39396
+ asset_thousands: asNumber3(financials?.ASSET) ?? asNumber3(institution.ASSET),
39397
+ deposit_thousands: asNumber3(financials?.DEP) ?? asNumber3(institution.DEP),
39398
+ offices: asNumber3(institution.OFFICES),
39399
+ charter_class: asString2(institution.BKCLASS),
39400
+ regulator: asString2(institution.REGAGNT),
39401
+ established: asString2(institution.ESTYMD),
39402
+ report_date: repdte
39403
+ },
39404
+ assessment: {
39405
+ official_rating: false,
39406
+ proxy_band: riskSignals.length > 0 ? "review" : "no major public flags",
39407
+ caveat: "Public off-site analytical dashboard; not an official CAMELS rating or confidential supervisory conclusion."
39408
+ },
39409
+ metrics: {
39410
+ roa: formatRatio(financials?.ROA),
39411
+ roe: formatRatio(financials?.ROE),
39412
+ tier1_leverage: formatRatio(financials?.IDT1CER),
39413
+ noncurrent_loans: formatRatio(financials?.NCLNLSR),
39414
+ loan_to_deposit: formatRatio(financials?.LNLSDEPR),
39415
+ net_interest_margin: formatRatio(financials?.NIMY),
39416
+ efficiency_ratio: formatRatio(financials?.EEFFR)
39417
+ },
39418
+ risk_signals: riskSignals,
39419
+ warnings,
39420
+ sources: [
39421
+ {
39422
+ title: "FDIC BankFind institution profile",
39423
+ url: getInstitutionUrl(rawParams.cert)
39424
+ }
39425
+ ]
39426
+ };
39427
+ return {
39428
+ content: [
39429
+ {
39430
+ type: "text",
39431
+ text: truncateIfNeeded(
39432
+ buildDashboardText(institution, repdte, riskSignals, warnings),
39433
+ CHARACTER_LIMIT
39434
+ )
39435
+ }
39436
+ ],
39437
+ structuredContent,
39438
+ _meta: {
39439
+ widget: {
39440
+ resourceUri: BANK_DEEP_DIVE_WIDGET_URI
39441
+ },
39442
+ raw: {
39443
+ institution,
39444
+ financials: financials ?? null
39445
+ }
39446
+ }
39447
+ };
39448
+ } catch (err) {
39449
+ return formatToolError(err);
39450
+ }
39451
+ }
39452
+ );
39453
+ }
39454
+
38628
39455
  // src/resources/schemaResources.ts
38629
39456
  var RESOURCE_SCHEME = "fdic";
38630
39457
  var INDEX_URI = `${RESOURCE_SCHEME}://schemas/index`;
@@ -38721,6 +39548,9 @@ function createServer() {
38721
39548
  registerFranchiseFootprintTools(server);
38722
39549
  registerHoldingCompanyProfileTools(server);
38723
39550
  registerRegionalContextTools(server);
39551
+ registerChatGptRetrievalTools(server);
39552
+ registerChatGptBankDeepDiveTool(server);
39553
+ registerChatGptAppResources(server);
38724
39554
  registerSchemaResources(server);
38725
39555
  return server;
38726
39556
  }