Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions 6_mcp/community_contributions/iyanuashiri/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Frankfurter MCP — Currency Conversion Server & Client

A Model Context Protocol (MCP) server and client for live currency conversion using the [Frankfurter API](https://www.frankfurter.dev/v1/) (ECB rates, no API key needed).

---

## Files

- `server.py` — MCP server exposing currency tools via stdio
- `client.py` — MCP client that connects to the server and calls its tools

---

## Tools

| Tool | Description |
|---|---|
| `convert_currency` | Convert an amount from one currency to another |
| `get_exchange_rate` | Get the mid-market rate between two currencies |
| `list_supported_currencies` | List all ISO 4217 codes supported by Frankfurter |

---

## Usage

### Run the server (stdio, for MCP clients like Claude Desktop / Cursor)

```bash
uv run server.py
```

### Run the client demo

```bash
uv run client.py
```

This will list available tools, print supported currencies, and run a couple of sample conversions.

### Use the client in your own code

```python
import asyncio
from client import convert_currency, get_exchange_rate, list_supported_currencies

async def main():
print(await get_exchange_rate("USD", "EUR"))
print(await convert_currency(250, "GBP", "JPY"))
print(await list_supported_currencies())

asyncio.run(main())
```

---

## Connecting to Claude Desktop / Cursor

Add this to your MCP config (`mcp.json`):

```json
{
"mcpServers": {
"frankfurter": {
"command": "uv",
"args": ["run", "/path/to/server.py"]
}
}
}
```

Replace `/path/to/server.py` with the absolute path to `server.py`.

---

## Requirements

- Python 3.10+
- `uv` ([install guide](https://docs.astral.sh/uv/getting-started/installation/))
- Dependencies: `httpx`, `mcp[cli]`

Install deps:

```bash
uv pip install httpx "mcp[cli]"
```

---

## Notes

- Rates are sourced from the European Central Bank (ECB) via Frankfurter — updated on business days.
- No API key required.
- Currency codes must be valid ISO 4217 three-letter codes (e.g. `USD`, `EUR`, `NGN`).
86 changes: 86 additions & 0 deletions 6_mcp/community_contributions/iyanuashiri/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""MCP client for the Frankfurter currency server.

Connects to server.py via stdio and exposes helpers for:
- listing available tools
- calling any tool directly
- three convenience wrappers matching the server's tools
"""

import asyncio
import json
from pathlib import Path

import mcp
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

# Point at server.py sitting next to this file
_SERVER_PATH = str(Path(__file__).parent / "server.py")

params = StdioServerParameters(
command="uv",
args=["run", _SERVER_PATH],
env=None,
)


async def list_tools():
"""Return all tools exposed by the Frankfurter MCP server."""
async with stdio_client(params) as streams:
async with mcp.ClientSession(*streams) as session:
await session.initialize()
result = await session.list_tools()
return result.tools


async def call_tool(tool_name: str, tool_args: dict):
"""Call any tool on the server by name with the given arguments."""
async with stdio_client(params) as streams:
async with mcp.ClientSession(*streams) as session:
await session.initialize()
result = await session.call_tool(tool_name, tool_args)
return result


# --- Convenience wrappers ---

async def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
result = await call_tool(
"convert_currency",
{"amount": amount, "from_currency": from_currency, "to_currency": to_currency},
)
return result.content[0].text


async def get_exchange_rate(from_currency: str, to_currency: str) -> str:
result = await call_tool(
"get_exchange_rate",
{"from_currency": from_currency, "to_currency": to_currency},
)
return result.content[0].text


async def list_supported_currencies() -> str:
result = await call_tool("list_supported_currencies", {})
return result.content[0].text


# --- Quick smoke-test when run directly ---

async def _demo():
print("=== Tools available ===")
for tool in await list_tools():
print(f" {tool.name}: {tool.description}")

print("\n=== Supported currencies ===")
print(await list_supported_currencies())

print("\n=== Exchange rate: USD → EUR ===")
print(await get_exchange_rate("USD", "EUR"))

print("\n=== Convert 100 USD to GBP ===")
print(await convert_currency(100, "USD", "GBP"))


if __name__ == "__main__":
asyncio.run(_demo())
136 changes: 136 additions & 0 deletions 6_mcp/community_contributions/iyanuashiri/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Frankfurter MCP server — Frankfurter v1 API (no API key).

Uses https://api.frankfurter.dev/v1/ (documented v1: base + symbols), same
contract as api.frankfurter.app/latest?from=&to= but without relying on the
legacy host/redirect. See https://www.frankfurter.dev/v1/
"""

from typing import Any

import httpx
from mcp.server.fastmcp import FastMCP

# Official v1 base — matches e.g. GET .../v1/latest?base=USD&symbols=EUR
FRANKFURTER_V1 = "https://api.frankfurter.dev/v1"
USER_AGENT = "frankfurter-mcp/1.0"

mcp = FastMCP("frankfurter-mcp")


async def _frankfurter_get(path: str, params: dict[str, Any]) -> dict[str, Any] | None:
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
url = f"{FRANKFURTER_V1}{path}"
async with httpx.AsyncClient() as client:
try:
response = await client.get(
url,
params=params,
headers=headers,
timeout=30.0,
)
response.raise_for_status()
return response.json()
except Exception:
return None


def _normalize_code(code: str) -> str | None:
c = code.strip().upper()
if len(c) != 3 or not c.isalpha():
return None
return c


@mcp.tool()
async def convert_currency(amount: float, from_currency: str, to_currency: str) -> str:
"""Convert an amount between currencies using live ECB-based rates.

Args:
amount: Amount in the source currency (must be >= 0).
from_currency: Three-letter ISO 4217 code (e.g. USD, EUR, GBP).
to_currency: Three-letter ISO 4217 code for the result.
"""
if amount < 0:
return "Amount must be non-negative."

from_c = _normalize_code(from_currency)
to_c = _normalize_code(to_currency)
if not from_c or not to_c:
return "Currency codes must be three letters (ISO 4217), e.g. USD, EUR."

if from_c == to_c:
return f"{amount} {from_c} = {amount} {to_c} (same currency)."

data = await _frankfurter_get(
"/latest", {"base": from_c, "symbols": to_c}
)
if not data or "rates" not in data:
return (
"Could not fetch exchange rates. "
"Check that both currency codes are supported (use list_supported_currencies)."
)

rates = data["rates"]
if to_c not in rates:
return f"No rate returned for {from_c} → {to_c}."

rate = float(rates[to_c])
converted = amount * rate
date = data.get("date", "unknown")
return (
f"{amount} {from_c} = {converted:.6g} {to_c}\n"
f"Rate: 1 {from_c} = {rate} {to_c} (ECB via Frankfurter, date: {date})"
)


@mcp.tool()
async def get_exchange_rate(from_currency: str, to_currency: str) -> str:
"""Return the mid-market rate: how much target currency equals one unit of source.

Args:
from_currency: Three-letter ISO 4217 source currency code.
to_currency: Three-letter ISO 4217 target currency code.
"""
from_c = _normalize_code(from_currency)
to_c = _normalize_code(to_currency)
if not from_c or not to_c:
return "Currency codes must be three letters (ISO 4217), e.g. USD, EUR."

if from_c == to_c:
return f"1 {from_c} = 1 {to_c} (same currency)."

data = await _frankfurter_get(
"/latest", {"base": from_c, "symbols": to_c}
)
if not data or "rates" not in data:
return (
"Could not fetch the rate. "
"Check currency codes (use list_supported_currencies)."
)

rates = data["rates"]
if to_c not in rates:
return f"No rate for {from_c} → {to_c}."

rate = float(rates[to_c])
date = data.get("date", "unknown")
return f"1 {from_c} = {rate} {to_c} (Frankfurter / ECB, date: {date})"


@mcp.tool()
async def list_supported_currencies() -> str:
"""List ISO 4217 currency codes supported by the Frankfurter API for conversion."""
data = await _frankfurter_get("/currencies", {})
if not data:
return "Could not fetch the currency list."

lines = [f"{code}: {name}" for code, name in sorted(data.items())]
return "Supported currencies:\n" + "\n".join(lines)


def main() -> None:
mcp.run(transport="stdio")


if __name__ == "__main__":
main()