ifsc-mcp
# ifsc-mcp
An MCP server that gives an LLM accurate Indian bank branch data: look up a branch by
IFSC, validate a user-typed IFSC offline, find branches by bank/city/state, and map bank
names to their 4-letter codes.
## The problem
Every Indian payment integration needs IFSC data, and every team solves it badly. The
usual options are a stale CSV someone committed in 2021, a paid API with a card on file,
or scraping a bank website. Meanwhile the LLM in front of it hallucinates: it invents
IFSC codes, guesses that a bank supports UPI, and returns branch addresses that were
renamed years ago. IFSCs are 11 characters and unforgiving — one wrong character means a
payment sits in a NEAF account and nobody knows why.
This server wraps the razorpay.com IFSC API and the published razorpay/ifsc dataset, so
the model asks a tool instead of guessing. Invalid input is rejected locally before a
request is made, and every answer carries the branch's real address, MICR and
settlement-rail support.
## Install
Not on PyPI yet — install from source or run straight from the repo:
```bash
git clone https://github.com/uttkarsh-26/ifsc-mcp.git
cd ifsc-mcp
uv venv .venv && uv pip install -e ".[dev]"
```
Or without cloning, into any Python environment:
```bash
pip install "git+https://github.com/uttkarsh-26/ifsc-mcp"
```
## MCP client config
Ready to paste into Claude Desktop, Cursor, VS Code or any MCP client:
```json
{
"mcpServers": {
"ifsc": {
"command": "ifsc-mcp",
"transport": "stdio"
}
}
}
```
Via `uvx`, no install needed:
```json
{
"mcpServers": {
"ifsc": {
"command": "uvx",
"args": ["--from", "git+https://github.com/uttkarsh-26/ifsc-mcp", "ifsc-mcp"]
}
}
}
```
Streamable HTTP instead of stdio:
```bash
ifsc-mcp --transport http --host 127.0.0.1 --port 8000 --path /mcp
```
```json
{
"mcpServers": {
"ifsc": { "type": "http", "url": "http://127.0.0.1:8000/mcp" }
}
}
```
## Tools
| Tool | Purpose | Key parameters | Notes |
| --- | --- | --- | --- |
| `ifsc_lookup` | Full details for one branch | `ifsc` | The only tool that confirms a branch exists. Returns address, MICR, SWIFT, contact, `supports` (IMPS/NEFT/RTGS/UPI/SWIFT). |
| `ifsc_validate` | Offline format check | `ifsc` | No network. 4 letters + `0` + 6 alphanumerics. Returns a stable `reason` code. |
| `search_branches` | Find branches by bank/city/state/branch | `bank_code`, `city`, `state`, `branch`, `limit`, `offset` | Filters are ANDed; at least one is required. Paged, with `count` and `has_next`. |
| `branch_places` | Discover valid filter spellings | `bank_code`, `state`, `district`, `limit` | Walks states → districts → branches. Use when a search returns nothing. |
| `bank_directory` | Bank name → 4-letter code | `query`, `limit` | 1511+ banks, cached. Returns `{code, name}` pairs. |
| `bank_metadata` | Bank type and rail support | `bank_code` | Private/public/foreign/SFB/payments/co-op, plus UPI, ACH, NACH, APBS flags. |
Every result carries `ok: true`. Every failure returns `ok: false` with a stable `code`:
`INVALID_INPUT`, `NOT_FOUND`, `UPSTREAM_TIMEOUT`, `UPSTREAM_UNAVAILABLE`,
`UPSTREAM_ERROR`, `BAD_UPSTREAM_RESPONSE`, `UNKNOWN` — never a traceback.
## Configuration
All optional, all read from the environment:
| Variable | Default | Purpose |
| --- | --- | --- |
| `IFSC_API_BASE_URL` | `https://ifsc.razorpay.com` | Upstream API host. |
| `IFSC_DATASET_BASE_URL` | `https://raw.githubusercontent.com/razorpay/ifsc/master/src` | Bank dataset host. |
| `IFSC_REQUEST_TIMEOUT` | `10` | Read timeout, seconds. |
| `IFSC_CONNECT_TIMEOUT` | `5` | Connect timeout, seconds. |
| `IFSC_MAX_RETRIES` | `2` | Retries on timeout/429/5xx. |
| `IFSC_DATASET_TTL` | `3600` | Bank-directory cache TTL, seconds. |
| `IFSC_LOG_LEVEL` | `INFO` | `DEBUG`/`INFO`/`WARNING`/`ERROR`. Logs go to stderr. |
| `IFSC_MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
| `IFSC_MCP_HOST` / `IFSC_MCP_PORT` / `IFSC_MCP_PATH` | `127.0.0.1` / `8000` / `/mcp` | HTTP binding. |
## Data sources
| Source | Used for | Verified |
| --- | --- | --- |
| `ifsc.razorpay.com/{IFSC}` | Single-IFSC lookup | 200 + JSON |
| `ifsc.razorpay.com/search` | Branch search by bankcode/city/state/branch | 200 + JSON, 400 on bad limit |
| `ifsc.razorpay.com/places` | State/district/branch name discovery | 200 + JSON, 400 with no bankcode |
| `raw.githubusercontent.com/razorpay/ifsc` `src/banknames.json` | Bank code → name (1511 entries) | 200 + JSON |
| `raw.githubusercontent.com/razorpay/ifsc` `src/banks.json` | Bank type, UPI/NACH/ACH, MICR, IIN | 200 + JSON |
## Evals
The repo scores its own tool descriptions. `evals/golden.json` holds 32
natural-language queries with the expected tool and expected arguments, spread
across all six tools. Two scorers run against it:
- `router` — a deterministic, LLM-free keyword router. No network, no API key,
same answer every run. This is the number that matters: it measures whether the
descriptions make the tool boundaries legible.
- `live` — an optional model runner using any OpenAI-compatible endpoint from
`OPENAI_API_KEY` / `OPENAI_BASE_URL`. Skipped cleanly when unset.
```bash
# deterministic baseline, no network and no API key
.venv/bin/python -m evals.run --scorer router
# optional model-backed run
OPENAI_API_KEY=... OPENAI_BASE_URL=... .venv/bin/python -m evals.run --scorer live
```
Measured, on the committed golden set:
| Scorer | Model | Passed | Total | Pass rate | Threshold | Verdict |
| --- | --- | --- | --- | --- | --- | --- |
| `router` (offline baseline) | none — keyword rules | 32 | 32 | **100.0%** | 85% | PASS |
| `live` | `deepseek-v4.1-flash` | 31–32 | 32 | **96.9–100%** | 90% | PASS |
Read those numbers honestly. The 32-case golden set is a smoke gate, not a
benchmark, and the keyword baseline was tuned against this same set — it is a
floor for "are the tool boundaries legible at all", not a generalisation claim.
The `live` figure is the interesting one, and it is not stable: two consecutive
runs scored 32/32 and 31/32. The one flaky case (`search-05`) is a real finding,
not noise — given "find the Axis Bank branch named PALAKKAD KERALA", the model
sometimes drops `bank_code` because the bank name is buried inside the branch
name. Fixing that is on the roadmap below.
Results are written to `evals/results/results.json` (machine-readable) and
`evals/results/results.md` (human table, including the per-case routing
confusion list). The runner exits non-zero below the threshold in
`evals/config.json`, so a regression fails CI instead of quietly being a number
nobody reads.
See **[EVALS.md](EVALS.md)** for the scoring rules, the per-tool breakdown, the
routing-confusion list and what the eval caught in the tool design.
## Verification
Everything in this README was executed, not assumed.
```bash
.venv/bin/pytest # 131 unit tests, hermetic and offline
.venv/bin/pytest -m live # 6 live upstream smoke tests
.venv/bin/python scripts/verify_e2e.py # real MCP client over stdio -> VERIFICATION.md
.venv/bin/python scripts/verify_http.py # real MCP client over streamable HTTP
```
**[VERIFICATION.md](VERIFICATION.md)** holds a raw transcript: a real
`mcp.Client` subprocess session performing a genuine handshake, `tools/list` and
nine `tools/call` round trips against the live API.
## Development
```bash
uv pip install -e ".[dev]"
.venv/bin/ruff check . && .venv/bin/ruff format --check .
.venv/bin/pytest # unit tests (live ones deselected)
.venv/bin/pytest -m live # real network smoke tests
```
After editing the golden cases in `evals/golden.py`, regenerate the JSON that
the runner and the tests read:
```bash
.venv/bin/python -c "
import json
from evals.golden import _CASES, MIN_CASES
with open('evals/golden.json','w') as f:
json.dump({'version':1,'min_cases':MIN_CASES,'cases':list(_CASES)}, f, indent=2); f.write('\n')
"
```
## Roadmap
- Make `search_branches` infer `bank_code` from the branch string (or allow it to
be omitted when `branch` is present) — the one case the live eval misses.
- Bundle a pinned snapshot of the bank dataset so `bank_directory` works offline.
- A `nefc_advice` tool: given account number + IFSC, compute the NEFC limit and the
`a/` and `a@` sublet categories RBI assigns.
- Search by pincode, which RBI publishes but the current API does not expose.
- Prompt caching for `bank_directory` output to cut repeat token cost.
## License
MIT — see [LICENSE](LICENSE).
Upstream data is provided by [razorpay/ifsc](https://github.com/razorpay/ifsc) (MIT) and
the razorpay.com IFSC API. This project is not affiliated with Razorpay or the RBI.
TDQS
Scored across 6 tools
Each tool has a clearly distinct purpose: offline validation vs. online lookup, branch search vs. place-name discovery, bank-name-to-code mapping vs. bank-level capabilities. The descriptions explicitly guide when to prefer one over another, eliminating overlap. No two tools appear to do the same thing.
Names mix conventions: ifsc_lookup and ifsc_validate use entity_action, search_branches uses verb_entity, and branch_places, bank_directory, bank_metadata use entity_noun. While all are snake_case and readable, there is no single predictable pattern. This makes the set feel slightly inconsistent despite clear individual names.
Six tools is well-scoped for an IFSC/bank lookup service. Each tool serves a distinct, non-redundant role, covering validation, lookup, search, discovery, directory, and metadata. No tool feels extraneous or missing at this count.
The surface covers the full read-only lifecycle: offline validation, online branch resolution, filtered search with pagination, place-name discovery to aid search, bank name-to-code mapping, and bank-level capabilities. No obvious dead ends remain for typical IFSC lookups. Minor gaps like reverse MICR lookup are outside the stated domain.