@cyanheads/exchange-rates-mcp-server
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:
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. Optional |
| 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. Short ranges (≤90 days) are returned inline; when DataCanvas is enabled, long ranges spill to it with a |
| 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)
Optional
symbolsparameter narrows the response to specific quote currenciesNaming 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 → first N rows inline +
canvas_id,table_name, andspilled: true— the full series is registered as a DuckDB-backed table. WithoutCANVAS_PROVIDER_TYPE=duckdba long range comes back inline withspilled: falseand anoticesaying the threshold was crossed but no canvas was configuredRequesting 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
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 |
| 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 setIdentity 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 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 inlineSuccess-path
noticeenrichment — explains an empty series or a long range that stayed inline, 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.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.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 |
| 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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/exchange-rates-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server