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/README.md +3 -0
- package/dist/index.js +890 -58
- package/dist/server.js +890 -58
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -32,7 +32,7 @@ var import_types = require("@modelcontextprotocol/sdk/types.js");
|
|
|
32
32
|
var import_express2 = __toESM(require("express"));
|
|
33
33
|
|
|
34
34
|
// src/constants.ts
|
|
35
|
-
var VERSION = true ? "1.
|
|
35
|
+
var VERSION = true ? "1.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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
+
"&": "&",
|
|
39160
|
+
"<": "<",
|
|
39161
|
+
">": ">",
|
|
39162
|
+
'"': """,
|
|
39163
|
+
"'": "'",
|
|
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
|
}
|