fdic-mcp-server 1.23.1 → 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.1" : 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;
@@ -99,7 +99,7 @@ var DEFAULT_CHAT_RATE_LIMIT_MAX_REQUESTS = 10;
99
99
  var DEFAULT_CHAT_RATE_LIMIT_WINDOW_MS = 6e4;
100
100
  var DEFAULT_CHAT_MAX_MESSAGES = 20;
101
101
  var DEFAULT_CHAT_MAX_MESSAGE_LENGTH = 500;
102
- var DEFAULT_CHAT_MAX_TOOL_ROUNDS = 5;
102
+ var DEFAULT_CHAT_MAX_TOOL_ROUNDS = 15;
103
103
  var DEFAULT_CHAT_GENERATE_RETRIES = 2;
104
104
  var genAIModulePromise;
105
105
  function loadGenAIModule() {
@@ -343,7 +343,10 @@ async function runConversation(ai, model, functionDeclarations, server, history)
343
343
  }
344
344
  return { history: contents, reply };
345
345
  }
346
- throw new Error("Chat tool-call limit exceeded");
346
+ return {
347
+ history: contents,
348
+ reply: "I used several tools but couldn\u2019t reach a final answer. Try a more specific question."
349
+ };
347
350
  }
348
351
  function sweepIdleChatSessions(sessions, idleTimeoutMs, now) {
349
352
  for (const [sessionId, session] of sessions.entries()) {
@@ -432,8 +435,7 @@ function createChatRouter(options) {
432
435
  error
433
436
  });
434
437
  const message = error instanceof Error ? error.message : "Failed to process chat request";
435
- const status = message === "Chat tool-call limit exceeded" ? 502 : 500;
436
- res.status(status).json({ error: message });
438
+ res.status(500).json({ error: message });
437
439
  }
438
440
  });
439
441
  return router;
@@ -31881,50 +31883,7 @@ function registerInstitutionTools(server) {
31881
31883
  "fdic_search_institutions",
31882
31884
  {
31883
31885
  title: "Search FDIC Institutions",
31884
- description: `Search for FDIC-insured financial institutions (banks and savings institutions) using flexible filters.
31885
-
31886
- Returns institution profile data including name, location, charter class, asset size, deposit totals, profitability metrics, and regulatory status.
31887
-
31888
- Common filter examples:
31889
- - By state: STNAME:"California"
31890
- - Active banks only: ACTIVE:1
31891
- - Large banks: ASSET:[10000000 TO *] (assets in $thousands)
31892
- - By bank class: BKCLASS:N (national bank), BKCLASS:SM (state member bank), BKCLASS:NM (state non-member)
31893
- - By name: NAME:"Wells Fargo"
31894
- - Commercial banks: CB:1
31895
- - Savings institutions: MUTUAL:1
31896
- - Recently established: ESTYMD:[2010-01-01 TO *]
31897
-
31898
- Charter class codes (BKCLASS):
31899
- N = National commercial bank (OCC-supervised)
31900
- SM = State-chartered, Federal Reserve member
31901
- NM = State-chartered, non-member (FDIC-supervised)
31902
- SB = Federal savings bank (OCC-supervised)
31903
- SA = State savings association
31904
- OI = Insured branch of foreign bank
31905
-
31906
- Key returned fields:
31907
- - CERT: FDIC Certificate Number (unique ID)
31908
- - NAME: Institution name
31909
- - CITY, STALP (two-letter state code), STNAME (full state name): Location
31910
- - ASSET: Total assets ($thousands)
31911
- - DEP: Total deposits ($thousands)
31912
- - BKCLASS: Charter class code (see above)
31913
- - ACTIVE: 1 if currently active, 0 if inactive
31914
- - ROA, ROE: Profitability ratios
31915
- - OFFICES: Number of branch offices
31916
- - ESTYMD: Establishment date (YYYY-MM-DD)
31917
- - REGAGNT: Primary federal regulator (OCC, FRS, FDIC)
31918
-
31919
- Args:
31920
- - filters (string, optional): ElasticSearch query filter
31921
- - fields (string, optional): Comma-separated field names
31922
- - limit (number): Records to return, 1-10000 (default: 20)
31923
- - offset (number): Pagination offset (default: 0)
31924
- - sort_by (string, optional): Field to sort by
31925
- - sort_order ('ASC'|'DESC'): Sort direction (default: 'ASC')
31926
-
31927
- 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.",
31928
31887
  inputSchema: CommonQuerySchema,
31929
31888
  annotations: {
31930
31889
  readOnlyHint: true,
@@ -31968,15 +31927,7 @@ Prefer concise human-readable summaries or tables when answering users. Structur
31968
31927
  "fdic_get_institution",
31969
31928
  {
31970
31929
  title: "Get Institution by Certificate Number",
31971
- description: `Retrieve detailed information for a specific FDIC-insured institution using its FDIC Certificate Number (CERT).
31972
-
31973
- Use this when you know the exact CERT number for an institution. To find a CERT number, use fdic_search_institutions first.
31974
-
31975
- Args:
31976
- - cert (number): FDIC Certificate Number (e.g., 3511 for Bank of America)
31977
- - fields (string, optional): Comma-separated list of fields to return
31978
-
31979
- 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.",
31980
31931
  inputSchema: CertSchema,
31981
31932
  annotations: {
31982
31933
  readOnlyHint: true,
@@ -38623,6 +38574,884 @@ NOTE: Requires FRED_API_KEY environment variable for reliable data access. Degra
38623
38574
  );
38624
38575
  }
38625
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
+
38626
39455
  // src/resources/schemaResources.ts
38627
39456
  var RESOURCE_SCHEME = "fdic";
38628
39457
  var INDEX_URI = `${RESOURCE_SCHEME}://schemas/index`;
@@ -38719,6 +39548,9 @@ function createServer() {
38719
39548
  registerFranchiseFootprintTools(server);
38720
39549
  registerHoldingCompanyProfileTools(server);
38721
39550
  registerRegionalContextTools(server);
39551
+ registerChatGptRetrievalTools(server);
39552
+ registerChatGptBankDeepDiveTool(server);
39553
+ registerChatGptAppResources(server);
38722
39554
  registerSchemaResources(server);
38723
39555
  return server;
38724
39556
  }