
Wraps the ECB's exchange rate data via the Frankfurter API with seven tools covering currency lookup, spot rates, historical conversions, and time series queries. You get point-in-time FX rates back to 1999, automatic cross-rate triangulation through EUR, and weekend/holiday date snapping with clear flags when the API returns a prior business day. Long time series (over 90 days) spill into a DuckDB-backed DataCanvas workspace where you can run SQL aggregations and window functions. Built on mcp-ts-core, runs over stdio or streamable HTTP, and needs no API keys. Useful when you need reliable historical currency data or want to analyze multi-year exchange rate trends without managing your own dataset.
Convert currencies, get FX rates, and query historical ECB exchange rate data via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://exchange-rates.caseyjhand.com/mcp
ECB reference exchange rates via Frankfurter — a keyless proxy covering ~30 currencies back to 1999-01-04. Convert amounts, disambiguate currency codes, and pull point-in-time or historical rates from any MCP client, with SQL analytics over long time-series when DataCanvas is enabled. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
| Tool | Description |
|---|---|
fx_list_currencies | List all ~30 ECB-supported ISO 4217 currencies with full names |
fx_get_rates | Snapshot of all rates for a base currency at latest or a historical date |
fx_get_rate | Exchange rate for a single currency pair at latest or a historical date |
fx_convert_currency | Convert an amount between two currencies at latest or a historical rate |
fx_get_timeseries | Historical daily rates for a currency pair over a date range |
fx_dataframe_describe | List DataCanvas tables and columns staged by a prior fx_get_timeseries call |
fx_dataframe_query | Run a read-only SQL SELECT against a staged DataCanvas table |
fx_dataframe_drop | Remove one staged DataCanvas table or view (opt-in, destructive) |
The three fx_dataframe_* tools need CANVAS_PROVIDER_TYPE=duckdb — unset, they're not advertised in tools/list at all, and fx_get_timeseries returns every range inline instead. fx_dataframe_drop additionally needs FX_ENABLE_CANVAS_DROP=true.
| Resource | Description |
|---|---|
fx://currencies | All supported currencies as a stable reference document |
fx://rates/latest/{base} | Latest rates snapshot for a base currency as a stable URI |
All resource data is also reachable via tools — use fx_list_currencies or fx_get_rates for programmatic access.
fx_list_currencies tool[{ code, name }] for all ~30 ECB-scoped currencies, sorted alphabetically by codefx_get_rates toolbase_currency required; date optional (default latest, ECB data from 1999-01-04, no future dates); optional symbols array narrows the response and must name at least one coderates map (quote code → rate), the actual rate_date, and date_snapped: true when a weekend/holiday request snapped to the prior business daysymbols is valid — answered locally with a rate of 1 rather than sent upstreaminvalid_date_format, unsupported_currency, date_out_of_range, upstream_no_datafx_get_rate toolbase_currency, quote_currency required; date optional (default latest, ECB data from 1999-01-04, no future dates)rate, rate_date, and date_snapped: true when a weekend/holiday request snapped to the prior business dayinvalid_date_format, unsupported_currency, date_out_of_range, upstream_no_datafx_convert_currency toolbase_currency, quote_currency, amount (must be > 0) required; date optional (default latest, ECB data from 1999-01-04, no future dates)quote_amount (rounded to 6 decimal places), rate, rate_date, date_snapped, plus rate_type and source provenanceinvalid_date_format, unsupported_currency, date_out_of_range, upstream_no_datafx_get_timeseries toolbase_currency, quote_currency, start_date, end_date required (ECB data from 1999-01-04, no future dates, start ≤ end); optional canvas_id appends to an existing canvasrate_count is always the range total; truncated: true plus next_start_date continue the pageFX_TIMESERIES_CANVAS_THRESHOLD_DAYS (default 90 days) spill to DataCanvas when configured — response carries spilled: true, canvas_id, table_name; without DataCanvas they're paged inline insteadrate_count: 0 with an explanatory notice, distinguishable from an errorfx_dataframe_describe toolcanvas_id required (from a prior fx_get_timeseries call)kind, row_count, and column schema (name, type, nullable), plus expires_atfx_dataframe_query; needs CANVAS_PROVIDER_TYPE=duckdb — unregistered otherwisecanvas_not_found when the ID doesn't exist or has expiredfx_dataframe_query toolcanvas_id and a read-only SQL query required; row_limit optional (1–10,000, default 150)fx_get_timeseries callsrow_limit rows; truncated: true plus a notice give the ORDER BY <column> LIMIT <n> OFFSET <m> shape for the next page — ORDER BY is required for stable pagingstructuredContent keeps raw valuesCANVAS_PROVIDER_TYPE=duckdb; typed failures: canvas_not_found, missing_table, invalid_queryfx_dataframe_drop toolcanvas_id and exact table_name (from fx_dataframe_describe) requiredfx_get_timeseriesdropped: true/false depending on whether the table existedFX_ENABLE_CANVAS_DROP=true — listed with its enable hint but uncallable otherwise; also needs CANVAS_PROVIDER_TYPE=duckdbfx://currencies resourcecurrencies, count, source as application/json — the same payload as fx_list_currenciesfx://rates/latest/{base} resourcebase is an ISO 4217 currency code in the URIbase_currency, rate_date, a rates map, rate_type, and source for the latest ECB fixBuilt on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.
ECB-specific:
date_snapped surfaces when the API returns a different date than requestedAgent-friendly output:
rate_type, source, rate_date, and date_snapped so agents can reason about trust and freshnessreason fields (unsupported_currency, date_out_of_range, invalid_query, …) let callers branch on failure type, not string parsingnext_start_date, and SQL results cap at row_limit, so no call returns an unbounded payloadnotice enrichment — explains an empty series, where to continue a paged series, or which tools read a staged one, so a legitimate zero-result never reads as a failureA public instance is available at https://exchange-rates.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "streamable-http",
"url": "https://exchange-rates.caseyjhand.com/mcp"
}
}
}
No API key required — Frankfurter is keyless. Add the following to your MCP client configuration file:
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/exchange-rates-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with npx (no Bun required):
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/exchange-rates-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}
Or with Docker:
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/exchange-rates-mcp-server:latest"
]
}
}
}
To enable DataCanvas for long time-series SQL analytics — which also registers fx_dataframe_describe and fx_dataframe_query, skipped from tools/list otherwise — add CANVAS_PROVIDER_TYPE=duckdb:
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/exchange-rates-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"CANVAS_PROVIDER_TYPE": "duckdb"
}
}
}
}
For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcp
git clone https://github.com/cyanheads/exchange-rates-mcp-server.git
cd exchange-rates-mcp-server
bun install
cp .env.example .env
# edit .env as needed (all vars are optional — no keys required)
All configuration is validated at startup via Zod schemas. Environment variables:
| Variable | Description | Default |
|---|---|---|
FRANKFURTER_BASE_URL | Frankfurter API base URL. Override for local testing or a self-hosted instance. | https://api.frankfurter.dev/v1 |
FX_TIMESERIES_CANVAS_THRESHOLD_DAYS | Day range above which fx_get_timeseries spills to DataCanvas, when one is configured. | 90 |
FX_ENABLE_CANVAS_DROP | Enable the destructive fx_dataframe_drop tool. Off by default: the tool stays listed with its enable hint but is uncallable. | false |
CANVAS_PROVIDER_TYPE | Canvas engine. Set to duckdb to enable DataCanvas for fx_get_timeseries long-range spillover and to register the three fx_dataframe_* tools. At none they are skipped from tools/list. | none |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_SESSION_MODE | HTTP session mode: auto, stateful, or stateless. The server declares stateless in code — no handler here asks the client for input mid-call, so nothing needs a session to resume — and setting this variable overrides that declaration. | stateless |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_LOG_LEVEL | Log level (RFC 5424: debug, info, notice, warning, error). | info |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides including storage, session, and telemetry vars.
Build and run:
bun run rebuild
bun run start:stdio
# or
bun run start:http
Run checks and tests:
bun run devcheck # Lint, format, typecheck, security, changelog sync
bun run test # Vitest test suite
bun run lint:mcp # Validate MCP definitions against spec
docker build -t exchange-rates-mcp-server .
docker run --rm -p 3010:3010 exchange-rates-mcp-server
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/exchange-rates-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them. DuckDB native binaries are pre-built in the build stage and copied to production, keeping the production image free of build tools.
| Directory | Purpose |
|---|---|
src/index.ts | createApp() entry point — registers tools, resources, and canvas accessor. |
src/config/ | Server-specific environment variable parsing and validation with Zod. |
src/mcp-server/tools/ | Tool definitions (*.tool.ts) — fx_* tools. |
src/mcp-server/resources/ | Resource definitions — fx://currencies and fx://rates/latest/{base}. |
src/services/frankfurter/ | Frankfurter HTTP client, retry logic, and domain types. |
src/services/canvas/ | Module-level DataCanvas accessor for fx_get_timeseries spillover. |
src/utils/ | Output helpers — Markdown table-cell escaping for fx_dataframe_query. |
tests/ | Unit and integration tests mirroring src/. |
docs/ | Design document and idea notes. |
See CLAUDE.md for development guidelines and architectural rules. The short version:
try/catch in tool logicctx.log for request-scoped logging, ctx.state for tenant-scoped storagesrc/mcp-server/*/definitions/index.tsrate_type provenance in every responseIssues are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
Apache-2.0 — see LICENSE for details.
FRANKFURTER_BASE_URLdefault: https://api.frankfurter.dev/v1Frankfurter API base URL. Override for local testing or self-hosted instances.
FX_TIMESERIES_CANVAS_THRESHOLD_DAYSdefault: 90Day range above which fx_get_timeseries spills results to DataCanvas for SQL analytics.
MCP_LOG_LEVELdefault: infoSets the minimum log level for output (e.g., 'debug', 'info', 'warn').
MCP_HTTP_HOSTdefault: 127.0.0.1The hostname for the HTTP server.
MCP_HTTP_PORTdefault: 3010The port to run the HTTP server on.
MCP_HTTP_ENDPOINT_PATHdefault: /mcpThe endpoint path for the MCP server.
MCP_AUTH_MODEdefault: noneAuthentication mode to use: 'none', 'jwt', or 'oauth'.