boi-mcp
<div align="center">
<img src=".github/banner.svg" alt="boi-mcp" width="100%"/>
[](https://github.com/Shtibel1/boi-mcp/actions/workflows/ci.yml)
[](package.json)
[](LICENSE)

[](https://modelcontextprotocol.io)

**Official Bank of Israel exchange rates (שערים יציגים) and interest rate for any MCP client.**
</div>
<p align="center"><a href="#quick-start">Quick start</a> · <a href="#tools">Tools</a> · <a href="#live-example">Live example</a> · <a href="#architecture">Architecture</a> · <a href="#roadmap">Roadmap</a></p>
A [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI agents the **official representative exchange rates** published daily by the Bank of Israel, the rate on any past date (with automatic previous-business-day fallback), history and statistics over a range, ILS conversions and cross rates, and the **Bank of Israel policy interest rate** with its change history.
It talks directly to the Bank of Israel's free, public [SDMX REST API](https://www.boi.org.il/en/economic-roles/statistics/) at `edge.boi.gov.il`: no API key, no scraping, no third-party FX feed.
Part of a small family of Israeli MCP servers: [hebrew-il-mcp](https://github.com/Shtibel1/hebrew-il-mcp) (local validators: ID, phone, VAT, IBAN, plates, holidays) and [il-opendata-mcp](https://github.com/Shtibel1/il-opendata-mcp) (data.gov.il datasets).
## Tools
| Tool | What it does |
| --- | --- |
| `list_currencies` | No network. The 14 currencies the BOI publishes a representative rate for (USD, EUR, GBP, JPY, CHF, CAD, AUD, DKK, NOK, SEK, ZAR, JOD, EGP, LBP), with English and Hebrew names and the quoting unit. |
| `get_latest_rates` | Latest published rate for all currencies (or a `currencies` subset) in **one** SDMX request (`lastNObservations=1`). Each entry has `rate` as published, `unit`, `ils_per_unit` and its own `date`. |
| `get_rate` | Rate for one `currency` on a `date` (default: today in Israel). Weekends and holidays fall back to the previous business day, flagged with `is_fallback` and `days_back`. Future dates are rejected. |
| `get_rate_history` | Daily rates between `start_date` and `end_date` (≤ ~10 years). Returns `count`, `first`/`last`, `min`/`max` with dates, `change` and `change_pct`, plus the observations (the most recent 400 if longer; stats always cover the whole range). |
| `convert_currency` | Convert `amount` between ILS (also `NIS`, `₪`, `שקל`) and any supported currency, or between two foreign currencies via ILS, on the latest or a historical `date`. Returns the result, the cross rate and the rate date used for each leg. |
| `get_interest_rate` | The BOI policy rate (ריבית בנק ישראל) on a `date`, `effective_since` and every change in the look-back window (default 2 years, or `since`) with the previous rate and the change in basis points. |
Every tool returns both human-readable JSON text and `structuredContent`. Bad input (unknown currency, malformed or future date, empty range) produces an MCP `isError` result with a clear message instead of crashing the session.
Currency inputs are forgiving: `usd`, `$`, `€`, `£`, `¥`, `דולר`, `יורו` all work.
### Details that are easy to get wrong
- **Quoting units.** The BOI quotes JPY per **100** yen and LBP per **10** pounds (`UNIT_MULT` in SDMX). `rate` is kept exactly as published and `ils_per_unit` is normalized, so conversions are always right.
- **"Today" is Israeli time.** Dates default to `Asia/Jerusalem`, not UTC, so a request at 01:00 in Tel Aviv asks for the right day.
- **No data ≠ error.** The API answers HTML `404` when a query matches nothing (e.g. a Saturday). The client maps that to "no observations" and the tools fall back or explain.
## Quick start
### Claude Desktop / Cursor / any MCP client
```json
{
"mcpServers": {
"boi": {
"command": "npx",
"args": ["-y", "github:Shtibel1/boi-mcp"]
}
}
}
```
Claude Desktop: put this in `claude_desktop_config.json`. Cursor: `~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project).
Or from a local clone:
```bash
npm install && npm run build
node dist/cli.js # speaks MCP over stdio
```
Then ask your agent things like:
- "What is the dollar's representative rate today, and how did it move this month?"
- "Convert 2,500 EUR to shekels at the rate of 2026-10-04."
- "מה ריבית בנק ישראל ומתי היא השתנתה לאחרונה?"
### Hosted: Streamable HTTP
```bash
npx -y github:Shtibel1/boi-mcp --http --port 3000
# → boi-mcp listening on http://127.0.0.1:3000/mcp
curl http://127.0.0.1:3000/healthz # {"status":"ok","name":"boi-mcp","version":"0.1.0"}
```
```json
{ "mcpServers": { "boi": { "url": "http://127.0.0.1:3000/mcp" } } }
```
| Flag | Env | Default | Notes |
| --- | --- | --- | --- |
| `--http` | | off | Serve [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) instead of stdio |
| `--port` | `PORT` | `3000` | |
| `--host` | `HOST` | `127.0.0.1` | Binds to loopback by default; use `0.0.0.0` inside a container |
| `--allowed-origin` | `ALLOWED_ORIGINS` | none | Repeatable flag, or a comma-separated env var. Browser requests from any other `Origin` get `403` (DNS-rebinding protection); non-browser clients without `Origin` are always allowed |
| `--version` | | | Print the version and exit |
The HTTP mode is **stateless**: every `POST /mcp` gets a fresh server and transport, there are no session ids, and `GET`/`DELETE /mcp` return `405`, so instances can sit behind any load balancer. Bodies over 1 MB get `413`, malformed JSON gets `400`.
### Docker
The image runs the HTTP mode as the unprivileged `node` user on `0.0.0.0:3000` with a `HEALTHCHECK` against `/healthz`:
```bash
docker build -t boi-mcp .
docker run --rm -p 3000:3000 -e ALLOWED_ORIGINS=https://app.example boi-mcp
```
CI builds the image on every push and smoke-tests it: health check, a real `tools/list` call, the origin guard and the non-root user.
### As a library
```ts
import { BoiClient } from "boi-mcp";
const boi = new BoiClient();
const { rates } = await boi.getLatestRates(["USD", "EUR"]);
const onSunday = await boi.getRate("USD", "2026-10-04"); // → Friday 2026-10-02, is_fallback: true
const history = await boi.getRateHistory("EUR", "2026-10-01", "2026-11-14");
const { result } = await boi.convert({ amount: 100, from: "EUR", to: "JPY" });
const policy = await boi.getInterestRate();
```
## Live example
Real output from the built server against the live Bank of Israel API, run on **2026-11-15** (Sunday, Asia/Jerusalem — the latest rates are Friday's):
| Call | Result |
| --- | --- |
| `get_latest_rates` | as of **2026-11-13**: USD **3.199**, EUR **3.5394**, GBP **4.2365**, JPY **2.0009** per 100 (0.020009 per yen) |
| `get_rate { currency: "usd", date: "2026-11-14" }` | 3.199 from **2026-11-13**, `is_fallback: true`, `days_back: 1` (Saturday) |
| `get_rate_history { EUR, 2026-10-01 → 2026-11-14 }` | 31 business days, 3.4826 → 3.5394 (**+1.63%**), min 3.4291 on 2026-10-09, max 3.5724 on 2026-11-06 |
| `convert_currency { 1000 USD → ILS }` | **3,199 ILS** |
| `convert_currency { 100 EUR → JPY }` | **17,689.0399 JPY** (cross rate 176.8904 via ILS) |
| `get_interest_rate` | **3.25%** since 2026-09-03; five −25 bp cuts since 4.5% (2025-11-27, 2026-01-08, 2026-05-28, 2026-07-09, 2026-09-03) |
<details>
<summary>Raw <code>get_rate</code> response</summary>
```json
{
"currency": "USD",
"name": "US Dollar",
"name_he": "דולר ארה\"ב",
"rate": 3.199,
"unit": 1,
"ils_per_unit": 3.199,
"date": "2026-11-13",
"requested_date": "2026-11-14",
"is_fallback": true,
"days_back": 1,
"source": "Bank of Israel SDMX API (edge.boi.gov.il)"
}
```
</details>
## Architecture
```mermaid
flowchart LR
C["MCP client<br/>(Claude Desktop, Cursor, agents)"] -- stdio --> CLI[cli.ts]
R["Remote agents"] -- "POST /mcp" --> H["http.ts<br/>stateless Streamable HTTP<br/>origin check · /healthz"]
CLI --> S["server.ts<br/>zod schemas · isError results"]
CLI -- "--http" --> H
H -- "new server per request" --> S
S --> BOI
subgraph L["src/lib — no MCP imports"]
BOI["boi.ts<br/>injectable fetch · SDMX client<br/>fallback · stats · conversion"]
CSV["csv.ts<br/>RFC 4180 parser"]
CUR["currencies.ts<br/>14 currencies · units · aliases"]
DT["dates.ts<br/>Asia/Jerusalem today"]
BOI --> CSV
BOI --> CUR
BOI --> DT
end
BOI -- "HTTPS · format=csv" --> API["edge.boi.gov.il<br/>SDMX v2: EXR · BR"]
APP["Your TS app"] -- import --> L
```
### How the data is fetched
| Need | SDMX query (dataflow `BOI.STATISTICS`) |
| --- | --- |
| Latest rates | `EXR/1.0/RER_USD_ILS+RER_EUR_ILS+…?lastNObservations=1` |
| Rate on a date (with fallback) | `EXR/1.0/RER_USD_ILS?endperiod=2026-10-04&lastNObservations=1` |
| History | `EXR/1.0/RER_EUR_ILS?startperiod=…&endperiod=…` |
| Policy interest rate | `BR/1.0/MNT_RIB_BOI_D?startperiod=…&endperiod=…` |
All with `format=csv`. The fallback is done by the API itself (`endperiod` + `lastNObservations=1`), so a holiday week never needs a retry loop.
### Layout
```
src/
lib/boi.ts injectable-fetch SDMX client: rates, fallback, history stats, conversion, policy rate
lib/csv.ts small RFC 4180 CSV parser (quoted Hebrew comments, doubled quotes, CRLF, BOM)
lib/currencies.ts supported currencies, quoting units, Hebrew names and aliases
lib/dates.ts Asia/Jerusalem "today", strict YYYY-MM-DD validation
server.ts MCP tool wiring (zod schemas)
http.ts stateless Streamable HTTP transport (node:http, no framework)
cli.ts entry: stdio by default, --http for hosted mode
test/
fixtures/ real BOI API responses captured on 2026-11-15 (see fixtures/README.md)
*.test.ts 55 vitest tests; fetch is mocked with the fixtures, so CI never hits the network
```
## Development
```bash
npm test # vitest, replaying recorded BOI responses (no network)
npm run typecheck
npm run build
```
## Attribution
This is an unofficial client. Exchange rates and interest rates come from the [Bank of Israel](https://www.boi.org.il) and are subject to its terms of use. Representative rates are reference rates, not dealing quotes. This project has no affiliation with the Bank of Israel.
## Roadmap
- [ ] Publish to npm so `npx boi-mcp` works without the GitHub shorthand
- [ ] Effective exchange-rate indices (nominal/real basket) and USD/ILS implied volatility from the same EXR dataflow
- [ ] CPI and inflation expectations series
- [ ] Small in-memory cache for the HTTP mode (rates change once per business day)
- [ ] Render / Fly one-click deploy
## Contributing
Issues and PRs are welcome. Keep API logic in `src/lib` with unit tests and add an end-to-end case in `test/server.test.ts` for new tools. New fixtures must be real captured responses — never hand-written API data.
## License
[MIT](LICENSE) © Nadav Shtibel
TDQS
Scored across 6 tools
Each tool has a fairly distinct purpose, but get_rate, get_latest_rates, and get_rate_history all deal with representative rates and could be confused at a glance (single-date vs latest snapshot vs range). The descriptions do a good job of disambiguating them, so misselection risk is low but not zero.
All names follow a clean verb_noun pattern (list_currencies, convert_currency, get_rate, get_rate_history, get_latest_rates, get_interest_rate) with consistent snake_case and the get_ prefix for retrieval. The list_ vs get_ distinction is used meaningfully.
Six tools is well-scoped for a central-bank exchange-rate/interest-rate server, with no redundant or filler tools. Every tool maps to a clear data need (enumerate, convert, current, single-date, history, policy rate).
Coverage is strong: currency enumeration, conversion (including cross rates and historical dates), latest/single/historical rates, and policy interest rate with change history. Minor gaps exist, such as no multi-currency snapshot for an arbitrary historical date or batch conversion, but core workflows are fully served.