@cyanheads/exchange-rates-mcp-server
Enables SQL analytics over long exchange rate time-series by staging historical FX data into DuckDB-backed DataCanvas tables, supporting read-only queries, aggregations, joins, and window functions.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/exchange-rates-mcp-serverconvert 100 USD to EUR"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://exchange-rates.caseyjhand.com/mcp
Overview
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.
Tools
Tool | Description |
| List all ~30 ECB-supported ISO 4217 currencies with full names |
| Snapshot of all rates for a base currency at latest or a historical date |
| Exchange rate for a single currency pair at latest or a historical date |
| Convert an amount between two currencies at latest or a historical rate |
| Historical daily rates for a currency pair over a date range |
| List DataCanvas tables and columns staged by a prior |
| Run a read-only SQL SELECT against a staged DataCanvas table |
| 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.
Resources
Resource | Description |
| All supported currencies as a stable reference document |
| 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.
Related MCP server: Realtime Exchange Rate MCP Server
Capability reference
fx_list_currencies tool
No input parameters
Returns
[{ code, name }]for all ~30 ECB-scoped currencies, sorted alphabetically by codeECB coverage shifts as currencies enter or exit scope — call this to validate a user-supplied code rather than hard-coding a list
fx_get_rates tool
base_currencyrequired;dateoptional (default latest, ECB data from 1999-01-04, no future dates); optionalsymbolsarray narrows the response and must name at least one codeReturns a
ratesmap (quote code → rate), the actualrate_date, anddate_snapped: truewhen a weekend/holiday request snapped to the prior business dayNaming the base currency in
symbolsis valid — answered locally with a rate of 1 rather than sent upstreamTyped failures:
invalid_date_format,unsupported_currency,date_out_of_range,upstream_no_data
fx_get_rate tool
base_currency,quote_currencyrequired;dateoptional (default latest, ECB data from 1999-01-04, no future dates)Returns
rate,rate_date, anddate_snapped: truewhen a weekend/holiday request snapped to the prior business dayCross-rates (neither side EUR) triangulate through EUR in one upstream call; a same-currency pair returns a rate of 1 without reaching the API, still dated to the real publication day
Typed failures:
invalid_date_format,unsupported_currency,date_out_of_range,upstream_no_data
fx_convert_currency tool
base_currency,quote_currency,amount(must be > 0) required;dateoptional (default latest, ECB data from 1999-01-04, no future dates)Handles EUR↔any, any↔EUR, and cross-rate pairs (e.g. USD→JPY) in a single upstream call
Returns
quote_amount(rounded to 6 decimal places),rate,rate_date,date_snapped, plusrate_typeandsourceprovenanceTyped failures:
invalid_date_format,unsupported_currency,date_out_of_range,upstream_no_data
fx_get_timeseries tool
base_currency,quote_currency,start_date,end_daterequired (ECB data from 1999-01-04, no future dates, start ≤ end); optionalcanvas_idappends to an existing canvasInline results page at 500 publication days —
rate_countis always the range total;truncated: trueplusnext_start_datecontinue the pageRanges over
FX_TIMESERIES_CANVAS_THRESHOLD_DAYS(default 90 days) spill to DataCanvas when configured — response carriesspilled: true,canvas_id,table_name; without DataCanvas they're paged inline insteadA same-currency pair returns a rate of 1 on each real ECB publication day in range, not a synthetic Mon–Fri loop
An empty range (only weekends/holidays) returns
rate_count: 0with an explanatorynotice, distinguishable from an error
fx_dataframe_describe tool
canvas_idrequired (from a priorfx_get_timeseriescall)Returns each staged table's
kind,row_count, and column schema (name,type,nullable), plusexpires_atRequired first step before
fx_dataframe_query; needsCANVAS_PROVIDER_TYPE=duckdb— unregistered otherwisecanvas_not_foundwhen the ID doesn't exist or has expired
fx_dataframe_query tool
canvas_idand a read-only SQLqueryrequired;row_limitoptional (1–10,000, default 150)Supports aggregations, GROUP BY, window functions, and JOINs across tables from multiple
fx_get_timeseriescallsReturns at most
row_limitrows;truncated: trueplus anoticegive theORDER BY <column> LIMIT <n> OFFSET <m>shape for the next page — ORDER BY is required for stable pagingMarkdown table cells are escaped so pipes, angle brackets, and line breaks stay inside their cell;
structuredContentkeeps raw valuesNeeds
CANVAS_PROVIDER_TYPE=duckdb; typed failures:canvas_not_found,missing_table,invalid_query
fx_dataframe_drop tool
canvas_idand exacttable_name(fromfx_dataframe_describe) requiredRemoves one staged table or view; ECB rate data is untouched and the series can be re-staged via
fx_get_timeseriesReturns
dropped: true/falsedepending on whether the table existedDisabled unless
FX_ENABLE_CANVAS_DROP=true— listed with its enable hint but uncallable otherwise; also needsCANVAS_PROVIDER_TYPE=duckdb
fx://currencies resource
No parameters; returns
currencies,count,sourceasapplication/json— the same payload asfx_list_currenciesListed as a single static resource
fx://rates/latest/{base} resource
baseis an ISO 4217 currency code in the URIReturns
base_currency,rate_date, aratesmap,rate_type, andsourcefor the latest ECB fixListed with four sample URIs (EUR, USD, GBP, JPY) as discovery hints
Features
Built 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:
Keyless access via Frankfurter — a Cloudflare-fronted ECB proxy; no API keys required
Cross-rate triangulation: any pair works — USD → JPY is one upstream call, cross-rated through EUR on Frankfurter's side
Weekend/holiday date semantics:
date_snappedsurfaces when the API returns a different date than requestedIdentity pairs never reach the upstream API: a currency against itself returns a rate of 1, dated to the day the ECB actually published for that currency rather than to the calendar date requested
Long time-series spill to DataCanvas (DuckDB) when enabled, for SQL aggregation over the full range
Agent-friendly output:
Rate provenance on every response —
rate_type,source,rate_date, anddate_snappedso agents can reason about trust and freshnessStructured error contracts — typed
reasonfields (unsupported_currency,date_out_of_range,invalid_query, …) let callers branch on failure type, not string parsingBounded responses — inline time-series pages continue from
next_start_date, and SQL results cap atrow_limit, so no call returns an unbounded payloadSuccess-path
noticeenrichment — 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 failure
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 — 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/mcpPrerequisites
Bun v1.4.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.gitNavigate into the directory:
cd exchange-rates-mcp-serverInstall dependencies:
bun installConfigure 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 API base URL. Override for local testing or a self-hosted instance. |
|
| Day range above which |
|
| Enable the destructive |
|
| Canvas engine. Set to |
|
| Transport: |
|
| Port for HTTP server. |
|
| HTTP session mode: |
|
| Auth mode: |
|
| Log level (RFC 5424: |
|
| Enable OpenTelemetry instrumentation. |
|
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:httpRun 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-serverThe 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 |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| Tool definitions ( |
| Resource definitions — |
| Frankfurter HTTP client, retry logic, and domain types. |
| Module-level DataCanvas accessor for |
| Output helpers — Markdown table-cell escaping for |
| Unit and integration tests mirroring |
| 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 logicUse
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storageRegister new tools and resources via the barrels in
src/mcp-server/*/definitions/index.tsWrap 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 are welcome. Run checks and tests before submitting:
bun run devcheck
bun run testLicense
Apache-2.0 — see LICENSE for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Exchange MCP — wraps the Frankfurter currency exchange API (free, no auth)
Frankfurter MCP — wraps Frankfurter API (api.frankfurter.dev)
ExchangeRate MCP — wraps open.er-api.com (free, no auth)
Convert currencies on the daily European Central Bank reference rates. No API key, no signup.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceReal-time currency exchange rates and crypto prices via MCP. Convert between 60+ fiat currencies and 30+ cryptocurrencies with multi-source failover. No API keys needed for upstream data.9 npmISC
- AlicenseAqualityBmaintenanceProvides real-time foreign-exchange rates, historical data, and multi-currency lookups to MCP-compatible AI coding assistants like Claude Code and Cursor.4130 npmMIT
- -licenseNot gradedqualityNot gradedmaintenanceMCP server that provides real exchange-rate data from the European Central Bank, including latest rates, currency conversion, historical rates, and time series.-
- AlicenseNot gradedqualityAmaintenanceMCP server for the Frankfurter exchange-rate API, enabling currency conversion and rate queries.118 npm3MIT