crypto-mcp-server
# crypto-mcp-server
An MCP server exposing live cryptocurrency market data from the
[CoinGecko v3 API](https://www.coingecko.com/en/api) as 12 typed tools.
Built on the MCP Python SDK 2.x (`MCPServer`), fully asynchronous, with a
shared connection pool, client-side rate limiting, response caching, bounded
retries, and structured logging.
## Tools
| Tool | Purpose |
| --- | --- |
| `check_api_status` | Upstream health plus this server's config and cache stats |
| `list_supported_currencies` | Every accepted `vs_currency` code |
| `search_coins` | Resolve a name or symbol to a CoinGecko coin id |
| `get_coin_price` | Spot prices for many coins in many currencies at once |
| `convert_crypto_amount` | Convert a quantity of a coin into another currency |
| `get_coin_details` | Full profile: supply, ATH, 24h/7d/30d changes |
| `list_top_coins` | Ranked market table by market cap, volume, or id |
| `get_market_chart` | Historical price / market cap / volume series |
| `get_ohlc_candles` | Candlestick data |
| `get_historical_price` | Market state on one past date (needs a paid plan) |
| `get_trending_coins` | Most-searched coins of the last 24 hours |
| `get_global_market_overview` | Total market cap, volume, BTC/ETH dominance |
Every tool is read-only, returns a typed model (so clients get an
`outputSchema`), and pairs raw numerics with preformatted `*_display` strings —
models quote the display string and compute on the raw value.
## Install
```bash
uv sync
```
## Run
```bash
# stdio (default) — how MCP clients launch it
uv run crypto-mcp-server
# HTTP, for remote clients or debugging
uv run crypto-mcp-server --transport streamable-http --port 8000
# verbose, machine-readable logs
uv run crypto-mcp-server --log-level DEBUG --log-format json
```
`python -m crypto_mcp_server` works identically.
### Client configuration
```json
{
"mcpServers": {
"crypto": {
"command": "uv",
"args": ["run", "--directory", "/path/to/crypto_mcp_server", "crypto-mcp-server"],
"env": { "CRYPTO_MCP_API_KEY": "CG-xxxxxxxxxxxx" }
}
}
}
```
## Configuration
All settings come from `CRYPTO_MCP_*` environment variables and are resolved
once at startup by `Settings.from_env()`. Everything is optional — the server
runs anonymously against CoinGecko's public tier out of the box.
| Variable | Default | Meaning |
| --- | --- | --- |
| `CRYPTO_MCP_API_KEY` | *(unset)* | CoinGecko Demo or Pro key |
| `CRYPTO_MCP_API_TIER` | inferred | `public`, `demo`, or `pro` |
| `CRYPTO_MCP_BASE_URL` | follows tier | API root; override for a proxy or mock |
| `CRYPTO_MCP_TIMEOUT_SECONDS` | `15.0` | Total request timeout |
| `CRYPTO_MCP_CONNECT_TIMEOUT_SECONDS` | `5.0` | Connect timeout |
| `CRYPTO_MCP_MAX_RETRIES` | `3` | Retries after the first attempt |
| `CRYPTO_MCP_BACKOFF_BASE_SECONDS` | `0.5` | First-retry backoff factor |
| `CRYPTO_MCP_BACKOFF_MAX_SECONDS` | `8.0` | Ceiling on any single sleep |
| `CRYPTO_MCP_MAX_CONNECTIONS` | `10` | Connection pool size |
| `CRYPTO_MCP_RATE_LIMIT_PER_MINUTE` | tier default | Client-side outbound ceiling |
| `CRYPTO_MCP_CACHE_TTL_SECONDS` | `30.0` | Response cache TTL; `0` disables |
| `CRYPTO_MCP_CACHE_MAX_ENTRIES` | `512` | Cache size before LRU eviction |
| `CRYPTO_MCP_LOG_LEVEL` | `INFO` | `DEBUG`…`CRITICAL` |
| `CRYPTO_MCP_LOG_FORMAT` | `text` | `text` or `json` |
The tier is inferred from the key's presence, and the base URL follows the
tier, so a Pro user only sets `CRYPTO_MCP_API_KEY` and
`CRYPTO_MCP_API_TIER=pro`.
### A note on rate limits
The anonymous tier is throttled per source IP and shared with every other
unauthenticated caller behind it. The default client-side budgets (5/min
public, 25/min demo, 450/min pro) sit deliberately below CoinGecko's published
ceilings — measured against the live API, even 10/min drew constant 429s
without a key. For anything beyond casual use, set an API key.
## Architecture
```
server.py MCP tools: argument validation, error translation, lifespan
↓
mappers.py raw CoinGecko JSON → typed models
↓
client.py the only module that knows HTTP
↓
utils.py retry, rate limiting, TTL cache, formatting, input hygiene
```
`config.py`, `exceptions.py`, `logging_config.py`, and `models.py` are shared
by every layer. A single request flows through:
```
get_json()
└─ TTLCache.get_or_load de-duplicates concurrent identical calls
└─ retry_async exponential backoff with full jitter
└─ AsyncRateLimiter
└─ httpx2 one attempt
```
Caching sits outside retries so a retried call is stored once; the limiter sits
inside them so every physical attempt is metered.
### Error handling
Failures are expressed as a shallow hierarchy under `CryptoMCPError`
(`ToolInputError`, `RateLimitError`, `AuthenticationError`,
`ResourceNotFoundError`, `UpstreamTimeoutError`, …). Each tool is wrapped by
`@tool_handler`, which guarantees three things:
- Argument mistakes fail before any network call, with a message naming a valid
value (`order must be one of market_cap_desc, …`).
- Known failures surface as `ToolError` with their message intact.
- Anything unexpected is logged with a full traceback and returned as a generic
message — no traceback ever reaches the client.
Retries cover timeouts, connection errors, and 5xx/429 responses. When
CoinGecko supplies a `Retry-After` longer than `BACKOFF_MAX_SECONDS`, the
server **stops** rather than retrying: sleeping less than the server demanded
only earns another rejection, and honouring a 60-second window inside a tool
call would stall the session.
### Logging
Logs go to **stderr**, never stdout — under the stdio transport, stdout is the
JSON-RPC channel, and a stray write there corrupts the frame the client is
parsing and drops the session. `configure_logging` also detaches any stdout
handler it finds on the root logger.
## Tests
```bash
uv run pytest
```
109 tests, no network access: upstream behaviour is simulated with
`httpx2.MockTransport` injected into the real client, and the tool layer is
driven through a genuine in-process MCP client session, so retries, caching,
rate limiting, error mapping, and the wire protocol all run as they do in
production.
TDQS
Scored across 12 tools
Each tool targets a distinct resource and action: price, details, history, market overview, search, conversion, health check, etc. Even similar tools like get_historical_price and get_market_chart differ by single-date vs series, and get_trending_coins vs list_top_coins differ by metric. No two tools could be easily confused.
All tool names follow a consistent verb_noun snake_case pattern (list_, get_, search_, convert_, check_). There is no mixing of camelCase or inconsistent verb styles, making the API predictable and easy to navigate.
12 tools is well within the ideal 3-15 range. The server is scoped to cryptocurrency market data, and each tool serves a clear purpose without unnecessary bloat or missing essential operations.
The tool surface comprehensively covers the domain: real-time prices, historical data, OHLC, conversion, market rankings, global stats, coin discovery, and API diagnostics. There are no obvious gaps or dead ends; the historical price limitation is an external API restriction, not a missing tool.