io.github.cyanheads/exchange-rates-mcp-server icon

exchange-rates-mcp-server

by Cyanheads

io.github.cyanheads/exchange-rates-mcp-server

Convert currencies, get FX rates, and query historical ECB exchange rate data.

Version 0.2.1 · latest
Data
Local
View source

exchange-rates-mcp-server · v0.2.1 (latest)

by Cyanheads

74

@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 • 2 Resources

Install in Cursor


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.

The two 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_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 symbols parameter narrows the response to specific quote currencies
  • Naming the base currency in symbols is 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: true when 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_date and date_snapped read the same as for any other pair
  • Use fx_convert_currency when 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, plus rate_type and source provenance 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) → inline rates map + metadata
  • Long ranges, when DataCanvas is enabled → first N rows inline + canvas_id, table_name, and spilled: true — the full series is registered as a DuckDB-backed table. Without CANVAS_PROVIDER_TYPE=duckdb a long range comes back inline with spilled: false and a notice saying 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:

  1. fx_dataframe_describe — list the tables and columns on the canvas (required before fx_dataframe_query)
  2. fx_dataframe_query — run arbitrary SQL SELECT against the registered table; supports aggregations, GROUP BY, window functions, JOINs across tables from multiple fx_get_timeseries calls

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_snapped flag surfaces when the API returns a different date than requested
  • ECB data covers ~30 major currencies from 1999-01-04 to present; fx_list_currencies always reflects the live set
  • Identity pairs never surface an upstream rejection: fx_get_rate, fx_get_rates, and fx_get_timeseries all 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_timeseries spills long ranges to DuckDB for aggregations and trend analysis
  • Rate provenance on every response: rate_type: "ECB reference (mid-market)" and source: "ECB via Frankfurter" — explicitly mid-market, not tradeable bid/ask

Agent-friendly output:

  • Rate provenance on every response — rate_type, source, rate_date, and date_snapped so agents can reason about trust and freshness
  • Structured error contracts — typed reason fields (unsupported_currency, date_out_of_range, invalid_query, …) let callers branch on failure type, not string parsing
  • Discriminated DataCanvas output — spilled: true plus canvas_id signal when a time-series was staged for SQL follow-up rather than returned inline
  • Success-path notice enrichment — 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

  1. Clone the repository:
git clone https://github.com/cyanheads/exchange-rates-mcp-server.git
  1. Navigate into the directory:
cd exchange-rates-mcp-server
  1. Install dependencies:
bun install
  1. 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
CANVAS_PROVIDER_TYPE Canvas engine. Set to duckdb to enable DataCanvas for fx_get_timeseries long-range spillover and to register the two 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_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/catch in tool logic
  • Use ctx.log for request-scoped logging, ctx.state for 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_type provenance 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.