@cyanheads/exchange-rates-mcp-server
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
Tools
Seven tools for working with ECB FX rate data — currency lookup and disambiguation, point-in-time rates and conversions, historical time-series retrieval, and SQL analytics over the DataCanvas workspace that long time-series calls produce:
| Tool | Description |
|---|---|
fx_list_currencies | List all ~30 ECB-supported ISO 4217 currencies with full names. Use before converting to disambiguate "dollars" (USD vs AUD vs CAD vs HKD vs SGD). |
fx_get_rates | Snapshot of all available rates for a base currency at latest or a historical date. Optional symbols filter for smaller responses. |
fx_get_rate | Exchange rate for a single currency pair at latest or a historical date. Surfaces date_snapped when a weekend/holiday request returns the prior business-day rate. |
fx_convert_currency | Convert an amount between any two currencies at latest or a historical rate. Cross-rates are triangulated through EUR. Returns converted amount, rate used, rate date, and whether the date was snapped. |
fx_get_timeseries | Historical daily rates for a currency pair over a date range. Short ranges (≤90 days) are returned inline; long ranges spill to DataCanvas with a canvas_id for SQL follow-up. |
fx_dataframe_describe | List DataCanvas tables and their columns from a prior fx_get_timeseries call. Required first step before fx_dataframe_query. |
fx_dataframe_query | Run a read-only SQL SELECT against a DataCanvas table produced by fx_get_timeseries. Supports aggregations, GROUP BY, window functions, and JOINs across multiple registered tables. |
fx_list_currencies
Enumerate all supported currencies before converting or querying.
- Returns
[{ code, name }]for all ~30 ECB-scoped currencies - ECB coverage fluctuates as currencies enter/exit scope — always call this tool to validate user-supplied codes rather than hard-coding a list
fx_get_rates
Full rates snapshot for a base currency in one call.
- Returns all available quote currencies at a given date (default: latest)
- Optional
symbolsparameter narrows the response to specific quote currencies - Useful for seeding bulk comparison workflows or discovering what's available
fx_get_rate
Point-in-time exchange rate for a single pair.
- Returns the rate, the actual rate date, and
date_snapped: truewhen the API silently moved a weekend/holiday request to the prior business day - Cross-rates (neither side EUR) are triangulated in a single API call — no extra round trip
- Use
fx_convert_currencywhen you need the converted amount; use this tool when you only need the rate number
fx_convert_currency
Convert an amount between any two currencies.
- Handles EUR ↔ any, any ↔ EUR, and cross-rate (USD → JPY via EUR) in one upstream call
- Returns
converted_amount,rate,rate_date,date_snapped, plusrate_typeandsourceprovenance on every response - Historical conversions supported back to 1999-01-04 (ECB launch date)
fx_get_timeseries + fx_dataframe_describe / fx_dataframe_query
Historical rate series and DataCanvas SQL analytics.
fx_get_timeseries returns a date-keyed series (business days only — ECB publishes once per business day):
- Short ranges (≤
FX_TIMESERIES_CANVAS_THRESHOLD_DAYS, default 90 days) → inlineratesmap + metadata - Long ranges → first N rows inline +
canvas_id,table_name, andtruncated: true— the full series is registered as a DuckDB-backed table
Once a canvas_id is in hand:
fx_dataframe_describe— list the tables and columns on the canvas (required beforefx_dataframe_query)fx_dataframe_query— run arbitrary SQL SELECT against the registered table; supports aggregations, GROUP BY, window functions, JOINs across tables from multiplefx_get_timeseriescalls
The canvas uses a session-scoped TTL. To continue working with a prior series, call fx_get_timeseries again with the same parameters to obtain a fresh canvas_id.
Resources and prompts
| Type | Name | Description |
|---|---|---|
| Resource | fx://currencies | All supported currencies as a stable reference document. Injectable context for clients that support resources. |
| Resource | 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.
Features
Built on @cyanheads/mcp-ts-core:
- Declarative tool and resource definitions — single file per primitive, framework handles registration and validation
- Unified error handling — handlers throw, framework catches, classifies, and formats
- Typed error contracts with recovery hints —
unsupported_currency,date_out_of_range,canvas_not_found,invalid_query - Pluggable auth:
none,jwt,oauth - Structured logging with optional OpenTelemetry tracing
- STDIO and Streamable HTTP transports
ECB FX–specific:
- Keyless access via Frankfurter — a Cloudflare-fronted ECB proxy; no API keys required
- Cross-rate triangulation: any pair works (USD → JPY fetches EUR/USD and EUR/JPY in one call, computes JPY/USD ratio)
- Weekend/holiday date semantics:
date_snappedflag surfaces when the API returns a different date than requested - ECB data covers ~30 major currencies from 1999-01-04 to present;
fx_list_currenciesalways reflects the live set - DataCanvas integration:
fx_get_timeseriesspills long ranges to DuckDB for aggregations and trend analysis - Rate provenance on every response:
rate_type: "ECB reference (mid-market)"andsource: "ECB via Frankfurter"— explicitly mid-market, not tradeable bid/ask
Agent-friendly output:
- Rate provenance on every response —
rate_type,source,rate_date, anddate_snappedso agents can reason about trust and freshness - Structured error contracts — typed
reasonfields (unsupported_currency,date_out_of_range,invalid_query, …) let callers branch on failure type, not string parsing - Discriminated DataCanvas output —
canvas_idandtruncated: truesignal when a time-series exceeds the inline limit and SQL follow-up is needed
Getting started
Public Hosted Instance
A 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"
}
}
}
Self-Hosted / Local
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, 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
Prerequisites
- Bun v1.3.0 or higher (or Node.js v24+).
- No API key — Frankfurter is free and keyless.
Installation
- Clone the repository:
git clone https://github.com/cyanheads/exchange-rates-mcp-server.git
- Navigate into the directory:
cd exchange-rates-mcp-server
- Install dependencies:
bun install
- Configure environment:
cp .env.example .env
# edit .env as needed (all vars are optional — no keys required)
Configuration
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 instead of returning inline. | 90 |
CANVAS_PROVIDER_TYPE | Canvas engine. Set to duckdb to enable DataCanvas for fx_get_timeseries long-range spillover. | none |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
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.
Running the server
Local development
-
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
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.
Project structure
| 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. |
tests/ | Unit and integration tests mirroring src/. |
docs/ | Design document and idea notes. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/index.ts - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
- ECB rates are mid-market reference rates — preserve the
rate_typeprovenance in every response
Contributing
Issues and pull requests are welcome. Run checks and tests before submitting:
bun run devcheck
bun run test
License
Apache-2.0 — see LICENSE for details.