mcp-trm-colombia
# mcp-trm-colombia
**The problem:** ask any LLM what a dollar is worth in Colombian pesos and it will confidently make one up — a number from its training data, months stale, with no source.
**What this does:** gives the model the *official* rate. An MCP server over Colombia's TRM (*Tasa Representativa del Mercado*), published by the Superintendencia Financiera as open data. Every answer carries the exact rate used and the dates it is valid for, so it can be audited.
**Try it:**
```bash
uvx --from git+https://github.com/andresFLZ/mcp-trm-colombia mcp-trm-colombia
```
https://github.com/user-attachments/assets/a2e5ba43-9727-4638-a4df-f68cbd58c455
---
## Why an MCP server instead of a plain API call
Because the awkward parts are not the HTTP request. They are the rules a model
gets wrong on its own:
- **One rate covers several days.** The rate published on a Friday is normally
valid through Sunday, and holidays stretch it further. Asking "the rate on
Saturday" and getting nothing back is a bug; this returns the rate *in force*,
with its window.
- **"Today" is ambiguous.** Colombia is UTC-5 with no DST. A model reasoning in
UTC asks for tomorrow and gets "not published yet". Handled here, once.
- **Old rates never change; today's still can.** History is cached forever, the
current one never is.
- **Every number needs provenance.** A conversion that does not say which rate
it used cannot be checked by anyone.
## Tools
| Tool | What it answers |
|---|---|
| `get_trm(date?)` | The official rate in force on a date, with its validity window |
| `convert(amount, currency, date?)` | USD ⇄ COP at the rate for that date, reporting the rate used |
| `trm_series(start_date, end_date)` | Every rate in a period, plus min, max, average and the change across it |
| `today()` | Today's date *in Colombia* and today's rate |
All dates are ISO `YYYY-MM-DD`. `date` defaults to today in Bogotá.
### Errors are answers, not stack traces
A tool that throws leaves the model guessing. Every failure comes back as data,
including the one field it actually needs — whether trying again could help:
```json
{
"ok": false,
"error": "source_unavailable",
"message": "datos.gov.co did not respond in time. It is an open-data portal and it is occasionally slow; retrying usually works.",
"retryable": true
}
```
| `error` | Means | `retryable` |
|---|---|---|
| `invalid_input` | Bad date, unknown currency, negative amount | `false` |
| `not_published` | Valid date, no rate published for it | `false` |
| `source_unavailable` | datos.gov.co timed out, errored, or sent something unreadable | `true` |
## Install
Requires Python 3.10+ and the MCP Python SDK 2.x.
```bash
git clone https://github.com/andresFLZ/mcp-trm-colombia
cd mcp-trm-colombia
pip install -e ".[dev]"
```
### Use it from an MCP client
This repo ships a `.mcp.json`, so a client opened in this directory finds the
server with no configuration at all. For Claude Desktop, add this to
`claude_desktop_config.json` and restart it:
```json
{
"mcpServers": {
"trm-colombia": {
"command": "uvx",
"args": ["--from", "git+https://github.com/andresFLZ/mcp-trm-colombia", "mcp-trm-colombia"]
}
}
}
```
<details>
<summary>Running from a local clone instead</summary>
```json
{
"mcpServers": {
"trm-colombia": {
"command": "python",
"args": ["-m", "mcp_trm_colombia.server"],
"env": { "PYTHONPATH": "/absolute/path/to/mcp-trm-colombia/src" }
}
}
}
```
</details>
Then ask, in plain language:
> *"I invoiced USD 1,850 on August 15th. How many pesos was that at the official rate?"*
> *"How much did the peso move against the dollar this month?"*
## Data source
[datos.gov.co dataset `32sa-8pi3`](https://www.datos.gov.co/resource/32sa-8pi3.json) —
*Tasa de Cambio Representativa del Mercado Histórico*, published by the
Superintendencia Financiera de Colombia. Public, no API key, series starts
**1991-12-02**.
## Tests
31 tests. 28 run offline against a mock transport, so the suite is
deterministic and does not depend on a government portal being up:
```bash
pytest # 28 offline tests
pytest -m live # 3 more, against the real dataset
```
The offline set deliberately covers the failure paths, not just the happy one:
HTTP 503, timeouts, malformed bodies, rows with missing fields, dates before the
series begins, dates too far ahead, reversed ranges, negative amounts, unknown
currencies — and that history is cached while today's rate is not.
## Design notes
- **`trm.py` has no MCP import.** The domain logic is plain Python and is tested
on its own; `server.py` only adapts it. Swapping the transport touches one file.
- **`Decimal`, never `float`, for money.** Rounding happens once, at the edge.
- **The cache follows the data.** A settled past rate is immutable, so it is kept
forever. Today's can still be superseded, so it is re-read every time.
- **Errors are a taxonomy, not one catch-all.** "You asked for something
impossible" and "the source is down" need different reactions from the caller.
- **Built against MCP SDK 2.x** (`MCPServer`). The 1.x name for the same class
was `FastMCP`.
## License
MIT — see [LICENSE](LICENSE).
TDQS
Scored across 4 tools
Most tools are clearly distinct: get_trm fetches a single rate, convert performs currency conversion, and trm_series returns period statistics. There is mild overlap between get_trm and today, since both can supply today's rate, but the descriptions clarify that today is a convenience wrapper for date anchoring.
The names are readable and lowercase, but they do not follow a single pattern: get_trm uses verb_noun, convert is a bare verb, trm_series is a noun phrase, and today is a standalone time reference. This mixed style is understandable but not fully consistent.
Four tools is well-scoped for a specialized TRM server. Each tool addresses a distinct need: single rate lookup, conversion, historical series analysis, and current date context, with no redundant bulk.
The server covers the core domain completely: fetching current or historical rates, converting between USD and COP, and obtaining series summaries with statistics. The inclusion of today's date fills a practical gap, and there are no obvious missing operations for this narrow purpose.