@cyanheads/exchange-rates-mcp-server
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
Tools
Eight 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. Five are advertised by default; the three fx_dataframe_* tools need CANVAS_PROVIDER_TYPE=duckdb, and the destructive one among them additionally needs FX_ENABLE_CANVAS_DROP=true.
The three fx_dataframe_* tools require DataCanvas. With CANVAS_PROVIDER_TYPE unset (the default) they are not advertised in tools/list at all, so a client never sees a tool it cannot call; the HTTP landing page still lists them as disabled cards hinting CANVAS_PROVIDER_TYPE=duckdb, so operators can tell they exist. In that mode fx_get_timeseries returns every range inline, paged at 500 publication days:
Tool | Description |
| 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). |
| Snapshot of all available rates for a base currency at latest or a historical date. Surfaces |
| Exchange rate for a single currency pair at latest or a historical date. Surfaces |
| 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. |
| Historical daily rates for a currency pair over a date range, never including a date outside it. Inline results come back in pages of 500 publication days, continued with |
| List DataCanvas tables and their columns from a prior |
| Run a read-only SQL SELECT against a DataCanvas table produced by |
| Permanently remove one staged table or view from a DataCanvas. Deletes staged analytical data only — ECB rate data is untouched and the series can be re-staged. Needs |
fx_list_currencies
Enumerate all supported currencies before converting or querying.
Returns
[{ code, name }]for all ~30 ECB-scoped currenciesECB 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), the actual
rate_date, anddate_snapped: truewhen the API silently moved a weekend/holiday request to the prior business day — alwaysfalsewhendateis omittedOptional
symbolsparameter narrows the response to specific quote currencies; when sent it must name at least one code — omit it to get every currencyNaming the base currency in
symbolsis valid — it is answered locally with a rate of 1 rather than sent upstream, which keeps a self-quote from failingUseful 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 dayCross-rates (neither side EUR) are triangulated in a single API call — no extra round trip
A same-currency pair returns a rate of 1 without a self-quote reaching the API, but still reports the publication date the ECB actually had for that currency, so
rate_dateanddate_snappedread the same as for any other pairUse
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
quote_amount,rate,rate_date,date_snapped, plusrate_typeandsourceprovenance on every responseHistorical 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 + metadataLong ranges, when DataCanvas is enabled → a preview inline +
canvas_id,table_name,spilled: true, and anoticenamingfx_dataframe_describethenfx_dataframe_query— the full series is registered as a DuckDB-backed tableLong ranges without DataCanvas → returned inline with
spilled: false, paged (below), and anoticesaying the threshold was crossed but no canvas was configuredInline results are paged at 500 publication days.
rate_countis always the total for the requested range; when a page is cut short the response carriestruncated: trueandnext_start_date. Call again withstart_dateset tonext_start_dateand the sameend_datefor the next page — the final page hastruncated: falseand nonext_start_dateRequesting the same currency on both sides returns a rate of 1 on each publication day in the range, taken from the ECB's real calendar rather than a synthetic Mon–Fri loop
The response never carries a date outside the requested range. Frankfurter snaps a range that opens on a weekend or bank holiday back to the prior publication day; those rows are dropped, so start_date and end_date always sit inside the window you asked for. A range covering only non-publication days therefore returns an empty rates map with rate_count: 0 and a notice explaining that the ECB published nothing in that window — distinguishable from an error.
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. Returns at mostrow_limitrows (default 150, max 10,000) on both response surfaces; past that,truncated: trueand anoticegive theORDER BY <column> LIMIT <n> OFFSET <m>shape for the next page (ORDER BYis required for stable paging). Cell values are escaped in the Markdown table so pipes, angle brackets, and line breaks stay inside their cell;structuredContentkeeps the raw values
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.
Related MCP server: currency-exchange-mcp
Resources and prompts
Type | Name | Description |
Resource |
| All supported currencies as a stable reference document. Injectable context for clients that support resources. |
Resource |
| 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,missing_table,invalid_queryPluggable auth:
none,jwt,oauthStructured 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 is one upstream call, cross-rated through EUR on Frankfurter's side
Weekend/holiday date semantics:
date_snappedflag surfaces when the API returns a different date than requestedECB data covers ~30 major currencies from 1999-01-04 to present;
fx_list_currenciesalways reflects the live set, and anunsupported_currencyrejection lists that live set inline so a caller can correct the code without a second callIdentity pairs never surface an upstream rejection:
fx_get_rate,fx_get_rates, andfx_get_timeseriesall return a rate of 1 for a currency against itself, dated to the days the ECB actually published for that currency rather than to the calendar dates requestedDataCanvas integration: when enabled,
fx_get_timeseriesspills long ranges to DuckDB for aggregations and trend analysisRate 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 snapshot, rate, and conversion 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 parsingDiscriminated DataCanvas output —
spilled: truepluscanvas_idsignal when a time-series was staged for SQL follow-up rather than returned inlineBounded 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 and pull requests 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
- FlicenseCqualityFmaintenanceAn MCP server providing real-time currency conversion and exchange rate data through the Frankfurter API, enabling users to convert currencies, fetch latest or historical rates, and list available currencies.435-
- 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.3 npmISC
- AlicenseAqualityBmaintenanceProvides real-time foreign-exchange rates, historical data, and multi-currency lookups to MCP-compatible AI coding assistants like Claude Code and Cursor.453 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.-