Skip to main content
Glama
@cyanheads/fdic-banks-mcp-server

@cyanheads/fdic-banks-mcp-server

by cyanheads
README.md
<div align="center">
  <h1>@cyanheads/fdic-banks-mcp-server</h1>
  <p><b>Search FDIC-insured banks, Call Report financials, peer rankings, failures, mergers, deposit share via MCP. STDIO or Streamable HTTP.</b>
  <div>11 Tools</div>
  </p>
</div>

<div align="center">

[![Version](https://img.shields.io/badge/Version-0.3.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/fdic-banks-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.4.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/fdic-banks-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/fdic-banks-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.4-blueviolet.svg?style=flat-square)](https://bun.sh/)

</div>

<div align="center">

[![Install in Claude Desktop](https://img.shields.io/badge/Install_in-Claude_Desktop-D97757?style=for-the-badge&logo=anthropic&logoColor=white)](https://github.com/cyanheads/fdic-banks-mcp-server/releases/latest/download/fdic-banks-mcp-server.mcpb) [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=fdic-banks-mcp-server&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBjeWFuaGVhZHMvZmRpYy1iYW5rcy1tY3Atc2VydmVyIl19) [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=for-the-badge&logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect?url=vscode:mcp/install?%7B%22name%22%3A%22fdic-banks-mcp-server%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40cyanheads%2Ffdic-banks-mcp-server%22%5D%7D)

[![Framework](https://img.shields.io/badge/Built%20on-@cyanheads/mcp--ts--core-67E8F9?style=flat-square)](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)

</div>

<div align="center">

**Public Hosted Server:** [https://fdic-banks.caseyjhand.com/mcp](https://fdic-banks.caseyjhand.com/mcp)

</div>

---

## Overview

Every FDIC-insured bank and savings institution, open, merged, or failed, from the [FDIC BankFind Suite API](https://api.fdic.gov/banks/docs/). Read quarterly Call Report financials back to 1984, rank a bank against peers, screen many at once, search failures since 1934, trace a bank's mergers, renames, and charter changes, and map branch deposits and market share. Credit unions are NCUA-insured and not included. No API key required.

### Tools

| Tool | Description |
|:---|:---|
| `fdic_search_institutions` | Find institutions by name, CERT, location, size, charter class, or holding company; returns the CERT every other tool takes |
| `fdic_get_institution_financials` | One institution's quarterly Call Report history, most recent first, with its profile |
| `fdic_get_institution_history` | One institution's structure-change events, newest first: mergers, failures, branch purchases, renames, relocations, and charter changes, with the other institution named |
| `fdic_compare_peers` | One institution against a peer group for one quarter: peer median, quartiles, range, percentile, and rank per metric |
| `fdic_query_financials` | Multi-bank, multi-quarter Call Report panel filtered by CERTs, state, asset range, and metric thresholds |
| `fdic_search_failures` | Bank failures and assistance transactions since 1934, with totals, per-method subtotals, and optional grouping |
| `fdic_get_deposits` | Summary of Deposits: an institution's branches and state market share, or a market ranked by deposits with HHI |
| `fdic_list_reference` | Metric catalog, charter classes, failure methods, structure-change codes, insurance funds, peer asset bands, and dataset coverage |
| `fdic_dataframe_describe` | List staged dataframes, or describe one by name: provenance, columns, and units |
| `fdic_dataframe_query` | Run one read-only DuckDB `SELECT` across staged dataframes |
| `fdic_dataframe_drop` | Drop a staged dataframe before its TTL; off unless `FDIC_DATAFRAME_DROP_ENABLED=true` |

### Dataframe workflow

1. `fdic_query_financials` and `fdic_get_deposits` return one page of rows at a time (`limit`, `offset`); while rows remain, `next_offset` names the next page. At `offset` 0, when the full result is larger than `limit`, they also stage it as a `df_XXXXX_XXXXX` table and name it in the `dataset` field; later pages never stage.
2. Pass that name to `fdic_dataframe_describe` for the table's columns, units, and basis.
3. Query it with `fdic_dataframe_query`: joins, aggregates, window functions, and CTEs across every staged table. `register_as` saves a result as a new table.
4. Tables expire after `FDIC_DATASET_TTL_SECONDS` (default 24 hours). The live tables hold 1,000,000 rows together, and the oldest are dropped past that.

Staging runs on DuckDB and is on by default; `CANVAS_PROVIDER_TYPE=none` turns it off. The Claude Desktop `.mcpb` bundle ships without DuckDB's native binding and sets `CANVAS_PROVIDER_TYPE=none`, so there the two producers return their rows a page at a time, continued with `offset` and `next_offset`, the dataframe tools report that dataframes are off, and every other tool works normally.

## Capability reference

### `fdic_search_institutions` <sub>tool</sub>

- Filters: `name` (up to 100 characters; every word must match a current, former, or trade name), `certs` (first 50 unique), `state`, `city`, `bank_classes`, `min_assets` / `max_assets` (thousands of US dollars), `holding_company_rssd`
- `status` is `active`, `inactive`, or `any`; it defaults to `any` when `name` or `certs` is given and `active` otherwise, and `status_filter` reports the value applied
- Up to 100 per page (default 20), paged with `offset` / `next_offset`. Records carry `active`, `successor_cert` for merged or failed banks, `holding_company.rssd`, and `matched_on`; CERTs with no record land in `missing_certs`

---

### `fdic_get_institution_financials` <sub>tool</sub>

- `cert` required; up to 25 `metrics` (default: a 15-metric health set); `quarters` accepts 1–10,000 and queries at most 200 (default 8), bounded by `from_date` / `to_date` as `2025-03-31`, `20250331`, or `2025Q1`
- Rows run most recent first, each a `values` map of metric → number, `null` when not reported; `metric_definitions` gives each metric's FDIC field, unit, and basis
- `quarters_available` counts the quarters in the window; an unknown CERT fails as `cert_not_found`

---

### `fdic_get_institution_history` <sub>tool</sub>

- `cert` required; `event_types` (omitted = every type but `branch`) selects from `merger`, `failure_or_assistance`, `branch_purchase`, `name_change`, `relocation`, `charter_change`, `opening`, `closing`, `other`, and `branch` (branch-level openings, closings, sales, and relocations); `from_date` / `to_date` (YYYY-MM-DD, inclusive) bound the effective date
- Events run newest first. Each carries `effective_date`, `event_type`, `change_code`, `change_description`, and `role` (`subject`, `outgoing`, `survivor`, `buyer`, `seller`), plus `counterparty` (CERT and name of the other institution), `changes` (former and new values of name, address, city, state, class, chartering agency, regulator), `branch` (the office, on branch events), and `transaction_number` (shared by the rows of one deal) where FDIC recorded them. A merger or failure appears once, whichever side the CERT was on
- `limit` (default 50, no maximum) and `offset` page the list; `total`, `next_offset`, and `event_types_filter` report the match; `data_as_of` is the history index build time. Mergers start in 1969 and failures in 1970; an unknown CERT fails as `cert_not_found`

---

### `fdic_compare_peers` <sub>tool</sub>

- `cert` required; `report_date` defaults to the latest published quarter (`report_date_defaulted`); up to 20 `metrics`
- Peer group: `peer_asset_band` (`same` by default, `any`, `under_100m`, `100m_1b`, `1b_10b`, `10b_250b`, `over_250b`) plus an optional `peer_state`, or `peer_certs` (first 200 unique); `peer_certs` can't be combined with the band or state (`conflicting_peer_filters`)
- Each comparison returns `value`, `peer_median`, `peer_p25` / `peer_p75`, `peer_min` / `peer_max`, `percentile`, and `rank` of `rank_of`, over the `peer_count_with_value` peers that reported the metric; `peer_cohort_incomplete` marks a peer group only partly processed

---

### `fdic_query_financials` <sub>tool</sub>

- Filters: up to 100 `certs`, `state`, `min_assets` / `max_assets`, up to 5 `metric_filters` (`metric` with `min` and/or `max`), and up to 30 `metrics`; `from_date` / `to_date` default to the latest published quarter
- `limit` (default 50) rows per page, ordered by `sort_by` / `sort_order`, continued with `offset` / `next_offset`; at `offset` 0 a larger panel is also staged whole as a dataframe
- The panel is capped at `FDIC_PANEL_MAX_ROWS` (default 50,000), keeping the newest quarters; `total_matching`, `rows_fetched`, and `panel_truncated` report what was left out

---

### `fdic_search_failures` <sub>tool</sub>

- Filters: `name`, `certs` (first 50 unique; most events before 1977 carry none), `state`, `from_date` / `to_date` (YYYY-MM-DD), `methods`, `min_assets`; `resolution` is `failure` by default, or `assistance` or `all`
- `summary` totals every matching event with a `by_method` breakdown; `group_by` (`year`, `state`, `method`, `insurance_fund`) adds `groups`
- `total_assets_missing_count`, `total_deposits_missing_count`, and `estimated_loss_missing_count` count unreported amounts, and a total is `null` when no match has a recorded amount. `sort` by date, loss, or assets; up to 200 per page (default 25)

---

### `fdic_get_deposits` <sub>tool</sub>

- `cert` alone returns the institution's branches and deposit share in each state (`mode: "institution"`); a geography (`state`, `county`, `city`, `zip`, `msa_code`) ranks every institution in that market with `hhi` (`market`); both together give `institution_in_market`. `county` and `city` require `state`
- `year` from 1994 on, defaulting to the latest survey (`year_defaulted`); deposits are domestic branch deposits as of June 30, in thousands of US dollars
- `limit` (default 25) rows per page, continued with `offset` / `next_offset`; at `offset` 0 a larger set is also staged as a dataframe, and `total_rows` gives the full count

---

### `fdic_list_reference` <sub>tool</sub>

- One `topic` per call: `metrics`, `bank_classes`, `failure_methods`, `change_codes`, `insurance_funds`, `peer_asset_bands`, or `coverage`
- `metrics` lists all 49 metric names with FDIC field, unit, basis, caveat, and `in_default_set`; `change_codes` lists the 41 FDIC structure-change codes with their `event_type` and `level` (`institution`, `branch`, or `both`); served from built-in tables with no request to FDIC

---

### `fdic_dataframe_describe` <sub>tool</sub>

- `name` (a `df_XXXXX_XXXXX` from a `dataset` field) returns that table's source tool, parameters, row count, expiry, `column_schema`, and `column_units`
- Without `name`, lists live tables newest first, 50 per page, paged with `offset` / `next_offset`; over HTTP with `MCP_AUTH_MODE=none` listing is off and fails as `listing_unavailable`
- Fails as `canvas_unavailable` when dataframe staging is off

---

### `fdic_dataframe_query` <sub>tool</sub>

- One `SELECT` per call, up to 20,000 characters; writes, DDL, file-reading functions, and system catalogs are rejected with typed reasons (`non_select_statement`, `denied_function`, `system_catalog_access`, and others)
- `row_limit` accepts 1–1,000,000 (default 1,000) and executes at most 10,000; `row_count_capped` reports when more rows matched. `preview` sets the inline row count
- `register_as` saves the result as a new dataframe, or fails as `register_as_too_large` past 1,000,000 rows; `BIGINT` results such as `COUNT(*)` come back as strings

---

### `fdic_dataframe_drop` <sub>tool</sub>

- `name` of a staged dataframe; returns `dropped: true`, or `dropped: false` when nothing by that name was staged
- Disabled unless `FDIC_DATAFRAME_DROP_ENABLED=true`; otherwise tables expire on their TTL

## Features

Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.

FDIC-specific:

- Reads five BankFind datasets: institutions (every insured charter, active and inactive), quarterly Call Report financials from 1984, failures and assistance transactions from 1934, structure-change history (mergers from 1969, failures from 1970), and the annual Summary of Deposits from 1994
- Paced requests (`FDIC_RATE_LIMIT_RPS`, at most four in flight) with a response cache and a cooldown after an FDIC 429, retried honoring `Retry-After`; each call has a 45-second budget (55 seconds for a panel, or a history read that includes `branch`)
- A curated catalog of 49 Call Report metrics with FDIC field code, unit, and basis; `_ytd` metrics accumulate from January 1, and unsuffixed income and return metrics cover a single quarter
- Handles BankFind's quirks: case-sensitive code values, string-typed report dates, absence sentinels, and ratios reported as `0` when a bank did not report them
- Array inputs are deduplicated in order and capped at 1,000 items; empty `certs`, `peer_certs`, `bank_classes`, `methods`, and `event_types` are rejected

Agent-friendly output:

- Every data response carries `data_as_of`, `metric_definitions`, and `report_date_defaulted` / `year_defaulted` when the server picked the period
- Unreported metrics come back `null`; nothing is filled in
- Typed errors with recovery text that names the next call (`cert_not_found`, `conflicting_peer_filters`, `no_report_for_period`); rate-limit errors (`pacer_shed`, `upstream_rate_limited`) carry `retryAfter`
- `processed_certs` / `deferred_certs` and `processed_metrics` / `deferred_metrics` separate what was queried from what to pass in a new call
- Publishes the `fdic-banks-guide` agent skill (root `skills/`: identifier chains, gates, multi-step workflows), served to MCP clients as `skill://fdic-banks-guide/SKILL.md` and shipped in the npm package, Docker image, and `.mcpb` bundle

## Getting started

Connect to the hosted instance, no install:

```sh
claude mcp add --transport http fdic-banks-mcp-server https://fdic-banks.caseyjhand.com/mcp
```

Every caller of the hosted instance shares one FDIC request pacer and one dataframe workspace, so `fdic_dataframe_describe` takes a table's exact name there; listing is off.

Or run it locally:

```sh
claude mcp add fdic-banks-mcp-server -- npx -y @cyanheads/fdic-banks-mcp-server
```

For other clients, add this to the MCP config file:

```json
{
  "mcpServers": {
    "fdic-banks-mcp-server": {
      "command": "npx",
      "args": ["-y", "@cyanheads/fdic-banks-mcp-server"]
    }
  }
}
```

No API key or account is needed: the [FDIC BankFind Suite API](https://api.fdic.gov/banks/docs/) is public. For Docker or a source build, see [Running the server](#running-the-server).

## Configuration

| Variable | Description | Default |
|:---|:---|:---|
| `FDIC_RATE_LIMIT_RPS` | Maximum request starts per second to `api.fdic.gov` (1–15), shared by every caller of the process. | `8` |
| `FDIC_CACHE_TTL_SECONDS` | Lifetime of the in-process FDIC response cache, in seconds; `0` disables caching. | `3600` |
| `FDIC_PANEL_MAX_ROWS` | Row cap for one `fdic_query_financials` panel (1,000–200,000); the newest quarters are kept when it binds. | `50000` |
| `FDIC_DATASET_TTL_SECONDS` | Lifetime of each staged dataframe, in seconds (minimum 60). | `86400` |
| `FDIC_DATAFRAME_DROP_ENABLED` | Set `true` to enable `fdic_dataframe_drop`. | `false` |
| `CANVAS_PROVIDER_TYPE` | Dataframe engine: `duckdb`, or `none` to turn staging and the dataframe tools off. The server sets `duckdb` when unset or empty; the Claude Desktop bundle sets `none`. | `duckdb` |
| `MCP_TRANSPORT_TYPE` | Transport: `stdio` or `http`. | `stdio` |
| `MCP_HTTP_PORT` | HTTP server port. | `3010` |
| `MCP_SESSION_MODE` | HTTP session mode: `stateless`, `stateful`, or `auto`. | `stateless` |
| `MCP_AUTH_MODE` | Authentication: `none`, `jwt`, or `oauth`. | `none` |
| `MCP_LOG_LEVEL` | Log level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
| `LOGS_DIR` | Directory for log files (Node.js only). | `<app-root>/logs` |
| `STORAGE_PROVIDER_TYPE` | Storage backend: `in-memory`, `filesystem`, `supabase`, `cloudflare-kv/r2/d1`. | `in-memory` |
| `OTEL_ENABLED` | Enable [OpenTelemetry](https://github.com/cyanheads/mcp-ts-core/tree/main/docs/telemetry). | `false` |

See [`.env.example`](./.env.example) for the full list of optional overrides.

## Running the server

### Local development

Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).

- **Clone and install:**

  ```sh
  git clone https://github.com/cyanheads/fdic-banks-mcp-server.git
  cd fdic-banks-mcp-server && bun install
  cp .env.example .env   # every variable is optional; edit to override a default
  ```

- **Build and run the production version:**

  ```sh
  # One-time build
  bun run rebuild

  # Run the built server
  bun run start:http   # http://localhost:3010/mcp
  # or
  bun run start:stdio
  ```

- **Run checks and tests:**

  ```sh
  bun run devcheck  # Lints, formats, type-checks, and more
  bun run test      # Runs the test suite
  ```

### Docker

```sh
docker run --rm -p 3010:3010 ghcr.io/cyanheads/fdic-banks-mcp-server:latest
```

The image defaults to HTTP transport on port 3010, stateless session mode, and logs to `/var/log/fdic-banks-mcp-server`. To run it over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/fdic-banks-mcp-server:latest"]`.

## Project structure

| Directory | Purpose |
|:---|:---|
| `src/index.ts` | `createApp()` entry point: registers the tools, sets the server instructions, and starts the FDIC service and canvas bridge. |
| `src/config` | Server-specific environment variable parsing and validation with Zod. |
| `src/mcp-server/tools` | Tool definitions (`*.tool.ts`), shared input schemas, and markdown helpers. Eleven tools. |
| `src/services/fdic` | FDIC BankFind client (request pacer, response cache, query builder, normalization), the metric catalog, and the reference tables. |
| `src/services/canvas-bridge` | Adapter to the framework's DataCanvas: `df_<id>` table names, per-table TTL, and provenance. |
| `tests/` | Unit and integration tests, mirroring the `src/` structure. |

## Development guide

See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:

- Handlers throw, framework catches — no `try/catch` in tool logic
- Use `ctx.log` for logging, `ctx.state` for storage
- Register new tools in `buildToolDefinitions()` in `src/mcp-server/tools/definitions/index.ts`
- Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields

## Contributing

Issues are welcome. Run checks and tests before submitting:

```sh
bun run devcheck
bun run test
```

## License

This project is licensed under the Apache 2.0 License. See the [LICENSE](./LICENSE) file for details.