Skip to main content
Glama
uttkarsh-26

ifsc-mcp

by uttkarsh-26
README.md
# 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

A4.4/5.0

Scored across 6 tools

Disambiguation5/5

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.

Naming Consistency3/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues