npm @cyanheads/exchange-rates-mcp-serverstreamable-httpupdated 14d ago
@cyanheads/exchange-rates-mcp-server Convert currencies, get FX rates, and query historical ECB exchange rate data via MCP. STDIO or Streamable HTTP. 7 Tools β’ 1 Opt-in Tool β’ 2 Resources
What can you do with 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 |
|---|---|
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; listing the base itself returns a rate of 1 for it. |
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, never including a date outside it. Short ranges (β€90 days) are returned inline; when DataCanvas is enabled, long ranges spill to it 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. Needs CANVAS_PROVIDER_TYPE=duckdb. |
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. Needs CANVAS_PROVIDER_TYPE=duckdb. |
fx_dataframe_drop |
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 CANVAS_PROVIDER_TYPE=duckdb and FX_ENABLE_CANVAS_DROP=true; disabled otherwise. |
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 - Naming 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 failing - 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
- 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 pair - 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
quote_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, 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 configured - Requesting 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 | 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,missing_table,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 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 requested - ECB data covers ~30 major currencies from 1999-01-04 to present;
fx_list_currenciesalways reflects the live set - Identity 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 requested - DataCanvas integration: when enabled,
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 β
spilled: truepluscanvas_idsignal when a time-series was staged for SQL follow-up rather than returned inline - Success-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/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, 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. .env.example and the Dockerfile both set stateless β no handler here asks the client for input mid-call, so nothing needs a session to resume. |
auto (resolves to stateful) |
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.
Install
Add exchange rates mcp server to your client. Pick the one you use.
{
"servers": {
"exchange-rates-mcp-server": {
"type": "http",
"url": "https://exchange-rates.caseyjhand.com/mcp"
}
}
}Add to `.vscode/mcp.json` in your workspace.
claude mcp add exchange-rates-mcp-server -- npx -y @cyanheads/exchange-rates-mcp-servercodex mcp add exchange-rates-mcp-server -- npx -y @cyanheads/exchange-rates-mcp-serveramp mcp add exchange-rates-mcp-server -- npx -y @cyanheads/exchange-rates-mcp-server{
"mcpServers": {
"exchange-rates-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}Add to `claude_desktop_config.json`, then restart Claude Desktop.
{
"mcpServers": {
"exchange-rates-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}Add to `~/.cursor/mcp.json`, or `.cursor/mcp.json` for a single project.
{
"mcpServers": {
"exchange-rates-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}Add to `~/.codeium/windsurf/mcp_config.json`.
{
"mcpServers": {
"exchange-rates-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}Add to `cline_mcp_settings.json` via the MCP Servers panel.
{
"mcpServers": {
"exchange-rates-mcp-server": {
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}Add to `~/.gemini/settings.json`.
{
"mcpServers": {
"exchange-rates-mcp-server": {
"type": "local",
"command": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
],
"tools": [
"*"
]
}
}
}Add to `~/.copilot/mcp-config.json`, or run `/mcp add` inside the CLI.
{
"context_servers": {
"exchange-rates-mcp-server": {
"command": {
"path": "npx",
"args": [
"-y",
"@cyanheads/exchange-rates-mcp-server"
]
}
}
}
}Add to your Zed `settings.json`.
npx -y @cyanheads/exchange-rates-mcp-serverRun `goose configure`, choose **Add Extension β Command-line Extension**, and paste this command.
8 tools
exchange rates mcp server exposes 8 tools to a connected agent.
- 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; listing the base itself returns a rate of 1 for it.
- 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, never including a date outside it. Short ranges (β€90 days) are returned inline; when DataCanvas is enabled, long ranges spill to it 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`. Needs `CANVAS_PROVIDER_TYPE=duckdb`.
- 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. Needs `CANVAS_PROVIDER_TYPE=duckdb`.
- fx_dataframe_drop
- 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 `CANVAS_PROVIDER_TYPE=duckdb` and `FX_ENABLE_CANVAS_DROP=true`; disabled otherwise.
Score
80 / 100
Excellent
- Documentation25/25
- Maintenance25/25
- Trust6/20
- Capability9/15
- Install experience15/15
- Documents what it does and how to connect
- Has a resolvable package or endpoint
- Exposes at least one tool, prompt or resource
- README has substantive content
- Includes a code example
- Documents its configuration
- Mentions credentials or security posture
- Last commit 7 days ago
- Has a release history
- Repository is not archived
- No licence detected
- Namespace verified in the official MCP registry
- Claimed by its owner
- Published under an organisation
- 8 tool(s) documented
- Provides prompt templates
- Provides resources
- 18 documented install method(s)
- Published to a package registry
- Offers a hosted endpoint β no local install
Version history
| Versions | Published |
|---|---|
| 0.3.1Latest | Aug 25, 2026 |
| 0.3.0 | Aug 25, 2026 |
| 0.2.1 | Jul 31, 2026 |
| 0.2.0 | Jul 31, 2026 |
| 0.1.5 | Jun 20, 2026 |
| 0.1.4 | Jun 12, 2026 |
| 0.1.3 | Jun 7, 2026 |
| 0.1.2 | Jun 6, 2026 |
| 0.1.1 | Jun 5, 2026 |