NBG Rates
# nbg-rates-mcp
Ask Claude, or any other MCP client, for the official National Bank of Georgia (NBG) rate of the lari (GEL) on any date, or to convert an amount at that rate. `nbg-rates-mcp` runs on your machine or as a hosted connector you paste into claude.ai or ChatGPT, needs no account and no key, and encodes the rules a raw lookup gets wrong: per-unit values, Tbilisi calendar days, an explicit flag when a day has no rate of its own, and an error, never a stale rate, for a date NBG has not published yet.
**[nbg-rates.env.ge](https://nbg-rates.env.ge)**: what it does and how to add it, in English and Georgian.
This is an independent open-source project, not affiliated with the National Bank of Georgia. NBG's website, [nbg.gov.ge](https://nbg.gov.ge), is the source of truth. The NBG endpoints this server reads are undocumented and may change.
## What you can ask
- What was the USD rate on 30 September 2026?
- Convert 1,500 USD to GEL at the NBG rate of 27 September 2026.
- Give me the EUR rate for every day of September 2026 as a table.
- How many US dollars is 1 euro today, at NBG rates?
Write the date with the month as a word, "10 September 2026": 10/09/2026 reads as two different dates.
For the second question the answer is: 27 September 2026 is a Sunday, so the rate in force is the one NBG set for Saturday 26 September, 2.6080 GEL per USD, still valid on Sunday; 1,500 USD = 3,912.00 GEL, and the answer says the rate is carried over from the 26th.
In Claude Desktop the "+" menu of a chat also lists four ready-made questions from this server: today's rates, the rate on a date, converting an amount, and a rates table for a month.
## Which rate you get
Georgian accounting and tax reporting use the official NBG rate in force on the date of the transaction. This server returns exactly that rate, the one shown on nbg.gov.ge for the day you name.
Converted amounts come rounded half up to two decimals (`resultRounded`), the way a ledger posts them, computed exactly from NBG's published figures.
NBG sets rates on business days around 17:00 Tbilisi time, valid from the next calendar day. A Sunday, a Monday or the day after a public holiday has no rate of its own and keeps the last one set; the answer says so (`carriedOver`) and names the day that rate took effect. A date NBG has not published yet, including tomorrow before about 17:00 Tbilisi time, is answered with an error, never a guess.
## Install
### claude.ai, Claude mobile and ChatGPT (nothing to install)
Paste this URL as a custom connector:
```text
https://nbg-rates.env.ge/mcp
```
On claude.ai open Customize, then Connectors, then "+ Add" and "Add custom connector", paste the URL and finish with "Add" (on a Team or Enterprise plan an owner adds it under Organization settings, then Connectors); the connector then also appears in the Claude mobile apps and in Claude Desktop under the same account. In ChatGPT open chatgpt.com/plugins, select the plus button, then "Add custom MCP server", paste the URL under Connection, leave authentication off and finish with "Create as a plugin" (in a workspace, an admin may need to allow custom MCP servers first). The landing page, [nbg-rates.env.ge](https://nbg-rates.env.ge), has step-by-step instructions for each app.
This hosted copy is the latest release of this server, run by the maintainer on AWS. It needs no account and never records the amounts, dates or currencies asked. It keeps two logs for 30 days: which tool was called from which client and whether NBG answered, and the gateway's access log (time, path, status, and the caller's IP address and user agent; for claude.ai and ChatGPT the caller is Anthropic's or OpenAI's server). The logs are used only to keep the service running, to see which clients use it and to investigate errors or abuse; they are not sold, shared or used for advertising. The landing page sets no cookies and loads nothing from anyone else.
### Claude Desktop (macOS, Windows)
Download [nbg-rates-mcp-latest.mcpb](https://github.com/akalongman/nbg-rates-mcp/releases/latest/download/nbg-rates-mcp-latest.mcpb) and open it. Claude Desktop installs the extension and runs it with its own Node.js, so nothing else is needed. The same file, under its version number, is attached to every [release](https://github.com/akalongman/nbg-rates-mcp/releases).
### Claude Desktop (Linux beta) and manual configuration
Add the server to `claude_desktop_config.json`. The file may not exist yet; create it with the snippet below as its whole content, then restart Claude Desktop.
| Platform | Location of `claude_desktop_config.json` |
| -------- | ---------------------------------------- |
| macOS | `~/Library/Application Support/Claude/` |
| Windows | `%APPDATA%\Claude\` |
| Linux | `~/.config/Claude/` |
```json
{ "mcpServers": { "nbg-rates": { "command": "npx", "args": ["-y", "nbg-rates-mcp"] } } }
```
This needs Node.js 22 or later; see [Node.js](#nodejs) below.
### Claude Code
```bash
claude mcp add nbg-rates -- npx -y nbg-rates-mcp
```
To make the server available in every project, add it with user scope:
```bash
claude mcp add --scope user nbg-rates -- npx -y nbg-rates-mcp
```
### Cursor and other clients
Add the same `mcpServers` entry to the client's MCP settings. In Cursor that is `~/.cursor/mcp.json` for every project, or `.cursor/mcp.json` inside one project.
### Node.js
`npx` needs Node.js 22 or later (the Claude Desktop bundle does not). Check with `node --version`, and install it if needed:
- macOS (Homebrew):
```bash
brew install node
```
- Debian and Ubuntu (NodeSource, Node.js 24):
```bash
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt-get install -y nodejs
```
- Other Linux distributions (nvm; open a new terminal after the first line):
```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.8/install.sh | bash
nvm install --lts
```
- Windows (PowerShell):
```powershell
winget install OpenJS.NodeJS.LTS
```
- WSL: the same commands as Debian and Ubuntu, run inside the WSL shell.
The rest of this document is for developers and for people writing instructions for a model.
### Self-hosting
The same server speaks Streamable HTTP: `nbg-rates-mcp --http --port 3000` serves `http://127.0.0.1:3000/mcp` (add `--host 0.0.0.0` to listen on every interface). On AWS Lambda the handler is `dist/lambda.handler` of the npm package with its production dependencies zipped beside it, behind an API Gateway HTTP API with payload format 2.0 and the `$default` stage (a named stage prefixes every path with its name, so each request would get a 404). Set `NBG_RATES_HTTP_HOSTS` to the public host name in either case, so the endpoint answers only for it.
## Tools
Every tool is read-only. Dates are calendar days in Tbilisi in the form `YYYY-MM-DD`, and every rate is GEL per one unit of the currency. Each tool returns its result as structured content and as the same JSON in a text block.
### `nbg_get_rates`
The official NBG rates in force on one calendar date. NBG quotes some currencies per 10, 100, 1000 or 10000 units; the rate is always divided down to one unit, and the raw pair is kept as `nbgQuantity` and `nbgRate`.
Inputs:
| Name | Required | Description |
| ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `date` | no | Calendar date. Defaults to today in Tbilisi. Tomorrow's rate exists after about 17:00 Tbilisi time. |
| `currencies` | no | ISO 4217 codes to return, case-insensitive, duplicates ignored, for example `["USD", "EUR"]`. When given, the list holds at least one entry. Omit for all currencies. |
| `language` | no | Language of the currency names: `en` (default) or `ka` (Georgian). Codes, numbers and dates are the same in both. |
Output:
- `requestedDate`: the date asked for, or today in Tbilisi when `date` is omitted.
- `effectiveDate`: the date the returned rates took effect, as NBG publishes it.
- `carriedOver`: `true` when the rates in force on `requestedDate` took effect on an earlier day. They are still the official rates for `requestedDate`; quote `effectiveDate` alongside them.
- `rates`: one entry per currency with `code`, `name`, `rate` (GEL per one unit), `diff` (change versus the previous published rate, per one unit), `nbgQuantity` (the units NBG quotes the raw rate for: 1, 10, 100, 1000 or 10000) and `nbgRate` (the raw rate as NBG publishes it, for `nbgQuantity` units). Values below 0.000001, mostly the `diff` of a currency quoted per 1000 or 10000 units, are JSON numbers in exponent notation, for example `4e-7`.
- `unknownCodes`: requested codes NBG did not quote on that date, and inputs that are not a three-letter currency code at all. The other requested codes are still answered. `GEL` is listed here too: every rate is in GEL, so the table has no row for it.
Worked example: on Monday 2026-10-05 a user asks for the AMD rate on Sunday 2026-10-04. NBG sets no rate for a Sunday, so the rate in force is the one valid from Saturday 2026-10-03, which NBG quotes as 7.1762 GEL per 1000 drams. The request
```json
{ "date": "2026-10-04", "currencies": ["AMD"] }
```
returns
```json
{
"requestedDate": "2026-10-04",
"effectiveDate": "2026-10-03",
"carriedOver": true,
"rates": [
{
"code": "AMD",
"name": "Armenian Dram",
"rate": 0.0071762,
"diff": 0.000012,
"nbgQuantity": 1000,
"nbgRate": 7.1762
}
],
"unknownCodes": []
}
```
The same request for Monday 2026-10-05 returns the same rate with `requestedDate` 2026-10-05: the rate NBG set on Friday for Saturday is still in force on Monday.
### `nbg_convert`
Converts an amount between two currencies with the official NBG rate in force on a calendar date. Either side may be GEL; a pair without GEL is converted through GEL.
Inputs:
| Name | Required | Description |
| -------- | -------- | ------------------------------------------------------------------------- |
| `amount` | yes | Amount in the `from` currency. A finite number, may be negative. |
| `from` | yes | ISO 4217 code, or `GEL`. |
| `to` | yes | ISO 4217 code, or `GEL`. |
| `date` | no | Calendar date whose official rate to apply. Defaults to today in Tbilisi. |
Output:
- `amount`, `from`, `to`: the request, with the codes in upper case.
- `result`: the converted amount, unrounded, for further calculation.
- `resultRounded`: the exact amount rounded half up to two decimals, computed from NBG's published figures; the amount to show. JSON drops trailing zeros, so 3.90 arrives as `3.9`.
- `rate`: one unit of `from` expressed in `to`.
- `via`: `direct` when either side is GEL (or both codes are the same), `GEL` when a pair without GEL is converted through GEL.
- `requestedDate`, `effectiveDate`, `carriedOver`: as in `nbg_get_rates`.
GEL to GEL is the identity and needs no NBG rate, so it answers for any valid date. A code NBG did not quote on the date is an error.
### `nbg_convert_many`
Converts a list of amounts, each at the official NBG rate in force on its own date: one call for a list of invoices. Items are answered independently, so a bad date or code fails only its own item. The server makes at most one NBG request per distinct date, one date after another; once NBG stops answering, the dates not yet asked fail with the same error without a request.
Inputs:
| Name | Required | Description |
| ------- | -------- | ---------------------------------------------------------------------------------------- |
| `items` | yes | 1 to 100 entries of `{ amount, from, to, date? }`, each with the rules of `nbg_convert`. |
An empty list, more than 100 items, or an item without the shape `{ amount, from, to, date? }` (a missing or non-numeric amount, a missing code, or a text over 64 characters) fails the whole call at the input schema; every other problem fails only its item.
Output:
- `items`: one entry per input, in input order. A converted item is the `nbg_convert` output, with `resultRounded`. A failed item is `{ amount, from, to, date, error }`: the item as given, without `date` when it had none, and `error`, the message that says what went wrong (see [Errors](#errors)).
- `converted`, `skipped`: how many items were converted and how many failed.
- `total`: `{ to, resultRounded }`, present only when at least one item was converted, every converted item has the same target currency, and the sum is a finite number: the sum of their `resultRounded` values, computed exactly, the way a ledger sums posted lines. It covers the converted items only, so it is partial when `skipped` is not 0.
Worked example: 10 USD and 1.5 USD to GEL on 2026-10-10, and 100 USD to GEL on 2026-13-01, a date that does not exist. The request
```json
{
"items": [
{ "amount": 10, "from": "USD", "to": "GEL", "date": "2026-10-10" },
{ "amount": 1.5, "from": "USD", "to": "GEL", "date": "2026-10-10" },
{ "amount": 100, "from": "USD", "to": "GEL", "date": "2026-13-01" }
]
}
```
returns
```json
{
"items": [
{
"amount": 10,
"from": "USD",
"to": "GEL",
"result": 26.005,
"resultRounded": 26.01,
"rate": 2.6005,
"via": "direct",
"requestedDate": "2026-10-10",
"effectiveDate": "2026-10-10",
"carriedOver": false
},
{
"amount": 1.5,
"from": "USD",
"to": "GEL",
"result": 3.9007499999999995,
"resultRounded": 3.9,
"rate": 2.6005,
"via": "direct",
"requestedDate": "2026-10-10",
"effectiveDate": "2026-10-10",
"carriedOver": false
},
{
"amount": 100,
"from": "USD",
"to": "GEL",
"date": "2026-13-01",
"error": "2026-13-01 is not a valid calendar date (not a real calendar date); use YYYY-MM-DD."
}
],
"converted": 2,
"skipped": 1,
"total": { "to": "GEL", "resultRounded": 29.91 }
}
```
The total, 26.01 + 3.9 = 29.91 GEL, covers the two converted items; `skipped` is 1, so it is partial.
### `nbg_list_currencies`
Lists every currency NBG publishes a GEL rate for today. NBG quoted other currencies in the past, so a code missing here may still have historical rates.
Inputs:
| Name | Required | Description |
| ---------- | -------- | ------------------------------------------------------------------ |
| `language` | no | Language of the currency names: `en` (default) or `ka` (Georgian). |
Output:
- `effectiveDate`: the date today's rates took effect.
- `currencies`: one entry per currency with `code`, `name` and `nbgQuantity` (the units NBG quotes the raw rate for).
### `nbg_rate_history`
The official rate of one currency, in GEL per one unit, in force on every calendar day of an inclusive range. One call makes one request to NBG.
Inputs:
| Name | Required | Description |
| ---------- | -------- | ---------------------------------------------------------------- |
| `currency` | yes | ISO 4217 code, for example `USD`. |
| `from` | yes | First calendar day, inclusive. |
| `to` | yes | Last calendar day, inclusive. At most 366 days including `from`. |
Output:
- `currency`, `from`, `to`: the request, with the code in upper case.
- `days`: one entry per calendar day. A day whose own rate is in force is `{ date, rate }`, with `rate` in GEL per one unit. A carried-over day adds `effectiveDate`, the day its rate took effect, and `carriedOver: true`; an absent `effectiveDate` means the rate took effect on `date` itself. A full year stays near 6,000 tokens.
- `summary`: `first`, `last`, `change` (last minus first), `min` and `max` with `minDate` and `maxDate` (first occurrence), and `average`, the arithmetic mean over every calendar day of the range, carried-over days included. `change` and `average` are exact, rounded half up to the decimals NBG's quantity gives the currency, 4 + log10(`nbgQuantity`), the largest over the days (4 for USD, 7 for AMD).
For example, USD from Friday 2026-10-02 to Tuesday 2026-10-06 (Saturday has its own rate; Sunday and Monday carry it):
```json
{
"currency": "USD",
"from": "2026-10-02",
"to": "2026-10-06",
"days": [
{ "date": "2026-10-02", "rate": 2.6042 },
{ "date": "2026-10-03", "rate": 2.6039 },
{ "date": "2026-10-04", "rate": 2.6039, "effectiveDate": "2026-10-03", "carriedOver": true },
{ "date": "2026-10-05", "rate": 2.6039, "effectiveDate": "2026-10-03", "carriedOver": true },
{ "date": "2026-10-06", "rate": 2.6032 }
],
"summary": {
"first": 2.6042,
"last": 2.6032,
"change": -0.001,
"min": 2.6032,
"minDate": "2026-10-06",
"max": 2.6042,
"maxDate": "2026-10-02",
"average": 2.6038
}
}
```
A range that reaches a date NBG has not published yet is an error, as is a day in the range for which NBG's export has no rate of the currency (for example before NBG first quoted it, or after NBG stopped).
### Resource `nbg://rates/{date}`
The full official NBG rate table in force on a calendar date (`YYYY-MM-DD`) or on `today`, as JSON with the same fields as the `nbg_get_rates` output and English currency names. `nbg://rates/today` is listed among the server's resources.
### Errors
A tool that cannot answer returns an error result whose text says what went wrong and what to do next:
- the date is not a real calendar date in the form `YYYY-MM-DD`, or a history range ends before it starts;
- a history range is longer than 366 days;
- the currency is not a three-letter code (the message names USD, EUR and GEL as examples), or NBG did not quote it on the requested date or range (`nbg_get_rates` lists such codes in `unknownCodes` instead); NBG has added and dropped currencies over the years, so the code may still have rates on other dates;
- a conversion result is too large to represent as a number;
- NBG has no rate in force on the date (the archive starts on 1995-10-14, and a currency has no rate before NBG first quoted it, after NBG stopped quoting it, or where NBG's records have a gap);
- NBG has not published a rate for the date yet; the message names today in Tbilisi, says that tomorrow is the furthest date that can have a rate and only once NBG has published it, and, when known, names the date from which the latest published rate is valid. A history range that reaches such a date fails as a whole: end it at today;
- NBG did not respond usably; the request can be retried;
- the NBG response did not have the expected shape, which means the NBG endpoint may have changed; please [open an issue](https://github.com/akalongman/nbg-rates-mcp/issues) with the package version.
Reading the resource fails with the same messages.
## Date rules
- NBG sets a rate on business days around 17:00 Tbilisi time, valid from the next calendar day until the next rate. Once it is published, tomorrow's rate can be requested by date.
- "Today" means the calendar date in Tbilisi, wherever the client runs.
- Sundays, Mondays and days after a public holiday have no rate of their own: they carry the earlier rate with `carriedOver: true` and `effectiveDate` set to the day it took effect. Before September 2021 NBG stored a rate for every calendar day, so older Sundays are not carried over.
- A date whose rate NBG has not published yet (tomorrow before about 17:00 Tbilisi time, or any later date) returns an error, never a guess.
- NBG occasionally publishes a table for a day after that day: on Monday 2026-09-28 at 17:01 it published the table valid from Saturday 2026-09-26, which replaced Friday's rate for 2026-09-26 to 2026-09-28. A carried-over answer for the last seven days can therefore change; the server asks NBG again after ten minutes for those, and after twelve hours for every other answer.
- The archive starts on 1995-10-14 with USD; other currencies start later.
## Environment variables
| Variable | Effect |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NBG_RATES_BASE_URL` | The NBG host to call. Defaults to `https://nbg.gov.ge`; a trailing slash is removed. Useful for testing. |
| `NBG_RATES_DEBUG` | Set to `1` to log every upstream request to stderr. Include this log in bug reports. |
| `NBG_RATES_HTTP_HOSTS` | Host names the HTTP endpoint (`--http`, Lambda) answers for, comma separated. Unset or empty: any host. |
| `NBG_RATES_OPENAI_CHALLENGE` | The token OpenAI's domain verification expects at `/.well-known/openai-apps-challenge`, served as plain text. Unset or empty: that path is a 404. |
Set them in the client's configuration, for example:
```json
{
"mcpServers": {
"nbg-rates": { "command": "npx", "args": ["-y", "nbg-rates-mcp"], "env": { "NBG_RATES_DEBUG": "1" } }
}
}
```
## Development
```bash
npm install
npm test # unit tests
npm run test:e2e # builds, then drives the server over stdio against recorded NBG responses
npm run test:contract # live: checks the assumptions against nbg.gov.ge
npm run record-fixtures # refreshes test/fixtures from nbg.gov.ge; review the diff before committing
```
Before a pull request, also run `npm run format:check`, `npm run lint` and `npm run typecheck`.
The code is split into `src/core`, pure functions with no I/O (date rules, parsing, per-unit normalisation, conversion, history), and `src/shell`, which holds the effects (the NBG HTTP client, the cache, the rates service and the MCP server). `src/bin.ts` is the executable.
The repository's rulesets (protection of `main`, and release tags that only admins may create) are defined in `.github/rulesets/` and applied with `scripts/apply-rulesets.sh`; its description, homepage and topics are defined in `.github/repository.json` and applied with `scripts/apply-repo-settings.sh`. Both are run by a maintainer after a change, never edited in the GitHub UI.
## License
MIT, see [LICENSE](LICENSE).
TDQS
Scored across 5 tools
Each tool has a clearly distinct purpose: single-date multi-currency rates (nbg_get_rates) vs single-currency date range (nbg_rate_history) vs single conversion (nbg_convert) vs batch conversion (nbg_convert_many) vs currency listing (nbg_list_currencies). The single/batch and single-date/range distinctions are spelled out in the descriptions, leaving little room for misselection.
All names share a consistent nbg_ prefix and snake_case, and four of five are verb-first (get_rates, convert, convert_many, list_currencies). nbg_rate_history breaks the verb-first convention by leading with a noun, a minor deviation from an otherwise predictable pattern.
Five tools is well-scoped for an exchange-rate service, covering current rates, historical ranges, single conversion, batch conversion, and currency enumeration. Each tool earns its place with no redundant or filler operations.
The surface covers the full lifecycle of the domain: discovering available currencies, fetching rates for a date or a range, and converting both single and batch amounts. No obvious gaps or dead ends for rate lookup and conversion workflows.