Skip to main content
Glama
akshaygp18

crypto-mcp-server

by akshaygp18
README.md
# 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

A4.4/5.0

Scored across 12 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness5/5

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.

Maintenance

ActivitySlowing
ResponsivenessNo issues