Skip to main content
Glama
cyanheads

@cyanheads/exchange-rates-mcp-server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

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 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

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/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.

Latest Blog Posts

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